Skip to content

Commit ea1839d

Browse files
author
Robert Szefler
committed
Support for aligning Slack message grouping intervals
1 parent 50f7f84 commit ea1839d

File tree

4 files changed

+85
-15
lines changed

4 files changed

+85
-15
lines changed

src/robusta/core/sinks/sink_base.py

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
import time
33
from abc import abstractmethod, ABC
44
from collections import defaultdict
5+
from datetime import datetime
56
from typing import Any, List, Dict, Tuple, DefaultDict, Optional
67

7-
from pydantic import BaseModel, Field
8+
from pydantic import BaseModel
89

910
from robusta.core.model.k8s_operation_type import K8sOperationType
1011
from robusta.core.reporting.base import Finding
@@ -33,20 +34,63 @@ def register_notification(self, interval: int, threshold: int) -> bool:
3334

3435
class NotificationSummary(BaseModel):
3536
message_id: Optional[str] = None # identifier of the summary message
36-
start_ts: float = Field(default_factory=lambda: time.time()) # Timestamp of the first notification
37+
start_ts: float = None
38+
end_ts: float = None
3739
# Keys for the table are determined by grouping.notification_mode.summary.by
3840
summary_table: DefaultDict[KeyT, List[int]] = None
3941

40-
def register_notification(self, summary_key: KeyT, resolved: bool, interval: int):
41-
now_ts = time.time()
42+
def register_notification(self, summary_key: KeyT, resolved: bool, interval: int, aligned: bool):
43+
now_dt = datetime.now()
44+
now_ts = int(now_dt.timestamp())
4245
idx = 1 if resolved else 0
43-
if now_ts - self.start_ts > interval or not self.summary_table:
44-
# Expired or the first summary ever for this group_key, reset the data
46+
if not self.end_ts or now_ts > self.end_ts:
47+
# Group expired or the first summary ever for this group_key, reset the data
4548
self.summary_table = defaultdict(lambda: [0, 0])
46-
self.start_ts = now_ts
49+
self.start_ts, self.end_ts = self.calculate_interval_boundaries(interval, aligned, now_dt)
4750
self.message_id = None
4851
self.summary_table[summary_key][idx] += 1
4952

53+
@classmethod
54+
def calculate_interval_boundaries(cls, interval: int, aligned: bool, now_dt: datetime) -> Tuple[float, float]:
55+
now_ts = int(now_dt.timestamp())
56+
if aligned:
57+
# This handles leap seconds by adjusting the length of the last interval in the
58+
# day to the actual end of day. Note leap seconds are expected to almost always be +1,
59+
# but it's also expected that some -1's will appear in the (far) future, and it's
60+
# not out of the realm of possibility that somewhat larger adjustments will happen
61+
# before the leap second adjustment is phased out around 2035.
62+
63+
start_of_this_day_ts, end_of_this_day_ts = cls.get_day_boundaries(now_dt)
64+
start_ts = now_ts - (now_ts - start_of_this_day_ts) % interval
65+
end_ts = start_ts + interval
66+
if (
67+
end_ts > end_of_this_day_ts # negative leap seconds
68+
or end_of_this_day_ts - end_ts < interval # positive leap seconds
69+
):
70+
end_ts = end_of_this_day_ts
71+
else:
72+
start_ts = now_ts
73+
end_ts = now_ts + interval
74+
return start_ts, end_ts
75+
76+
@staticmethod
77+
def get_day_boundaries(now_dt: datetime) -> Tuple[int, int]:
78+
# Note: we assume day boundaries according to the timezone configured on the pod
79+
# running Robusta runner. A caveat of this is that Slack will show times according
80+
# to the client's timezone, which may differ.
81+
82+
start_of_this_day = now_dt.replace(hour=0, minute=0, second=0, microsecond=0)
83+
start_of_this_day_ts = int(start_of_this_day.timestamp())
84+
try:
85+
end_of_this_day = start_of_this_day.replace(day=start_of_this_day.day + 1)
86+
except ValueError: # end of month
87+
try:
88+
end_of_this_day = start_of_this_day.replace(month=start_of_this_day.month + 1, day=1)
89+
except ValueError: # end of year
90+
end_of_this_day = start_of_this_day.replace(year=start_of_this_day.year + 1, month=1, day=1)
91+
end_of_this_day_ts = int(end_of_this_day.timestamp())
92+
return start_of_this_day_ts, end_of_this_day_ts
93+
5094

