| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407 |
- # -*- 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()
|