# -*- coding: utf-8 -*- """ Metriken-Collector für Satellites. Sammelt verschiedene Metriken wie Latenz, Durchsatz, Fehler und Ressourcennutzung. """ from __future__ import annotations import logging from collections import deque from dataclasses import dataclass, field from datetime import datetime, timedelta from enum import Enum, auto from typing import Any, Callable class MetricType(Enum): """Typ der Metrik.""" COUNTER = auto() # Zähler (nur steigend) GAUGE = auto() # Momentanwert HISTOGRAM = auto() # Verteilung TIMING = auto() # Zeitmessung @dataclass class MetricValue: """Ein einzelner Metrik-Wert.""" name: str value: float timestamp: datetime = field(default_factory=datetime.now) metric_type: MetricType = MetricType.GAUGE labels: dict[str, str] = field(default_factory=dict) unit: str = "" def to_dict(self) -> dict[str, Any]: """Konvertiert zu Dictionary.""" return { "name": self.name, "value": self.value, "timestamp": self.timestamp.isoformat(), "type": self.metric_type.name, "labels": self.labels, "unit": self.unit, } @dataclass class SatelliteMetrics: """ Gesammelte Metriken für einen Satellite. Enthält verschiedene Metrik-Kategorien und deren historische Werte. """ satellite_id: str last_updated: datetime = field(default_factory=datetime.now) # Netzwerk latency_ms: float = 0.0 # Aktuelle Latenz packet_loss_percent: float = 0.0 # Paketverlusr bandwidth_kbps: float = 0.0 # Bandbreite messages_sent: int = 0 # Gesendete Nachrichten messages_received: int = 0 # Empfangene Nachrichten bytes_sent: int = 0 # Gesendete Bytes bytes_received: int = 0 # Empfangene Bytes # Verbindung connection_uptime_seconds: float = 0.0 reconnect_count: int = 0 last_heartbeat: datetime | None = None # Audio (falls relevant) audio_level_db: float = 0.0 audio_samples_processed: int = 0 # Errors error_count: int = 0 last_error: str = "" last_error_time: datetime | None = None # Historische Daten (für Trends) latency_history: list[tuple[datetime, float]] = field(default_factory=list) def record_latency(self, latency_ms: float) -> None: """Zeichnet Latenz-Messung auf.""" self.latency_ms = latency_ms self.last_updated = datetime.now() self.latency_history.append((self.last_updated, latency_ms)) # Alte Einträge entfernen (max 1000) if len(self.latency_history) > 1000: self.latency_history = self.latency_history[-1000:] def record_error(self, error: str) -> None: """Zeichnet Fehler auf.""" self.error_count += 1 self.last_error = error self.last_error_time = datetime.now() self.last_updated = datetime.now() def get_avg_latency(self, seconds: int = 300) -> float: """Berechnet durchschnittliche Latenz der letzten Sekunden.""" cutoff = datetime.now() - timedelta(seconds=seconds) recent = [v for t, v in self.latency_history if t >= cutoff] return sum(recent) / len(recent) if recent else 0.0 def to_dict(self) -> dict[str, Any]: """Konvertiert zu Dictionary.""" return { "satellite_id": self.satellite_id, "last_updated": self.last_updated.isoformat(), "network": { "latency_ms": self.latency_ms, "packet_loss_percent": self.packet_loss_percent, "bandwidth_kbps": self.bandwidth_kbps, "messages_sent": self.messages_sent, "messages_received": self.messages_received, "bytes_sent": self.bytes_sent, "bytes_received": self.bytes_received, }, "connection": { "uptime_seconds": self.connection_uptime_seconds, "reconnect_count": self.reconnect_count, "last_heartbeat": ( self.last_heartbeat.isoformat() if self.last_heartbeat else None ), }, "errors": { "count": self.error_count, "last_error": self.last_error, "last_error_time": ( self.last_error_time.isoformat() if self.last_error_time else None ), }, } class SatelliteMetricsCollector: """ Sammelt Metriken von Satellites. Verwaltet Metriken für mehrere Satellites und bietet Methoden für Aufzeichnung und Abfrage. Beispiel: collector = SatelliteMetricsCollector() # Latenz aufzeichnen collector.record_latency("sat-001", 15.5) # Nachricht aufzeichnen collector.record_message_sent("sat-001", 256) # Metriken abrufen metrics = collector.get_metrics("sat-001") """ def __init__( self, history_size: int = 1000, logger: logging.Logger | None = None, ) -> None: """ Initialisiert den Collector. Args: history_size: Maximale Anzahl historischer Werte logger: Logger-Instanz """ self._history_size = history_size self.logger = logger or logging.getLogger(__name__) self._metrics: dict[str, SatelliteMetrics] = {} self._custom_metrics: dict[str, deque[MetricValue]] = {} self._callbacks: list[Callable[[str, MetricValue], None]] = [] @property def satellite_count(self) -> int: """Anzahl überwachter Satellites.""" return len(self._metrics) def _get_or_create(self, satellite_id: str) -> SatelliteMetrics: """Holt oder erstellt Metriken für Satellite.""" if satellite_id not in self._metrics: self._metrics[satellite_id] = SatelliteMetrics( satellite_id=satellite_id ) return self._metrics[satellite_id] def _notify(self, satellite_id: str, value: MetricValue) -> None: """Benachrichtigt registrierte Callbacks.""" for callback in self._callbacks: try: callback(satellite_id, value) except Exception as e: self.logger.error(f"Callback-Fehler: {e}") def on_metric( self, callback: Callable[[str, MetricValue], None], ) -> None: """Registriert Callback für neue Metriken.""" self._callbacks.append(callback) def record_latency(self, satellite_id: str, latency_ms: float) -> None: """ Zeichnet Latenz-Messung auf. Args: satellite_id: Satellite-ID latency_ms: Latenz in Millisekunden """ metrics = self._get_or_create(satellite_id) metrics.record_latency(latency_ms) value = MetricValue( name="latency", value=latency_ms, metric_type=MetricType.TIMING, unit="ms", ) self._notify(satellite_id, value) def record_message_sent( self, satellite_id: str, bytes_count: int, ) -> None: """Zeichnet gesendete Nachricht auf.""" metrics = self._get_or_create(satellite_id) metrics.messages_sent += 1 metrics.bytes_sent += bytes_count metrics.last_updated = datetime.now() def record_message_received( self, satellite_id: str, bytes_count: int, ) -> None: """Zeichnet empfangene Nachricht auf.""" metrics = self._get_or_create(satellite_id) metrics.messages_received += 1 metrics.bytes_received += bytes_count metrics.last_updated = datetime.now() def record_heartbeat(self, satellite_id: str) -> None: """Zeichnet Heartbeat auf.""" metrics = self._get_or_create(satellite_id) metrics.last_heartbeat = datetime.now() metrics.last_updated = datetime.now() def record_reconnect(self, satellite_id: str) -> None: """Zeichnet Reconnect auf.""" metrics = self._get_or_create(satellite_id) metrics.reconnect_count += 1 metrics.connection_uptime_seconds = 0.0 metrics.last_updated = datetime.now() def record_error(self, satellite_id: str, error: str) -> None: """Zeichnet Fehler auf.""" metrics = self._get_or_create(satellite_id) metrics.record_error(error) value = MetricValue( name="error", value=1, metric_type=MetricType.COUNTER, labels={"error": error[:100]}, # Begrenzen ) self._notify(satellite_id, value) def record_packet_loss( self, satellite_id: str, loss_percent: float, ) -> None: """Zeichnet Paketverlust auf.""" metrics = self._get_or_create(satellite_id) metrics.packet_loss_percent = loss_percent metrics.last_updated = datetime.now() def record_bandwidth( self, satellite_id: str, bandwidth_kbps: float, ) -> None: """Zeichnet Bandbreite auf.""" metrics = self._get_or_create(satellite_id) metrics.bandwidth_kbps = bandwidth_kbps metrics.last_updated = datetime.now() def record_custom( self, satellite_id: str, name: str, value: float, metric_type: MetricType = MetricType.GAUGE, labels: dict[str, str] | None = None, unit: str = "", ) -> None: """ Zeichnet benutzerdefinierte Metrik auf. Args: satellite_id: Satellite-ID name: Metrik-Name value: Wert metric_type: Metrik-Typ labels: Optionale Labels unit: Einheit """ metric_value = MetricValue( name=name, value=value, metric_type=metric_type, labels=labels or {}, unit=unit, ) key = f"{satellite_id}:{name}" if key not in self._custom_metrics: self._custom_metrics[key] = deque(maxlen=self._history_size) self._custom_metrics[key].append(metric_value) self._notify(satellite_id, metric_value) def get_metrics(self, satellite_id: str) -> SatelliteMetrics | None: """Holt Metriken für Satellite.""" return self._metrics.get(satellite_id) def get_all_metrics(self) -> dict[str, SatelliteMetrics]: """Holt alle Metriken.""" return self._metrics.copy() def get_custom_metric( self, satellite_id: str, name: str, ) -> list[MetricValue]: """Holt benutzerdefinierte Metrik-Historie.""" key = f"{satellite_id}:{name}" if key in self._custom_metrics: return list(self._custom_metrics[key]) return [] def remove_satellite(self, satellite_id: str) -> bool: """Entfernt Metriken für Satellite.""" if satellite_id in self._metrics: del self._metrics[satellite_id] # Custom Metriken entfernen to_remove = [ k for k in self._custom_metrics if k.startswith(f"{satellite_id}:") ] for key in to_remove: del self._custom_metrics[key] return True return False def get_summary(self) -> dict[str, Any]: """Liefert Zusammenfassung aller Metriken.""" if not self._metrics: return { "satellite_count": 0, "avg_latency_ms": 0, "total_messages": 0, "total_errors": 0, } avg_latency = sum( m.latency_ms for m in self._metrics.values() ) / len(self._metrics) total_messages = sum( m.messages_sent + m.messages_received for m in self._metrics.values() ) total_errors = sum( m.error_count for m in self._metrics.values() ) return { "satellite_count": self.satellite_count, "avg_latency_ms": round(avg_latency, 2), "total_messages": total_messages, "total_errors": total_errors, "satellites": { sat_id: { "latency_ms": m.latency_ms, "messages": m.messages_sent + m.messages_received, "errors": m.error_count, } for sat_id, m in self._metrics.items() }, } def clear_all(self) -> None: """Löscht alle Metriken.""" self._metrics.clear() self._custom_metrics.clear()