You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

83 lines
2.7 KiB
Python

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