105 lines
4.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""告警收敛:去重 + 聚合。
- 去重:同一 (host_id, rule_id) 在 dedup_window 内不重复产生同类新告警
Redis SET key NX EX dedup_windowInMemory 语义等价)。
- 聚合:同一 aggregate_keyscope/rule/severity + 时间桶)在 aggregate_window
内多主机同类告警合并为一条aggregated_count 累加,明细 hosts 列表合并。
- 收敛后的 alert 写入 AlertStore并发布到 alerts.converged 总线,由通知分发器消费。
"""
from __future__ import annotations
import uuid
from typing import Callable, Dict, List, Optional
from .models import Alert, Event, parse_duration
from .storage import AlertStore, Cache, MessageBus
DEDUP_PREFIX = "hms:dedup:"
class AlertConverger:
def __init__(
self,
alert_store: AlertStore,
cache: Cache,
bus: MessageBus,
dedup_window: str = "5m",
aggregate_window: str = "1m",
alerts_topic: str = "alerts.converged",
host_group_resolver: Optional[Callable[[str], str]] = None,
notifier: Optional[object] = None,
) -> None:
self.alert_store = alert_store
self.cache = cache
self.bus = bus
self.dedup_window = parse_duration(dedup_window) or 300
self.aggregate_window = parse_duration(aggregate_window) or 60
self.alerts_topic = alerts_topic
self.host_group_resolver = host_group_resolver or (lambda host_id: "all")
self.notifier = notifier
def handle_event(self, event: Event) -> Optional[Alert]:
if event.status == "firing":
return self._handle_firing(event)
if event.status == "resolved":
return self._handle_resolved(event)
return None
def _handle_firing(self, event: Event) -> Optional[Alert]:
dedup_key = f"{event.host_id}:{event.rule_id}"
if not self.cache.set_nx(DEDUP_PREFIX + dedup_key, "1", ttl=self.dedup_window):
# 去重窗口内重复事件,抑制新告警
return None
bucket = int(event.fired_at // self.aggregate_window)
group = self.host_group_resolver(event.host_id)
aggregate_key = f"{group}:{event.rule_id}:{event.severity}:{bucket}"
alert = Alert(
alert_id=f"a-{uuid.uuid4().hex[:16]}",
dedup_key=dedup_key,
aggregate_key=aggregate_key,
severity=event.severity,
status="firing",
title=self._title(event),
detail={
"hosts": [event.host_id],
"current": event.agg_value,
"threshold": event.threshold,
"metric": event.metric,
"rule_id": event.rule_id,
},
count=1,
first_at=event.fired_at,
last_at=event.fired_at,
notify_channels=list(event.notify_channels),
)
stored = self.alert_store.upsert_firing(alert)
# 聚合更新时使用已存在告警的渠道列表,避免覆盖丢失
channels = stored.notify_channels or event.notify_channels
self._publish(stored)
if self.notifier is not None and hasattr(self.notifier, "notify"):
self.notifier.notify(stored, channels)
return stored
def _handle_resolved(self, event: Event) -> Optional[Alert]:
dedup_key = f"{event.host_id}:{event.rule_id}"
# 恢复事件允许后续再次触发去重键失效
self.cache.delete(DEDUP_PREFIX + dedup_key)
alert = self.alert_store.resolve_by_dedup_key(dedup_key, event.fired_at)
if alert is not None:
self._publish(alert)
return alert
def _publish(self, alert: Alert) -> None:
self.bus.publish(self.alerts_topic, alert.aggregate_key, alert.to_dict())
@staticmethod
def _title(event: Event) -> str:
return f"{event.metric} {event.operator} {event.threshold} ({event.status})"
@staticmethod
def _channels(event: Event) -> List[str]:
return list(event.notify_channels)