| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358 |
- # -*- coding: utf-8 -*-
- """
- Metriken-Reporter für verschiedene Ausgabeformate.
- Bietet Export in Prometheus-Format, JSON und mehr.
- """
- from __future__ import annotations
- import json
- from abc import ABC, abstractmethod
- from datetime import datetime
- from typing import Any
- from trixy_core.service.metrics.collector import (
- Counter,
- Gauge,
- Histogram,
- Metric,
- MetricType,
- MetricsCollector,
- ServiceMetrics,
- )
- class MetricsReporter(ABC):
- """
- Abstrakte Basisklasse für Metriken-Reporter.
- """
- def __init__(self, collector: MetricsCollector) -> None:
- """
- Initialisiert den Reporter.
- Args:
- collector: Der MetricsCollector.
- """
- self._collector = collector
- @property
- def collector(self) -> MetricsCollector:
- """Gibt den Collector zurück."""
- return self._collector
- @abstractmethod
- def report(self) -> str:
- """
- Generiert den Metriken-Report.
- Returns:
- Report als String.
- """
- pass
- class PrometheusReporter(MetricsReporter):
- """
- Reporter für Prometheus-Format.
- Generiert Metriken im Prometheus Text Exposition Format.
- Example:
- collector = MetricsCollector()
- reporter = PrometheusReporter(collector)
- print(reporter.report())
- """
- def _format_labels(self, labels: dict[str, str]) -> str:
- """Formatiert Labels für Prometheus."""
- if not labels:
- return ""
- formatted = ", ".join(
- f'{k}="{v}"' for k, v in sorted(labels.items())
- )
- return f"{{{formatted}}}"
- def _format_counter(self, metric: Counter) -> list[str]:
- """Formatiert einen Counter."""
- lines = [
- f"# HELP {metric.name} {metric.help_text}",
- f"# TYPE {metric.name} counter",
- ]
- for mv in metric.get_all_values():
- labels = self._format_labels(mv.labels)
- lines.append(f"{metric.name}{labels} {mv.value}")
- return lines
- def _format_gauge(self, metric: Gauge) -> list[str]:
- """Formatiert eine Gauge."""
- lines = [
- f"# HELP {metric.name} {metric.help_text}",
- f"# TYPE {metric.name} gauge",
- ]
- for mv in metric.get_all_values():
- labels = self._format_labels(mv.labels)
- lines.append(f"{metric.name}{labels} {mv.value}")
- return lines
- def _format_histogram(self, metric: Histogram) -> list[str]:
- """Formatiert ein Histogram."""
- lines = [
- f"# HELP {metric.name} {metric.help_text}",
- f"# TYPE {metric.name} histogram",
- ]
- # Für jede Label-Kombination
- for key, observations in metric._observations.items():
- labels = dict(key) if key else {}
- base_labels = self._format_labels(labels)
- # Buckets
- buckets = metric.get_buckets(labels)
- for bucket, count in buckets.items():
- if bucket == float("inf"):
- bucket_str = "+Inf"
- else:
- bucket_str = str(bucket)
- if labels:
- all_labels = {**labels, "le": bucket_str}
- else:
- all_labels = {"le": bucket_str}
- formatted_labels = self._format_labels(all_labels)
- lines.append(f"{metric.name}_bucket{formatted_labels} {count}")
- # Sum und Count
- total = sum(observations)
- count = len(observations)
- lines.append(f"{metric.name}_sum{base_labels} {total}")
- lines.append(f"{metric.name}_count{base_labels} {count}")
- return lines
- def _format_service_metrics(self, sm: ServiceMetrics) -> list[str]:
- """Formatiert Service-Metriken."""
- lines = []
- prefix = f"service_{sm.service_name.replace('-', '_').replace('.', '_')}"
- # Startup-Zeit
- if sm.startup_time > 0:
- lines.append(f"# HELP {prefix}_startup_seconds Startup-Zeit")
- lines.append(f"# TYPE {prefix}_startup_seconds gauge")
- lines.append(f"{prefix}_startup_seconds {sm.startup_time}")
- # Health-Check
- if sm.health_check_count > 0:
- lines.append(f"# HELP {prefix}_health_check_duration_seconds Health-Check-Dauer")
- lines.append(f"# TYPE {prefix}_health_check_duration_seconds gauge")
- lines.append(f"{prefix}_health_check_duration_seconds {sm.health_check_duration}")
- lines.append(f"# HELP {prefix}_health_check_total Health-Check-Anzahl")
- lines.append(f"# TYPE {prefix}_health_check_total counter")
- lines.append(f"{prefix}_health_check_total {sm.health_check_count}")
- # Fehler
- lines.append(f"# HELP {prefix}_errors_total Fehler-Anzahl")
- lines.append(f"# TYPE {prefix}_errors_total counter")
- lines.append(f"{prefix}_errors_total {sm.error_count}")
- # Custom-Metriken
- for name, value in sm.custom_metrics.items():
- metric_name = f"{prefix}_{name.replace('-', '_').replace('.', '_')}"
- lines.append(f"# TYPE {metric_name} gauge")
- lines.append(f"{metric_name} {value}")
- return lines
- def report(self) -> str:
- """Generiert den Prometheus-Report."""
- lines: list[str] = []
- # Standard-Metriken
- for metric in self._collector.get_all_metrics().values():
- if isinstance(metric, Counter):
- lines.extend(self._format_counter(metric))
- elif isinstance(metric, Gauge):
- lines.extend(self._format_gauge(metric))
- elif isinstance(metric, Histogram):
- lines.extend(self._format_histogram(metric))
- lines.append("")
- # Service-Metriken
- for sm in self._collector.get_all_service_metrics().values():
- lines.extend(self._format_service_metrics(sm))
- lines.append("")
- return "\n".join(lines)
- class JSONReporter(MetricsReporter):
- """
- Reporter für JSON-Format.
- Generiert Metriken als JSON für APIs und Dashboards.
- """
- def __init__(
- self,
- collector: MetricsCollector,
- indent: int | None = 2,
- include_timestamp: bool = True,
- ) -> None:
- """
- Initialisiert den JSON-Reporter.
- Args:
- collector: Der MetricsCollector.
- indent: JSON-Einrückung.
- include_timestamp: Ob Timestamps inkludiert werden.
- """
- super().__init__(collector)
- self._indent = indent
- self._include_timestamp = include_timestamp
- def _format_metric(self, metric: Metric) -> dict[str, Any]:
- """Formatiert eine Metrik als Dictionary."""
- if isinstance(metric, Counter):
- values = []
- for mv in metric.get_all_values():
- entry: dict[str, Any] = {"value": mv.value}
- if mv.labels:
- entry["labels"] = mv.labels
- values.append(entry)
- return {
- "name": metric.name,
- "type": "counter",
- "help": metric.help_text,
- "values": values,
- }
- elif isinstance(metric, Gauge):
- values = []
- for mv in metric.get_all_values():
- entry = {"value": mv.value}
- if mv.labels:
- entry["labels"] = mv.labels
- values.append(entry)
- return {
- "name": metric.name,
- "type": "gauge",
- "help": metric.help_text,
- "values": values,
- }
- elif isinstance(metric, Histogram):
- data = []
- for key, observations in metric._observations.items():
- labels = dict(key) if key else {}
- entry: dict[str, Any] = {
- "count": len(observations),
- "sum": sum(observations),
- "buckets": metric.get_buckets(labels),
- }
- if labels:
- entry["labels"] = labels
- if observations:
- entry["percentiles"] = {
- "p50": metric.get_percentile(50, labels),
- "p90": metric.get_percentile(90, labels),
- "p99": metric.get_percentile(99, labels),
- }
- data.append(entry)
- return {
- "name": metric.name,
- "type": "histogram",
- "help": metric.help_text,
- "data": data,
- }
- return {"name": metric.name, "type": "unknown"}
- def report(self) -> str:
- """Generiert den JSON-Report."""
- result: dict[str, Any] = {}
- if self._include_timestamp:
- result["timestamp"] = datetime.now().isoformat()
- # Standard-Metriken
- result["metrics"] = [
- self._format_metric(m)
- for m in self._collector.get_all_metrics().values()
- ]
- # Service-Metriken
- result["services"] = [
- sm.to_dict()
- for sm in self._collector.get_all_service_metrics().values()
- ]
- return json.dumps(result, indent=self._indent, default=str)
- class ConsoleReporter(MetricsReporter):
- """
- Reporter für Konsolen-Ausgabe.
- Generiert eine menschenlesbare Darstellung der Metriken.
- """
- def report(self) -> str:
- """Generiert den Konsolen-Report."""
- lines: list[str] = []
- lines.append("=" * 60)
- lines.append("METRIKEN-REPORT")
- lines.append(f"Zeitpunkt: {datetime.now().isoformat()}")
- lines.append("=" * 60)
- # Standard-Metriken
- metrics = self._collector.get_all_metrics()
- if metrics:
- lines.append("\n--- METRIKEN ---")
- for name, metric in sorted(metrics.items()):
- if isinstance(metric, (Counter, Gauge)):
- for mv in metric.get_all_values():
- label_str = ""
- if mv.labels:
- label_str = str(mv.labels)
- lines.append(f" {name}{label_str}: {mv.value}")
- elif isinstance(metric, Histogram):
- for key, observations in metric._observations.items():
- labels = dict(key) if key else {}
- label_str = str(labels) if labels else ""
- lines.append(f" {name}{label_str}:")
- lines.append(f" count: {len(observations)}")
- if observations:
- lines.append(f" sum: {sum(observations):.4f}")
- lines.append(f" p50: {metric.get_percentile(50, labels):.4f}")
- lines.append(f" p99: {metric.get_percentile(99, labels):.4f}")
- # Service-Metriken
- services = self._collector.get_all_service_metrics()
- if services:
- lines.append("\n--- SERVICES ---")
- for name, sm in sorted(services.items()):
- lines.append(f" {name}:")
- lines.append(f" startup_time: {sm.startup_time:.4f}s")
- lines.append(f" health_checks: {sm.health_check_count}")
- lines.append(f" errors: {sm.error_count}")
- if sm.custom_metrics:
- for m_name, m_value in sm.custom_metrics.items():
- lines.append(f" {m_name}: {m_value}")
- lines.append("\n" + "=" * 60)
- return "\n".join(lines)
|