# backend/app/observability/metrics.py — 경량 Prometheus 메트릭 (의존성 없음) import threading from collections import defaultdict class _Registry: def __init__(self): self._lock = threading.Lock() self.counters: dict[tuple[str, tuple], float] = defaultdict(float) self.gauges: dict[tuple[str, tuple], float] = {} self.hist_sum: dict[tuple[str, tuple], float] = defaultdict(float) self.hist_count: dict[tuple[str, tuple], int] = defaultdict(int) self._help: dict[str, str] = {} def _key(self, name, labels): return (name, tuple(sorted((labels or {}).items()))) def inc(self, name, amount=1.0, **labels): with self._lock: self.counters[self._key(name, labels)] += amount def set_gauge(self, name, value, **labels): with self._lock: self.gauges[self._key(name, labels)] = value def observe(self, name, value, **labels): with self._lock: k = self._key(name, labels) self.hist_sum[k] += value self.hist_count[k] += 1 def help(self, name, text): self._help[name] = text def _fmt_labels(self, labels: tuple) -> str: if not labels: return "" inner = ",".join(f'{k}="{v}"' for k, v in labels) return "{" + inner + "}" def render(self) -> str: lines: list[str] = [] with self._lock: for (name, labels), v in sorted(self.counters.items()): lines.append(f"{name}_total{self._fmt_labels(labels)} {v}") for (name, labels), v in sorted(self.gauges.items()): lines.append(f"{name}{self._fmt_labels(labels)} {v}") for (name, labels), s in sorted(self.hist_sum.items()): lines.append(f"{name}_sum{self._fmt_labels(labels)} {s}") lines.append( f"{name}_count{self._fmt_labels(labels)} {self.hist_count[(name, labels)]}" ) return "\n".join(lines) + "\n" registry = _Registry() # 핵심 메트릭 헬퍼(운영 런북 §15.2) def http_request(method: str, path: str, status: int, dur_s: float) -> None: registry.inc("ari_http_requests", method=method, status=str(status)) registry.observe("ari_http_latency_seconds", dur_s, method=method) def llm_call(outcome: str) -> None: registry.inc("ari_llm_calls", outcome=outcome) def worker_job(job: str, outcome: str) -> None: registry.inc("ari_worker_jobs", job=job, outcome=outcome) def approval(risk: str, status: str) -> None: registry.inc("ari_approvals", risk=risk, status=status) def add_saved_minutes(m: float) -> None: registry.inc("ari_saved_minutes", m) def render() -> str: return registry.render()