|
2 | 2 | import time |
3 | 3 | from abc import abstractmethod, ABC |
4 | 4 | from collections import defaultdict |
| 5 | +from datetime import datetime |
5 | 6 | from typing import Any, List, Dict, Tuple, DefaultDict, Optional |
6 | 7 |
|
7 | | -from pydantic import BaseModel, Field |
| 8 | +from pydantic import BaseModel |
8 | 9 |
|
9 | 10 | from robusta.core.model.k8s_operation_type import K8sOperationType |
10 | 11 | from robusta.core.reporting.base import Finding |
@@ -33,20 +34,63 @@ def register_notification(self, interval: int, threshold: int) -> bool: |
33 | 34 |
|
34 | 35 | class NotificationSummary(BaseModel): |
35 | 36 | 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 |
37 | 39 | # Keys for the table are determined by grouping.notification_mode.summary.by |
38 | 40 | summary_table: DefaultDict[KeyT, List[int]] = None |
39 | 41 |
|
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()) |
42 | 45 | 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 |
45 | 48 | 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) |
47 | 50 | self.message_id = None |
48 | 51 | self.summary_table[summary_key][idx] += 1 |
49 | 52 |
|
| 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 | + |
50 | 94 |
|
51 | 95 | class SinkBase(ABC): |
52 | 96 | grouping_enabled: bool |
|
0 commit comments