thresholds.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. # -*- coding: utf-8 -*-
  2. """
  3. Health-Schwellenwerte und Alarme.
  4. Definiert Schwellenwerte für Metriken und löst
  5. Alarme bei Überschreitung aus.
  6. """
  7. from dataclasses import dataclass, field
  8. from datetime import datetime
  9. from enum import IntEnum, auto
  10. from typing import Any, Callable
  11. from trixy_core.satellite.health.scorer import HealthMetrics
  12. class ViolationSeverity(IntEnum):
  13. """Schweregrad einer Schwellenwert-Verletzung."""
  14. INFO = auto() # Informativ
  15. WARNING = auto() # Warnung
  16. ERROR = auto() # Fehler
  17. CRITICAL = auto() # Kritisch
  18. @dataclass
  19. class ThresholdConfig:
  20. """
  21. Konfiguration eines Schwellenwerts.
  22. Attributes:
  23. metric_name: Name der Metrik
  24. warning_threshold: Schwelle für Warnung
  25. error_threshold: Schwelle für Fehler
  26. critical_threshold: Schwelle für kritisch
  27. comparison: "gt" (größer), "lt" (kleiner), "eq" (gleich)
  28. cooldown: Mindestzeit zwischen Alarmen in Sekunden
  29. description: Beschreibung des Schwellenwerts
  30. """
  31. metric_name: str
  32. warning_threshold: float | None = None
  33. error_threshold: float | None = None
  34. critical_threshold: float | None = None
  35. comparison: str = "gt" # gt, lt, eq, ge, le
  36. cooldown: float = 60.0
  37. description: str = ""
  38. @dataclass
  39. class ThresholdViolation:
  40. """
  41. Eine erkannte Schwellenwert-Verletzung.
  42. Attributes:
  43. satellite_id: Betroffener Satellite
  44. config: Der verletzte Schwellenwert
  45. severity: Schweregrad
  46. current_value: Aktueller Wert
  47. threshold_value: Überschrittener Schwellenwert
  48. message: Beschreibende Nachricht
  49. timestamp: Zeitpunkt der Verletzung
  50. """
  51. satellite_id: str
  52. config: ThresholdConfig
  53. severity: ViolationSeverity
  54. current_value: float
  55. threshold_value: float
  56. message: str = ""
  57. timestamp: datetime = field(default_factory=datetime.now)
  58. def to_dict(self) -> dict[str, Any]:
  59. """Konvertiert zu Dictionary."""
  60. return {
  61. "satellite_id": self.satellite_id,
  62. "metric": self.config.metric_name,
  63. "severity": self.severity.name,
  64. "current_value": self.current_value,
  65. "threshold_value": self.threshold_value,
  66. "message": self.message,
  67. "timestamp": self.timestamp.isoformat(),
  68. }
  69. class HealthThresholds:
  70. """
  71. Verwaltet Schwellenwerte und prüft auf Verletzungen.
  72. Ermöglicht die Konfiguration von Alarmschwellen für
  73. verschiedene Metriken und benachrichtigt bei Überschreitung.
  74. Example:
  75. thresholds = HealthThresholds()
  76. # Schwellenwerte konfigurieren
  77. thresholds.add(ThresholdConfig(
  78. metric_name="latency_ms",
  79. warning_threshold=100,
  80. error_threshold=200,
  81. critical_threshold=500,
  82. comparison="gt"
  83. ))
  84. thresholds.add(ThresholdConfig(
  85. metric_name="packet_loss",
  86. warning_threshold=0.01,
  87. error_threshold=0.05,
  88. critical_threshold=0.10,
  89. ))
  90. # Callback für Verletzungen
  91. @thresholds.on_violation
  92. def handle_violation(violation: ThresholdViolation):
  93. print(f"ALARM: {violation.message}")
  94. # Metriken prüfen
  95. violations = thresholds.check("satellite_1", metrics)
  96. """
  97. # Vordefinierte Schwellenwerte
  98. DEFAULT_THRESHOLDS = [
  99. ThresholdConfig(
  100. metric_name="latency_ms",
  101. warning_threshold=100.0,
  102. error_threshold=200.0,
  103. critical_threshold=500.0,
  104. comparison="gt",
  105. description="Netzwerklatenz"
  106. ),
  107. ThresholdConfig(
  108. metric_name="packet_loss",
  109. warning_threshold=0.01,
  110. error_threshold=0.05,
  111. critical_threshold=0.10,
  112. comparison="gt",
  113. description="Paketverlustrate"
  114. ),
  115. ThresholdConfig(
  116. metric_name="error_rate",
  117. warning_threshold=0.01,
  118. error_threshold=0.05,
  119. critical_threshold=0.10,
  120. comparison="gt",
  121. description="Fehlerrate"
  122. ),
  123. ThresholdConfig(
  124. metric_name="last_heartbeat_age",
  125. warning_threshold=10.0,
  126. error_threshold=30.0,
  127. critical_threshold=60.0,
  128. comparison="gt",
  129. description="Heartbeat-Alter"
  130. ),
  131. ThresholdConfig(
  132. metric_name="cpu_usage",
  133. warning_threshold=0.70,
  134. error_threshold=0.85,
  135. critical_threshold=0.95,
  136. comparison="gt",
  137. description="CPU-Auslastung"
  138. ),
  139. ThresholdConfig(
  140. metric_name="memory_usage",
  141. warning_threshold=0.75,
  142. error_threshold=0.90,
  143. critical_threshold=0.95,
  144. comparison="gt",
  145. description="Speicherauslastung"
  146. ),
  147. ]
  148. def __init__(self, use_defaults: bool = True) -> None:
  149. """
  150. Initialisiert die Schwellenwert-Verwaltung.
  151. Args:
  152. use_defaults: Standard-Schwellenwerte laden
  153. """
  154. self._configs: dict[str, ThresholdConfig] = {}
  155. self._last_violations: dict[str, dict[str, datetime]] = {}
  156. self._callbacks: list[Callable[[ThresholdViolation], None]] = []
  157. if use_defaults:
  158. for config in self.DEFAULT_THRESHOLDS:
  159. self.add(config)
  160. def add(self, config: ThresholdConfig) -> None:
  161. """
  162. Fügt einen Schwellenwert hinzu.
  163. Args:
  164. config: Schwellenwert-Konfiguration
  165. """
  166. self._configs[config.metric_name] = config
  167. def remove(self, metric_name: str) -> ThresholdConfig | None:
  168. """
  169. Entfernt einen Schwellenwert.
  170. Args:
  171. metric_name: Name der Metrik
  172. Returns:
  173. Die entfernte Konfiguration oder None
  174. """
  175. return self._configs.pop(metric_name, None)
  176. def get(self, metric_name: str) -> ThresholdConfig | None:
  177. """Gibt eine Konfiguration zurück."""
  178. return self._configs.get(metric_name)
  179. def on_violation(
  180. self,
  181. callback: Callable[[ThresholdViolation], None]
  182. ) -> Callable[[ThresholdViolation], None]:
  183. """
  184. Registriert einen Callback für Verletzungen.
  185. Kann als Decorator verwendet werden.
  186. Args:
  187. callback: Die aufzurufende Funktion
  188. Returns:
  189. Der registrierte Callback
  190. """
  191. self._callbacks.append(callback)
  192. return callback
  193. def check(
  194. self,
  195. satellite_id: str,
  196. metrics: HealthMetrics
  197. ) -> list[ThresholdViolation]:
  198. """
  199. Prüft Metriken gegen alle Schwellenwerte.
  200. Args:
  201. satellite_id: Satellite-ID
  202. metrics: Zu prüfende Metriken
  203. Returns:
  204. Liste der Verletzungen
  205. """
  206. violations = []
  207. for config in self._configs.values():
  208. violation = self._check_threshold(satellite_id, metrics, config)
  209. if violation:
  210. # Cooldown prüfen
  211. if self._is_in_cooldown(satellite_id, config.metric_name, config.cooldown):
  212. continue
  213. violations.append(violation)
  214. self._record_violation(satellite_id, config.metric_name)
  215. self._notify_violation(violation)
  216. return violations
  217. def _check_threshold(
  218. self,
  219. satellite_id: str,
  220. metrics: HealthMetrics,
  221. config: ThresholdConfig
  222. ) -> ThresholdViolation | None:
  223. """Prüft einen einzelnen Schwellenwert."""
  224. # Metrik-Wert holen
  225. if hasattr(metrics, config.metric_name):
  226. value = getattr(metrics, config.metric_name)
  227. elif config.metric_name in metrics.custom_metrics:
  228. value = metrics.custom_metrics[config.metric_name]
  229. else:
  230. return None
  231. # Schweregrad bestimmen
  232. severity = None
  233. threshold_value = None
  234. # Vom kritischsten zum harmlosesten prüfen
  235. if config.critical_threshold is not None:
  236. if self._compare(value, config.critical_threshold, config.comparison):
  237. severity = ViolationSeverity.CRITICAL
  238. threshold_value = config.critical_threshold
  239. if severity is None and config.error_threshold is not None:
  240. if self._compare(value, config.error_threshold, config.comparison):
  241. severity = ViolationSeverity.ERROR
  242. threshold_value = config.error_threshold
  243. if severity is None and config.warning_threshold is not None:
  244. if self._compare(value, config.warning_threshold, config.comparison):
  245. severity = ViolationSeverity.WARNING
  246. threshold_value = config.warning_threshold
  247. if severity is None:
  248. return None
  249. # Verletzung erstellen
  250. message = self._format_message(config, value, threshold_value, severity)
  251. return ThresholdViolation(
  252. satellite_id=satellite_id,
  253. config=config,
  254. severity=severity,
  255. current_value=value,
  256. threshold_value=threshold_value,
  257. message=message,
  258. )
  259. def _compare(self, value: float, threshold: float, comparison: str) -> bool:
  260. """Vergleicht einen Wert mit einem Schwellenwert."""
  261. if comparison == "gt":
  262. return value > threshold
  263. elif comparison == "lt":
  264. return value < threshold
  265. elif comparison == "ge":
  266. return value >= threshold
  267. elif comparison == "le":
  268. return value <= threshold
  269. elif comparison == "eq":
  270. return value == threshold
  271. return False
  272. def _format_message(
  273. self,
  274. config: ThresholdConfig,
  275. value: float,
  276. threshold: float,
  277. severity: ViolationSeverity
  278. ) -> str:
  279. """Formatiert die Verletzungs-Nachricht."""
  280. comparison_text = {
  281. "gt": "überschreitet",
  282. "lt": "unterschreitet",
  283. "ge": "erreicht oder überschreitet",
  284. "le": "erreicht oder unterschreitet",
  285. "eq": "entspricht",
  286. }.get(config.comparison, "verletzt")
  287. name = config.description or config.metric_name
  288. return (
  289. f"{severity.name}: {name} {comparison_text} Schwellenwert "
  290. f"(Wert: {value:.3f}, Schwelle: {threshold:.3f})"
  291. )
  292. def _is_in_cooldown(
  293. self,
  294. satellite_id: str,
  295. metric_name: str,
  296. cooldown: float
  297. ) -> bool:
  298. """Prüft, ob noch im Cooldown."""
  299. if satellite_id not in self._last_violations:
  300. return False
  301. last_time = self._last_violations[satellite_id].get(metric_name)
  302. if last_time is None:
  303. return False
  304. elapsed = (datetime.now() - last_time).total_seconds()
  305. return elapsed < cooldown
  306. def _record_violation(self, satellite_id: str, metric_name: str) -> None:
  307. """Zeichnet eine Verletzung auf (für Cooldown)."""
  308. if satellite_id not in self._last_violations:
  309. self._last_violations[satellite_id] = {}
  310. self._last_violations[satellite_id][metric_name] = datetime.now()
  311. def _notify_violation(self, violation: ThresholdViolation) -> None:
  312. """Benachrichtigt alle Callbacks."""
  313. for callback in self._callbacks:
  314. try:
  315. callback(violation)
  316. except Exception:
  317. pass
  318. def get_all_configs(self) -> list[ThresholdConfig]:
  319. """Gibt alle Konfigurationen zurück."""
  320. return list(self._configs.values())
  321. def clear_cooldowns(self, satellite_id: str | None = None) -> None:
  322. """
  323. Löscht Cooldowns.
  324. Args:
  325. satellite_id: Optionaler spezifischer Satellite
  326. """
  327. if satellite_id:
  328. self._last_violations.pop(satellite_id, None)
  329. else:
  330. self._last_violations.clear()