Coverage for agentos/monitoring/alerts.py: 58%
92 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 21:19 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 21:19 +0800
1"""AgentOS monitoring — alert rules and webhook notification dispatcher."""
3from __future__ import annotations
5import json
6import time
7import urllib.request
8from collections.abc import Callable
9from dataclasses import dataclass, field
10from enum import StrEnum
13class AlertSeverity(StrEnum):
14 """告警实例。"""
16 """告警严重级别。"""
18 CRITICAL = "critical"
19 WARNING = "warning"
20 INFO = "info"
23class AlertState(StrEnum):
24 """告警状态。"""
26 FIRING = "firing"
27 RESOLVED = "resolved"
30@dataclass
31class AlertRule:
32 """告警规则。"""
34 name: str
35 description: str
36 severity: AlertSeverity = AlertSeverity.WARNING
37 condition: Callable[[], bool] | None = None
38 cooldown_seconds: int = 300
39 _last_fired: float = field(default=0.0, repr=False)
41 def evaluate(self) -> bool:
42 if not self.condition:
43 return False
44 now = time.time()
45 if now - self._last_fired < self.cooldown_seconds:
46 return False
47 result = self.condition()
48 if result:
49 self._last_fired = now
50 return result
53@dataclass
54class Alert:
55 rule_name: str
56 severity: AlertSeverity
57 message: str
58 state: AlertState = AlertState.FIRING
59 timestamp: float = field(default_factory=time.time)
60 labels: dict = field(default_factory=dict)
62 def to_dict(self) -> dict:
63 return {
64 "rule_name": self.rule_name,
65 "severity": self.severity.value,
66 "message": self.message,
67 "state": self.state.value,
68 "timestamp": self.timestamp,
69 "labels": self.labels,
70 }
72 def to_json(self) -> str:
73 return json.dumps(self.to_dict())
76@dataclass
77class MonitoringConfig:
78 """监控配置。"""
80 enabled: bool = True
81 evaluation_interval: int = 60
82 max_alerts_per_interval: int = 10
85@dataclass
86class WebhookConfig:
87 """Webhook 配置。"""
89 url: str = ""
90 method: str = "POST"
91 headers: dict = field(default_factory=dict)
92 timeout: float = 5.0
93 retry_count: int = 3
96class WebhookDispatcher:
97 """Dispatches Alerts to configured webhook endpoints."""
99 def __init__(self, config: WebhookConfig | None = None):
100 self.config = config or WebhookConfig()
102 def send(self, alert: Alert) -> bool:
103 if not self.config.url:
104 return False
105 payload = json.dumps(alert.to_dict()).encode("utf-8")
106 for attempt in range(self.config.retry_count + 1):
107 try:
108 req = urllib.request.Request(
109 self.config.url,
110 data=payload,
111 headers=self.config.headers,
112 method=self.config.method,
113 )
114 with urllib.request.urlopen(req, timeout=self.config.timeout) as resp:
115 return resp.status < 400
116 except Exception:
117 if attempt == self.config.retry_count:
118 return False
119 time.sleep(1.0 * (attempt + 1))
120 return False
123class AlertEvaluator:
124 """Evaluates AlertRules and generates Alerts."""
126 def __init__(self, config: MonitoringConfig | None = None):
127 self.config = config or MonitoringConfig()
128 self.rules: list[AlertRule] = []
130 def add_rule(self, rule: AlertRule):
131 self.rules.append(rule)
133 def evaluate(self) -> list[Alert]:
134 if not self.config.enabled:
135 return []
136 alerts: list[Alert] = []
137 count = 0
138 for rule in self.rules:
139 if count >= self.config.max_alerts_per_interval:
140 break
141 if rule.evaluate():
142 alerts.append(
143 Alert(
144 rule_name=rule.name,
145 severity=rule.severity,
146 message=f"Alert: {rule.description}",
147 )
148 )
149 count += 1
150 return alerts