5195
class SinkBase(ABC):
5296
grouping_enabled: bool

src/robusta/core/sinks/sink_base_params.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,21 @@ def validate_exactly_one_defined(cls, values: Dict):
9191
class GroupingParams(BaseModel):
9292
group_by: GroupingAttributeSelectorListT = ["cluster"]
9393
interval: int = 15*60 # in seconds
94+
aligned: bool = False
9495
notification_mode: Optional[NotificationModeParams]
9596

9697
@root_validator
97-
def validate_notification_mode(cls, values: Dict):
98-
if values is None:
99-
return {"summary": SummaryNotificationModeParams()}
98+
def validate_interval_alignment(cls, values: Dict):
99+
if values["aligned"]:
100+
if values["interval"] < 24 * 3600:
101+
if (24 * 3600) % values["interval"]:
102+
raise ValueError(f'Unable to properly align time interval of {values["interval"]} seconds')
103+
else:
104+
# TODO do we also want to support automatically aligning intervals longer than
105+
# a day? Using month/year boundaries? This would require additionally handling
106+
# leap years and daytime saving, just as we handle leap seconds in
107+
# NotificationSummary.register_notification
108+
raise ValueError(f"Automatically aligning time intervals longer than 24 hours is not supported")
100109
return values
101110

102111

src/robusta/core/sinks/slack/slack_sink.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from robusta.core.model.env_vars import ROBUSTA_UI_DOMAIN
2-
from robusta.core.reporting.base import Finding, FindingStatus
3-
from robusta.core.sinks.sink_base import NotificationGroup, NotificationSummary, SinkBase
2+
from robusta.core.reporting.base import Finding
3+
from robusta.core.sinks.sink_base import SinkBase
44
from robusta.core.sinks.slack.slack_sink_params import SlackSinkConfigWrapper, SlackSinkParams
55
from robusta.integrations import slack as slack_module
66

@@ -37,7 +37,6 @@ def handle_notification_grouping(self, finding: Finding, platform_enabled: bool)
3737
finding_data["cluster"] = self.cluster_name
3838
resolved = finding.title.startswith("[RESOLVED]")
3939

40-
# 1. Notification accounting
4140
group_key, group_header = self.get_group_key_and_header(
4241
finding_data, self.params.grouping.group_by
4342
)
@@ -48,7 +47,7 @@ def handle_notification_grouping(self, finding: Finding, platform_enabled: bool)
4847
)
4948
notification_summary = self.summaries[group_key]
5049
notification_summary.register_notification(
51-
summary_key, resolved, self.params.grouping.interval
50+
summary_key, resolved, self.params.grouping.interval, self.params.grouping.aligned
5251
)
5352
slack_thread_ts = self.slack_sender.send_or_update_summary_message(
5453
group_header,
@@ -75,6 +74,5 @@ def handle_notification_grouping(self, finding: Finding, platform_enabled: bool)
7574
finding, self.params, platform_enabled, thread_ts=slack_thread_ts
7675
)
7776

78-
7977
def get_timeline_uri(self, account_id: str, cluster_name: str) -> str:
8078
return f"{ROBUSTA_UI_DOMAIN}/graphs?account_id={account_id}&cluster={cluster_name}"

tests/test_slack_grouping.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import pytest
2+
3+
from datetime import datetime, timezone
4+
5+
from robusta.core.sinks.sink_base import NotificationSummary
6+
7+
8+
utc = timezone.utc
9+
10+
11+
class TestNotificationSummary:
12+
@pytest.mark.parametrize("input_dt,expected_output", [
13+
(datetime(2024, 6, 25, 12, 15, 33, tzinfo=utc), (1719273600, 1719360000)),
14+
(datetime(2024, 6, 30, 17, 22, 19, tzinfo=utc), (1719705600, 1719792000)),
15+
(datetime(2024, 12, 3, 10, 59, 59, tzinfo=utc), (1733184000, 1733270400)),
16+
(datetime(2024, 12, 31, 16, 42, 28, tzinfo=utc), (1735603200, 1735689600)),
17+
])
18+
def test_get_day_boundaries(self, input_dt, expected_output):
19+
assert NotificationSummary.get_day_boundaries(input_dt) == expected_output

0 commit comments

Comments
 (0)