- Fallback-Provider (app/providers/fallback.py) fuer STT/LLM/TTS: Provider-Kette der Reihe nach; Config *_FALLBACK; build_orchestrator baut Ketten (dedupliziert) - LLM-Stream-Fallback nur solange kein Token gesendet wurde - Metriken (app/metrics.py): In-Memory Counter/Timer, keine externe Dependency - HTTP-Middleware (Requests/Latenz/Status je Pfad); Pipeline-Stufen-Timing stt/llm/tts; Fallback-/Fehlerzaehler; GET /api/metrics (JSON + Prometheus) - Tests: 58 gruen (+6); Doku aktualisiert (README, BEDIENUNGSANLEITUNG, Architektur, .env.example) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
82 lines
2.6 KiB
Python
82 lines
2.6 KiB
Python
"""Schlanke In-Memory-Metriken (Counter + Timer) fuer einen Prozess.
|
|
|
|
Bewusst ohne externe Dependency. Fuer mehrere Instanzen/Prozesse spaeter durch
|
|
einen gemeinsamen Backend (z. B. Prometheus-Exporter) ersetzbar.
|
|
"""
|
|
|
|
import threading
|
|
import time
|
|
from collections import defaultdict
|
|
|
|
|
|
class Metrics:
|
|
def __init__(self):
|
|
self._lock = threading.Lock()
|
|
self._counters: dict[str, float] = defaultdict(float)
|
|
self._timers: dict[str, list] = defaultdict(lambda: [0.0, 0]) # [sum, count]
|
|
|
|
@staticmethod
|
|
def _key(name: str, labels: dict | None) -> str:
|
|
if not labels:
|
|
return name
|
|
rendered = ",".join(f'{k}="{v}"' for k, v in sorted(labels.items()))
|
|
return f"{name}{{{rendered}}}"
|
|
|
|
def inc(self, name: str, labels: dict | None = None, value: float = 1.0) -> None:
|
|
with self._lock:
|
|
self._counters[self._key(name, labels)] += value
|
|
|
|
def observe(self, name: str, seconds: float, labels: dict | None = None) -> None:
|
|
with self._lock:
|
|
agg = self._timers[self._key(name, labels)]
|
|
agg[0] += seconds
|
|
agg[1] += 1
|
|
|
|
def snapshot(self) -> dict:
|
|
with self._lock:
|
|
counters = dict(self._counters)
|
|
timers = {
|
|
key: {
|
|
"sum": agg[0],
|
|
"count": agg[1],
|
|
"avg": (agg[0] / agg[1] if agg[1] else 0.0),
|
|
}
|
|
for key, agg in self._timers.items()
|
|
}
|
|
return {"counters": counters, "timers": timers}
|
|
|
|
def prometheus(self) -> str:
|
|
snap = self.snapshot()
|
|
lines = []
|
|
for key, value in sorted(snap["counters"].items()):
|
|
lines.append(f"{key} {value}")
|
|
for key, agg in sorted(snap["timers"].items()):
|
|
base, _, labels = key.partition("{")
|
|
suffix = ("{" + labels) if labels else ""
|
|
lines.append(f"{base}_sum{suffix} {agg['sum']}")
|
|
lines.append(f"{base}_count{suffix} {agg['count']}")
|
|
return "\n".join(lines) + "\n"
|
|
|
|
def reset(self) -> None:
|
|
with self._lock:
|
|
self._counters.clear()
|
|
self._timers.clear()
|
|
|
|
|
|
metrics = Metrics()
|
|
|
|
|
|
class timer:
|
|
"""Context-Manager: misst die Dauer und schreibt sie als Timer-Beobachtung."""
|
|
|
|
def __init__(self, name: str, labels: dict | None = None):
|
|
self.name = name
|
|
self.labels = labels
|
|
|
|
def __enter__(self):
|
|
self._start = time.perf_counter()
|
|
return self
|
|
|
|
def __exit__(self, *exc):
|
|
metrics.observe(self.name, time.perf_counter() - self._start, self.labels)
|
|
return False
|