feat: Resilienz (Fallback-Ketten) und Metriken (#5)
- 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>
This commit is contained in:
parent
093da817d8
commit
6422444017
13 changed files with 452 additions and 23 deletions
82
app/metrics.py
Normal file
82
app/metrics.py
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
"""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
|
||||
Loading…
Add table
Add a link
Reference in a new issue