reporter.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. # -*- coding: utf-8 -*-
  2. """
  3. Metriken-Reporter für verschiedene Ausgabeformate.
  4. Bietet Export in Prometheus-Format, JSON und mehr.
  5. """
  6. from __future__ import annotations
  7. import json
  8. from abc import ABC, abstractmethod
  9. from datetime import datetime
  10. from typing import Any
  11. from trixy_core.service.metrics.collector import (
  12. Counter,
  13. Gauge,
  14. Histogram,
  15. Metric,
  16. MetricType,
  17. MetricsCollector,
  18. ServiceMetrics,
  19. )
  20. class MetricsReporter(ABC):
  21. """
  22. Abstrakte Basisklasse für Metriken-Reporter.
  23. """
  24. def __init__(self, collector: MetricsCollector) -> None:
  25. """
  26. Initialisiert den Reporter.
  27. Args:
  28. collector: Der MetricsCollector.
  29. """
  30. self._collector = collector
  31. @property
  32. def collector(self) -> MetricsCollector:
  33. """Gibt den Collector zurück."""
  34. return self._collector
  35. @abstractmethod
  36. def report(self) -> str:
  37. """
  38. Generiert den Metriken-Report.
  39. Returns:
  40. Report als String.
  41. """
  42. pass
  43. class PrometheusReporter(MetricsReporter):
  44. """
  45. Reporter für Prometheus-Format.
  46. Generiert Metriken im Prometheus Text Exposition Format.
  47. Example:
  48. collector = MetricsCollector()
  49. reporter = PrometheusReporter(collector)
  50. print(reporter.report())
  51. """
  52. def _format_labels(self, labels: dict[str, str]) -> str:
  53. """Formatiert Labels für Prometheus."""
  54. if not labels:
  55. return ""
  56. formatted = ", ".join(
  57. f'{k}="{v}"' for k, v in sorted(labels.items())
  58. )
  59. return f"{{{formatted}}}"
  60. def _format_counter(self, metric: Counter) -> list[str]:
  61. """Formatiert einen Counter."""
  62. lines = [
  63. f"# HELP {metric.name} {metric.help_text}",
  64. f"# TYPE {metric.name} counter",
  65. ]
  66. for mv in metric.get_all_values():
  67. labels = self._format_labels(mv.labels)
  68. lines.append(f"{metric.name}{labels} {mv.value}")
  69. return lines
  70. def _format_gauge(self, metric: Gauge) -> list[str]:
  71. """Formatiert eine Gauge."""
  72. lines = [
  73. f"# HELP {metric.name} {metric.help_text}",
  74. f"# TYPE {metric.name} gauge",
  75. ]
  76. for mv in metric.get_all_values():
  77. labels = self._format_labels(mv.labels)
  78. lines.append(f"{metric.name}{labels} {mv.value}")
  79. return lines
  80. def _format_histogram(self, metric: Histogram) -> list[str]:
  81. """Formatiert ein Histogram."""
  82. lines = [
  83. f"# HELP {metric.name} {metric.help_text}",
  84. f"# TYPE {metric.name} histogram",
  85. ]
  86. # Für jede Label-Kombination
  87. for key, observations in metric._observations.items():
  88. labels = dict(key) if key else {}
  89. base_labels = self._format_labels(labels)
  90. # Buckets
  91. buckets = metric.get_buckets(labels)
  92. for bucket, count in buckets.items():
  93. if bucket == float("inf"):
  94. bucket_str = "+Inf"
  95. else:
  96. bucket_str = str(bucket)
  97. if labels:
  98. all_labels = {**labels, "le": bucket_str}
  99. else:
  100. all_labels = {"le": bucket_str}
  101. formatted_labels = self._format_labels(all_labels)
  102. lines.append(f"{metric.name}_bucket{formatted_labels} {count}")
  103. # Sum und Count
  104. total = sum(observations)
  105. count = len(observations)
  106. lines.append(f"{metric.name}_sum{base_labels} {total}")
  107. lines.append(f"{metric.name}_count{base_labels} {count}")
  108. return lines
  109. def _format_service_metrics(self, sm: ServiceMetrics) -> list[str]:
  110. """Formatiert Service-Metriken."""
  111. lines = []
  112. prefix = f"service_{sm.service_name.replace('-', '_').replace('.', '_')}"
  113. # Startup-Zeit
  114. if sm.startup_time > 0:
  115. lines.append(f"# HELP {prefix}_startup_seconds Startup-Zeit")
  116. lines.append(f"# TYPE {prefix}_startup_seconds gauge")
  117. lines.append(f"{prefix}_startup_seconds {sm.startup_time}")
  118. # Health-Check
  119. if sm.health_check_count > 0:
  120. lines.append(f"# HELP {prefix}_health_check_duration_seconds Health-Check-Dauer")
  121. lines.append(f"# TYPE {prefix}_health_check_duration_seconds gauge")
  122. lines.append(f"{prefix}_health_check_duration_seconds {sm.health_check_duration}")
  123. lines.append(f"# HELP {prefix}_health_check_total Health-Check-Anzahl")
  124. lines.append(f"# TYPE {prefix}_health_check_total counter")
  125. lines.append(f"{prefix}_health_check_total {sm.health_check_count}")
  126. # Fehler
  127. lines.append(f"# HELP {prefix}_errors_total Fehler-Anzahl")
  128. lines.append(f"# TYPE {prefix}_errors_total counter")
  129. lines.append(f"{prefix}_errors_total {sm.error_count}")
  130. # Custom-Metriken
  131. for name, value in sm.custom_metrics.items():
  132. metric_name = f"{prefix}_{name.replace('-', '_').replace('.', '_')}"
  133. lines.append(f"# TYPE {metric_name} gauge")
  134. lines.append(f"{metric_name} {value}")
  135. return lines
  136. def report(self) -> str:
  137. """Generiert den Prometheus-Report."""
  138. lines: list[str] = []
  139. # Standard-Metriken
  140. for metric in self._collector.get_all_metrics().values():
  141. if isinstance(metric, Counter):
  142. lines.extend(self._format_counter(metric))
  143. elif isinstance(metric, Gauge):
  144. lines.extend(self._format_gauge(metric))
  145. elif isinstance(metric, Histogram):
  146. lines.extend(self._format_histogram(metric))
  147. lines.append("")
  148. # Service-Metriken
  149. for sm in self._collector.get_all_service_metrics().values():
  150. lines.extend(self._format_service_metrics(sm))
  151. lines.append("")
  152. return "\n".join(lines)
  153. class JSONReporter(MetricsReporter):
  154. """
  155. Reporter für JSON-Format.
  156. Generiert Metriken als JSON für APIs und Dashboards.
  157. """
  158. def __init__(
  159. self,
  160. collector: MetricsCollector,
  161. indent: int | None = 2,
  162. include_timestamp: bool = True,
  163. ) -> None:
  164. """
  165. Initialisiert den JSON-Reporter.
  166. Args:
  167. collector: Der MetricsCollector.
  168. indent: JSON-Einrückung.
  169. include_timestamp: Ob Timestamps inkludiert werden.
  170. """
  171. super().__init__(collector)
  172. self._indent = indent
  173. self._include_timestamp = include_timestamp
  174. def _format_metric(self, metric: Metric) -> dict[str, Any]:
  175. """Formatiert eine Metrik als Dictionary."""
  176. if isinstance(metric, Counter):
  177. values = []
  178. for mv in metric.get_all_values():
  179. entry: dict[str, Any] = {"value": mv.value}
  180. if mv.labels:
  181. entry["labels"] = mv.labels
  182. values.append(entry)
  183. return {
  184. "name": metric.name,
  185. "type": "counter",
  186. "help": metric.help_text,
  187. "values": values,
  188. }
  189. elif isinstance(metric, Gauge):
  190. values = []
  191. for mv in metric.get_all_values():
  192. entry = {"value": mv.value}
  193. if mv.labels:
  194. entry["labels"] = mv.labels
  195. values.append(entry)
  196. return {
  197. "name": metric.name,
  198. "type": "gauge",
  199. "help": metric.help_text,
  200. "values": values,
  201. }
  202. elif isinstance(metric, Histogram):
  203. data = []
  204. for key, observations in metric._observations.items():
  205. labels = dict(key) if key else {}
  206. entry: dict[str, Any] = {
  207. "count": len(observations),
  208. "sum": sum(observations),
  209. "buckets": metric.get_buckets(labels),
  210. }
  211. if labels:
  212. entry["labels"] = labels
  213. if observations:
  214. entry["percentiles"] = {
  215. "p50": metric.get_percentile(50, labels),
  216. "p90": metric.get_percentile(90, labels),
  217. "p99": metric.get_percentile(99, labels),
  218. }
  219. data.append(entry)
  220. return {
  221. "name": metric.name,
  222. "type": "histogram",
  223. "help": metric.help_text,
  224. "data": data,
  225. }
  226. return {"name": metric.name, "type": "unknown"}
  227. def report(self) -> str:
  228. """Generiert den JSON-Report."""
  229. result: dict[str, Any] = {}
  230. if self._include_timestamp:
  231. result["timestamp"] = datetime.now().isoformat()
  232. # Standard-Metriken
  233. result["metrics"] = [
  234. self._format_metric(m)
  235. for m in self._collector.get_all_metrics().values()
  236. ]
  237. # Service-Metriken
  238. result["services"] = [
  239. sm.to_dict()
  240. for sm in self._collector.get_all_service_metrics().values()
  241. ]
  242. return json.dumps(result, indent=self._indent, default=str)
  243. class ConsoleReporter(MetricsReporter):
  244. """
  245. Reporter für Konsolen-Ausgabe.
  246. Generiert eine menschenlesbare Darstellung der Metriken.
  247. """
  248. def report(self) -> str:
  249. """Generiert den Konsolen-Report."""
  250. lines: list[str] = []
  251. lines.append("=" * 60)
  252. lines.append("METRIKEN-REPORT")
  253. lines.append(f"Zeitpunkt: {datetime.now().isoformat()}")
  254. lines.append("=" * 60)
  255. # Standard-Metriken
  256. metrics = self._collector.get_all_metrics()
  257. if metrics:
  258. lines.append("\n--- METRIKEN ---")
  259. for name, metric in sorted(metrics.items()):
  260. if isinstance(metric, (Counter, Gauge)):
  261. for mv in metric.get_all_values():
  262. label_str = ""
  263. if mv.labels:
  264. label_str = str(mv.labels)
  265. lines.append(f" {name}{label_str}: {mv.value}")
  266. elif isinstance(metric, Histogram):
  267. for key, observations in metric._observations.items():
  268. labels = dict(key) if key else {}
  269. label_str = str(labels) if labels else ""
  270. lines.append(f" {name}{label_str}:")
  271. lines.append(f" count: {len(observations)}")
  272. if observations:
  273. lines.append(f" sum: {sum(observations):.4f}")
  274. lines.append(f" p50: {metric.get_percentile(50, labels):.4f}")
  275. lines.append(f" p99: {metric.get_percentile(99, labels):.4f}")
  276. # Service-Metriken
  277. services = self._collector.get_all_service_metrics()
  278. if services:
  279. lines.append("\n--- SERVICES ---")
  280. for name, sm in sorted(services.items()):
  281. lines.append(f" {name}:")
  282. lines.append(f" startup_time: {sm.startup_time:.4f}s")
  283. lines.append(f" health_checks: {sm.health_check_count}")
  284. lines.append(f" errors: {sm.error_count}")
  285. if sm.custom_metrics:
  286. for m_name, m_value in sm.custom_metrics.items():
  287. lines.append(f" {m_name}: {m_value}")
  288. lines.append("\n" + "=" * 60)
  289. return "\n".join(lines)