collector.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. # -*- coding: utf-8 -*-
  2. """
  3. Metriken-Collector für Service-Monitoring.
  4. Erfasst Metriken wie Startup-Zeit, Health-Check-Dauer,
  5. Fehler-Counts und mehr.
  6. """
  7. from __future__ import annotations
  8. import statistics
  9. import threading
  10. import time
  11. from abc import ABC, abstractmethod
  12. from collections import defaultdict
  13. from dataclasses import dataclass, field
  14. from datetime import datetime
  15. from enum import Enum
  16. from typing import Any, Callable, Dict, List
  17. class MetricType(Enum):
  18. """
  19. Typen von Metriken.
  20. """
  21. COUNTER = "counter"
  22. """Monoton steigender Zähler."""
  23. GAUGE = "gauge"
  24. """Wert der steigen und fallen kann."""
  25. HISTOGRAM = "histogram"
  26. """Verteilung von Werten."""
  27. SUMMARY = "summary"
  28. """Zusammenfassung mit Percentilen."""
  29. @dataclass
  30. class MetricValue:
  31. """
  32. Einzelner Metrik-Wert mit Metadaten.
  33. """
  34. name: str
  35. """Name der Metrik."""
  36. value: float
  37. """Aktueller Wert."""
  38. metric_type: MetricType
  39. """Typ der Metrik."""
  40. labels: dict[str, str] = field(default_factory=dict)
  41. """Labels/Tags für die Metrik."""
  42. timestamp: datetime = field(default_factory=datetime.now)
  43. """Zeitpunkt der Erfassung."""
  44. help_text: str = ""
  45. """Beschreibung der Metrik."""
  46. def to_dict(self) -> dict[str, Any]:
  47. """Konvertiert in ein Dictionary."""
  48. return {
  49. "name": self.name,
  50. "value": self.value,
  51. "type": self.metric_type.value,
  52. "labels": self.labels,
  53. "timestamp": self.timestamp.isoformat(),
  54. }
  55. class Metric(ABC):
  56. """
  57. Abstrakte Basisklasse für Metriken.
  58. """
  59. def __init__(
  60. self,
  61. name: str,
  62. help_text: str = "",
  63. labels: list[str] | None = None,
  64. ) -> None:
  65. """
  66. Initialisiert die Metrik.
  67. Args:
  68. name: Name der Metrik.
  69. help_text: Beschreibung.
  70. labels: Mögliche Label-Namen.
  71. """
  72. self.name = name
  73. self.help_text = help_text
  74. self.label_names = labels or []
  75. self._lock = threading.Lock()
  76. @abstractmethod
  77. def get_value(self, labels: dict[str, str] | None = None) -> float:
  78. """Gibt den aktuellen Wert zurück."""
  79. pass
  80. @abstractmethod
  81. def reset(self, labels: dict[str, str] | None = None) -> None:
  82. """Setzt die Metrik zurück."""
  83. pass
  84. class Counter(Metric):
  85. """
  86. Counter-Metrik (monoton steigend).
  87. Für Ereigniszähler, Request-Counts, etc.
  88. """
  89. metric_type = MetricType.COUNTER
  90. def __init__(
  91. self,
  92. name: str,
  93. help_text: str = "",
  94. labels: list[str] | None = None,
  95. ) -> None:
  96. """
  97. Initialisiert den Counter.
  98. Args:
  99. name: Name der Metrik.
  100. help_text: Beschreibung.
  101. labels: Mögliche Label-Namen.
  102. """
  103. super().__init__(name, help_text, labels)
  104. self._values: dict[tuple, float] = defaultdict(float)
  105. def _label_key(self, labels: dict[str, str] | None) -> tuple:
  106. """Erstellt einen Schlüssel aus Labels."""
  107. if not labels:
  108. return ()
  109. return tuple(sorted(labels.items()))
  110. def inc(self, value: float = 1.0, labels: dict[str, str] | None = None) -> None:
  111. """
  112. Erhöht den Counter.
  113. Args:
  114. value: Wert um den erhöht wird (muss positiv sein).
  115. labels: Label-Werte.
  116. """
  117. if value < 0:
  118. raise ValueError("Counter kann nur erhöht werden")
  119. key = self._label_key(labels)
  120. with self._lock:
  121. self._values[key] += value
  122. def get_value(self, labels: dict[str, str] | None = None) -> float:
  123. """Gibt den aktuellen Wert zurück."""
  124. key = self._label_key(labels)
  125. with self._lock:
  126. return self._values.get(key, 0.0)
  127. def reset(self, labels: dict[str, str] | None = None) -> None:
  128. """Setzt den Counter zurück."""
  129. key = self._label_key(labels)
  130. with self._lock:
  131. if labels:
  132. self._values[key] = 0.0
  133. else:
  134. self._values.clear()
  135. def get_all_values(self) -> list[MetricValue]:
  136. """Gibt alle Label-Kombinationen zurück."""
  137. with self._lock:
  138. return [
  139. MetricValue(
  140. name=self.name,
  141. value=value,
  142. metric_type=self.metric_type,
  143. labels=dict(key),
  144. help_text=self.help_text,
  145. )
  146. for key, value in self._values.items()
  147. ]
  148. class Gauge(Metric):
  149. """
  150. Gauge-Metrik (kann steigen und fallen).
  151. Für aktuelle Werte wie Speichernutzung, aktive Verbindungen, etc.
  152. """
  153. metric_type = MetricType.GAUGE
  154. def __init__(
  155. self,
  156. name: str,
  157. help_text: str = "",
  158. labels: list[str] | None = None,
  159. ) -> None:
  160. """
  161. Initialisiert die Gauge.
  162. Args:
  163. name: Name der Metrik.
  164. help_text: Beschreibung.
  165. labels: Mögliche Label-Namen.
  166. """
  167. super().__init__(name, help_text, labels)
  168. self._values: dict[tuple, float] = defaultdict(float)
  169. def _label_key(self, labels: dict[str, str] | None) -> tuple:
  170. """Erstellt einen Schlüssel aus Labels."""
  171. if not labels:
  172. return ()
  173. return tuple(sorted(labels.items()))
  174. def set(self, value: float, labels: dict[str, str] | None = None) -> None:
  175. """
  176. Setzt den Gauge-Wert.
  177. Args:
  178. value: Neuer Wert.
  179. labels: Label-Werte.
  180. """
  181. key = self._label_key(labels)
  182. with self._lock:
  183. self._values[key] = value
  184. def inc(self, value: float = 1.0, labels: dict[str, str] | None = None) -> None:
  185. """Erhöht den Gauge."""
  186. key = self._label_key(labels)
  187. with self._lock:
  188. self._values[key] += value
  189. def dec(self, value: float = 1.0, labels: dict[str, str] | None = None) -> None:
  190. """Verringert den Gauge."""
  191. key = self._label_key(labels)
  192. with self._lock:
  193. self._values[key] -= value
  194. def get_value(self, labels: dict[str, str] | None = None) -> float:
  195. """Gibt den aktuellen Wert zurück."""
  196. key = self._label_key(labels)
  197. with self._lock:
  198. return self._values.get(key, 0.0)
  199. def reset(self, labels: dict[str, str] | None = None) -> None:
  200. """Setzt den Gauge zurück."""
  201. key = self._label_key(labels)
  202. with self._lock:
  203. if labels:
  204. self._values[key] = 0.0
  205. else:
  206. self._values.clear()
  207. def get_all_values(self) -> list[MetricValue]:
  208. """Gibt alle Label-Kombinationen zurück."""
  209. with self._lock:
  210. return [
  211. MetricValue(
  212. name=self.name,
  213. value=value,
  214. metric_type=self.metric_type,
  215. labels=dict(key),
  216. help_text=self.help_text,
  217. )
  218. for key, value in self._values.items()
  219. ]
  220. class Histogram(Metric):
  221. """
  222. Histogram-Metrik für Verteilungen.
  223. Für Latenzen, Request-Größen, etc.
  224. """
  225. metric_type = MetricType.HISTOGRAM
  226. DEFAULT_BUCKETS = (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0)
  227. def __init__(
  228. self,
  229. name: str,
  230. help_text: str = "",
  231. labels: list[str] | None = None,
  232. buckets: tuple[float, ...] | None = None,
  233. ) -> None:
  234. """
  235. Initialisiert das Histogram.
  236. Args:
  237. name: Name der Metrik.
  238. help_text: Beschreibung.
  239. labels: Mögliche Label-Namen.
  240. buckets: Bucket-Grenzen für das Histogram.
  241. """
  242. super().__init__(name, help_text, labels)
  243. self.buckets = buckets or self.DEFAULT_BUCKETS
  244. self._observations: dict[tuple, list[float]] = defaultdict(list)
  245. def _label_key(self, labels: dict[str, str] | None) -> tuple:
  246. """Erstellt einen Schlüssel aus Labels."""
  247. if not labels:
  248. return ()
  249. return tuple(sorted(labels.items()))
  250. def observe(self, value: float, labels: dict[str, str] | None = None) -> None:
  251. """
  252. Zeichnet einen Wert auf.
  253. Args:
  254. value: Beobachteter Wert.
  255. labels: Label-Werte.
  256. """
  257. key = self._label_key(labels)
  258. with self._lock:
  259. self._observations[key].append(value)
  260. def get_value(self, labels: dict[str, str] | None = None) -> float:
  261. """Gibt die Summe aller Beobachtungen zurück."""
  262. key = self._label_key(labels)
  263. with self._lock:
  264. observations = self._observations.get(key, [])
  265. return sum(observations)
  266. def get_count(self, labels: dict[str, str] | None = None) -> int:
  267. """Gibt die Anzahl der Beobachtungen zurück."""
  268. key = self._label_key(labels)
  269. with self._lock:
  270. return len(self._observations.get(key, []))
  271. def get_buckets(self, labels: dict[str, str] | None = None) -> dict[float, int]:
  272. """Gibt die Bucket-Verteilung zurück."""
  273. key = self._label_key(labels)
  274. with self._lock:
  275. observations = self._observations.get(key, [])
  276. result = {}
  277. for bucket in self.buckets:
  278. count = sum(1 for v in observations if v <= bucket)
  279. result[bucket] = count
  280. result[float("inf")] = len(observations)
  281. return result
  282. def get_percentile(
  283. self,
  284. percentile: float,
  285. labels: dict[str, str] | None = None,
  286. ) -> float:
  287. """
  288. Berechnet ein Percentil.
  289. Args:
  290. percentile: Percentil (0-100).
  291. labels: Label-Werte.
  292. Returns:
  293. Percentil-Wert.
  294. """
  295. key = self._label_key(labels)
  296. with self._lock:
  297. observations = self._observations.get(key, [])
  298. if not observations:
  299. return 0.0
  300. sorted_obs = sorted(observations)
  301. index = int((percentile / 100) * len(sorted_obs))
  302. index = min(index, len(sorted_obs) - 1)
  303. return sorted_obs[index]
  304. def reset(self, labels: dict[str, str] | None = None) -> None:
  305. """Setzt das Histogram zurück."""
  306. key = self._label_key(labels)
  307. with self._lock:
  308. if labels:
  309. self._observations[key] = []
  310. else:
  311. self._observations.clear()
  312. @dataclass
  313. class ServiceMetrics:
  314. """
  315. Standard-Metriken für einen Service.
  316. """
  317. service_name: str
  318. """Name des Services."""
  319. startup_time: float = 0.0
  320. """Startup-Zeit in Sekunden."""
  321. startup_timestamp: datetime | None = None
  322. """Zeitpunkt des Starts."""
  323. health_check_duration: float = 0.0
  324. """Letzte Health-Check-Dauer."""
  325. health_check_count: int = 0
  326. """Anzahl der Health-Checks."""
  327. error_count: int = 0
  328. """Anzahl der Fehler."""
  329. last_error: str | None = None
  330. """Letzter Fehler."""
  331. last_error_timestamp: datetime | None = None
  332. """Zeitpunkt des letzten Fehlers."""
  333. custom_metrics: dict[str, float] = field(default_factory=dict)
  334. """Benutzerdefinierte Metriken."""
  335. def record_error(self, error: str) -> None:
  336. """Zeichnet einen Fehler auf."""
  337. self.error_count += 1
  338. self.last_error = error
  339. self.last_error_timestamp = datetime.now()
  340. def to_dict(self) -> dict[str, Any]:
  341. """Konvertiert in ein Dictionary."""
  342. return {
  343. "service_name": self.service_name,
  344. "startup_time_seconds": self.startup_time,
  345. "startup_timestamp": (
  346. self.startup_timestamp.isoformat()
  347. if self.startup_timestamp else None
  348. ),
  349. "health_check_duration_seconds": self.health_check_duration,
  350. "health_check_count": self.health_check_count,
  351. "error_count": self.error_count,
  352. "last_error": self.last_error,
  353. "custom_metrics": self.custom_metrics,
  354. }
  355. class MetricsCollector:
  356. """
  357. Zentraler Collector für Service-Metriken.
  358. Sammelt und aggregiert Metriken aus verschiedenen Services.
  359. Example:
  360. collector = MetricsCollector()
  361. # Metrik registrieren
  362. requests = collector.counter("http_requests_total")
  363. latency = collector.histogram("request_latency_seconds")
  364. # Metriken erfassen
  365. requests.inc(labels={"method": "GET"})
  366. latency.observe(0.5, labels={"endpoint": "/api"})
  367. # Alle Metriken abrufen
  368. metrics = collector.get_all_metrics()
  369. """
  370. def __init__(self, prefix: str = "") -> None:
  371. """
  372. Initialisiert den Collector.
  373. Args:
  374. prefix: Präfix für alle Metrik-Namen.
  375. """
  376. self._prefix = prefix
  377. self._metrics: dict[str, Metric] = {}
  378. self._service_metrics: dict[str, ServiceMetrics] = {}
  379. self._lock = threading.Lock()
  380. def _make_name(self, name: str) -> str:
  381. """Erstellt den vollständigen Metrik-Namen."""
  382. if self._prefix:
  383. return f"{self._prefix}_{name}"
  384. return name
  385. def counter(
  386. self,
  387. name: str,
  388. help_text: str = "",
  389. labels: list[str] | None = None,
  390. ) -> Counter:
  391. """
  392. Erstellt oder holt einen Counter.
  393. Args:
  394. name: Name der Metrik.
  395. help_text: Beschreibung.
  396. labels: Mögliche Label-Namen.
  397. Returns:
  398. Counter-Instanz.
  399. """
  400. full_name = self._make_name(name)
  401. with self._lock:
  402. if full_name not in self._metrics:
  403. self._metrics[full_name] = Counter(full_name, help_text, labels)
  404. return self._metrics[full_name] # type: ignore
  405. def gauge(
  406. self,
  407. name: str,
  408. help_text: str = "",
  409. labels: list[str] | None = None,
  410. ) -> Gauge:
  411. """
  412. Erstellt oder holt eine Gauge.
  413. Args:
  414. name: Name der Metrik.
  415. help_text: Beschreibung.
  416. labels: Mögliche Label-Namen.
  417. Returns:
  418. Gauge-Instanz.
  419. """
  420. full_name = self._make_name(name)
  421. with self._lock:
  422. if full_name not in self._metrics:
  423. self._metrics[full_name] = Gauge(full_name, help_text, labels)
  424. return self._metrics[full_name] # type: ignore
  425. def histogram(
  426. self,
  427. name: str,
  428. help_text: str = "",
  429. labels: list[str] | None = None,
  430. buckets: tuple[float, ...] | None = None,
  431. ) -> Histogram:
  432. """
  433. Erstellt oder holt ein Histogram.
  434. Args:
  435. name: Name der Metrik.
  436. help_text: Beschreibung.
  437. labels: Mögliche Label-Namen.
  438. buckets: Bucket-Grenzen.
  439. Returns:
  440. Histogram-Instanz.
  441. """
  442. full_name = self._make_name(name)
  443. with self._lock:
  444. if full_name not in self._metrics:
  445. self._metrics[full_name] = Histogram(
  446. full_name, help_text, labels, buckets
  447. )
  448. return self._metrics[full_name] # type: ignore
  449. def register_service(self, service_name: str) -> ServiceMetrics:
  450. """
  451. Registriert einen Service für Metriken-Erfassung.
  452. Args:
  453. service_name: Name des Services.
  454. Returns:
  455. ServiceMetrics für den Service.
  456. """
  457. with self._lock:
  458. if service_name not in self._service_metrics:
  459. self._service_metrics[service_name] = ServiceMetrics(
  460. service_name=service_name
  461. )
  462. return self._service_metrics[service_name]
  463. def get_service_metrics(self, service_name: str) -> ServiceMetrics | None:
  464. """Gibt die Metriken eines Services zurück."""
  465. return self._service_metrics.get(service_name)
  466. def get_all_metrics(self) -> dict[str, Metric]:
  467. """Gibt alle Metriken zurück."""
  468. with self._lock:
  469. return dict(self._metrics)
  470. def get_all_service_metrics(self) -> dict[str, ServiceMetrics]:
  471. """Gibt alle Service-Metriken zurück."""
  472. with self._lock:
  473. return dict(self._service_metrics)
  474. def reset_all(self) -> None:
  475. """Setzt alle Metriken zurück."""
  476. with self._lock:
  477. for metric in self._metrics.values():
  478. metric.reset()
  479. # Globaler Default-Collector
  480. _default_collector: MetricsCollector | None = None
  481. def get_default_collector() -> MetricsCollector:
  482. """Gibt den Default-Collector zurück."""
  483. global _default_collector
  484. if _default_collector is None:
  485. _default_collector = MetricsCollector()
  486. return _default_collector
  487. def set_default_collector(collector: MetricsCollector) -> None:
  488. """Setzt den Default-Collector."""
  489. global _default_collector
  490. _default_collector = collector