develop: 完善通知渠道透传与规则ID生成,清理pycache
This commit is contained in:
parent
a0236028cd
commit
1cd9df78e4
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.egg-info/
|
||||
.eggs/
|
||||
build/
|
||||
dist/
|
||||
.venv/
|
||||
venv/
|
||||
.coverage
|
||||
htmlcov/
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -17,6 +17,7 @@ import json
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||
from urllib.parse import urlparse
|
||||
@ -170,9 +171,10 @@ class APIServer:
|
||||
|
||||
def handle_create_rule(self, handler) -> None:
|
||||
data = handler._read_json()
|
||||
# api-design:POST /rules 允许不传 rule_id,服务端自动生成。
|
||||
if not data.get("rule_id"):
|
||||
data["rule_id"] = f"r-{uuid.uuid4().hex[:12]}"
|
||||
rule = MetricRule.from_dict(data)
|
||||
if not rule.rule_id:
|
||||
raise _APIError(400, 40001, "rule_id is required")
|
||||
created = self.app.rule_store.create_rule(rule)
|
||||
self.app.rule_loader.load()
|
||||
handler._write_json(200, {"code": 0, "message": "ok", "data": created.to_dict()})
|
||||
|
||||
@ -73,11 +73,14 @@ class AlertConverger:
|
||||
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, getattr(stored, "notify_channels", None) or self._channels(event))
|
||||
self.notifier.notify(stored, channels)
|
||||
return stored
|
||||
|
||||
def _handle_resolved(self, event: Event) -> Optional[Alert]:
|
||||
@ -98,4 +101,4 @@ class AlertConverger:
|
||||
|
||||
@staticmethod
|
||||
def _channels(event: Event) -> List[str]:
|
||||
return []
|
||||
return list(event.notify_channels)
|
||||
|
||||
@ -227,6 +227,7 @@ class DetectionEngine:
|
||||
severity=rule.severity,
|
||||
fired_at=now,
|
||||
labels=dict(sample.labels),
|
||||
notify_channels=list(rule.notify_channels),
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@ -153,7 +153,7 @@ class MetricRule:
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "MetricRule":
|
||||
return cls(
|
||||
rule_id=str(data["rule_id"]),
|
||||
rule_id=str(data.get("rule_id") or ""),
|
||||
name=str(data.get("name") or ""),
|
||||
metric=str(data.get("metric") or ""),
|
||||
aggregation=str(data.get("aggregation") or "avg"),
|
||||
@ -185,6 +185,7 @@ class Event:
|
||||
severity: str
|
||||
fired_at: float
|
||||
labels: Dict[str, str] = field(default_factory=dict)
|
||||
notify_channels: List[str] = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
@ -199,6 +200,7 @@ class Event:
|
||||
"severity": self.severity,
|
||||
"fired_at": self.fired_at,
|
||||
"labels": self.labels,
|
||||
"notify_channels": list(self.notify_channels),
|
||||
}
|
||||
|
||||
|
||||
@ -219,6 +221,7 @@ class Alert:
|
||||
ack_status: str = "open"
|
||||
ack_by: Optional[str] = None
|
||||
ack_at: Optional[float] = None
|
||||
notify_channels: List[str] = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
@ -235,4 +238,5 @@ class Alert:
|
||||
"ack_status": self.ack_status,
|
||||
"ack_by": self.ack_by,
|
||||
"ack_at": self.ack_at,
|
||||
"notify_channels": list(self.notify_channels),
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -73,6 +73,23 @@ class TestAPI(unittest.TestCase):
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(body["data"]["threshold"], 5.0)
|
||||
|
||||
def test_create_rule_autogenerates_id(self):
|
||||
payload = {
|
||||
"name": "load high",
|
||||
"metric": "load_1m",
|
||||
"aggregation": "avg",
|
||||
"operator": "gt",
|
||||
"threshold": 5.0,
|
||||
"for_duration": "60s",
|
||||
"severity": "warning",
|
||||
"scope": {"scope_type": "all"},
|
||||
"notify_channels": ["email"],
|
||||
"enabled": True,
|
||||
}
|
||||
status, body = self._request("POST", "/api/v1/rules", payload)
|
||||
self.assertEqual(status, 200)
|
||||
self.assertTrue(body["data"]["rule_id"].startswith("r-"))
|
||||
|
||||
def test_ingest_triggers_firing_event(self):
|
||||
samples = [
|
||||
{"name": "cpu_usage", "value": 95.0, "timestamp": float(i * 15), "labels": {"service": "web"}}
|
||||
|
||||
@ -2,6 +2,7 @@ import unittest
|
||||
|
||||
from detector.converger import AlertConverger
|
||||
from detector.models import Event
|
||||
from detector.notifier import Notifier
|
||||
from detector.storage import InMemoryAlertStore, InMemoryCache, InMemoryMessageBus
|
||||
|
||||
|
||||
@ -66,6 +67,17 @@ class TestAlertConverger(unittest.TestCase):
|
||||
self.assertEqual(topic, "alerts.converged")
|
||||
self.assertIn("alert_id", value)
|
||||
|
||||
def test_notify_channels_propagated(self):
|
||||
notifier = Notifier.memory(["email", "webhook"])
|
||||
conv = AlertConverger(
|
||||
self.store, self.cache, self.bus, dedup_window="5m", aggregate_window="1m",
|
||||
host_group_resolver=lambda h: "web", notifier=notifier,
|
||||
)
|
||||
event = make_event()
|
||||
event.notify_channels = ["email"]
|
||||
conv.handle_event(event)
|
||||
self.assertEqual(notifier.sent[0]["channel"], "email")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@ -17,6 +17,7 @@ def make_rule(rule_id="r-cpu", metric="cpu_usage", operator="gt", threshold=90.0
|
||||
for_duration=for_duration,
|
||||
severity="critical",
|
||||
scope=RuleScope(scope_type="all"),
|
||||
notify_channels=["email", "webhook"],
|
||||
)
|
||||
|
||||
|
||||
@ -102,6 +103,15 @@ class TestStateMachine(unittest.TestCase):
|
||||
self.assertEqual(events, [])
|
||||
self.assertEqual(engine.state_of("h-1", rule.rule_id)["status"], "firing")
|
||||
|
||||
def test_event_carries_notify_channels(self):
|
||||
rule = make_rule(for_duration="60s")
|
||||
engine = make_engine(rule)
|
||||
events = []
|
||||
for i in range(5):
|
||||
events.extend(engine.handle(MetricSample(name="cpu_usage", value=95.0, timestamp=float(i * 15)), "h-1"))
|
||||
self.assertEqual(len(events), 1)
|
||||
self.assertEqual(events[0].notify_channels, ["email", "webhook"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user