Files
pablan/backend/app/metrics.py
T
ProfessorNovaandClaude Opus 5 784b76baf7 Pablan, as it stands
Self-hosted knowledge management for SMEs: a split-screen Markdown editor
whose sections an LLM refines while you write, and RAG question answering
over the documents that result. FastAPI + Postgres/pgvector on the back,
SvelteKit on the front, everything OpenAI-compatible and self-hostable.

Squashed into a single commit; the development history stays local.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CA43ZJda8Rbp2hKXNy8f6b
2026-09-04 09:21:37 +02:00

93 lines
3.0 KiB
Python

"""In-process metrics registry — no dependencies, single event loop.
Counters, gauges and histogram summaries (count/sum/min/max), labeled.
Exposed as JSON via GET /api/admin/metrics; a Prometheus text exporter would
sit on top of this registry rather than replace it.
"""
from collections import defaultdict
from dataclasses import dataclass
from typing import Any
LabelKey = tuple[tuple[str, str], ...]
def _key(labels: dict[str, str] | None) -> LabelKey:
return tuple(sorted((labels or {}).items()))
@dataclass
class HistogramData:
count: int = 0
total: float = 0.0
minimum: float | None = None
maximum: float | None = None
class MetricsRegistry:
def __init__(self) -> None:
self._counters: dict[str, dict[LabelKey, float]] = defaultdict(
lambda: defaultdict(float)
)
self._gauges: dict[str, dict[LabelKey, float]] = defaultdict(dict)
self._histograms: dict[str, dict[LabelKey, HistogramData]] = defaultdict(dict)
def inc(
self, name: str, labels: dict[str, str] | None = None, value: float = 1.0
) -> None:
self._counters[name][_key(labels)] += value
def set_gauge(
self, name: str, value: float, labels: dict[str, str] | None = None
) -> None:
self._gauges[name][_key(labels)] = value
def observe(
self, name: str, value: float, labels: dict[str, str] | None = None
) -> None:
data = self._histograms[name].setdefault(_key(labels), HistogramData())
data.count += 1
data.total += value
data.minimum = value if data.minimum is None else min(data.minimum, value)
data.maximum = value if data.maximum is None else max(data.maximum, value)
def snapshot(self) -> dict[str, Any]:
return {
"counters": {
name: [
{"labels": dict(key), "value": value}
for key, value in sorted(series.items())
]
for name, series in sorted(self._counters.items())
},
"gauges": {
name: [
{"labels": dict(key), "value": value}
for key, value in sorted(series.items())
]
for name, series in sorted(self._gauges.items())
},
"histograms": {
name: [
{
"labels": dict(key),
"count": data.count,
"sum": data.total,
"min": data.minimum,
"max": data.maximum,
"avg": data.total / data.count if data.count else None,
}
for key, data in sorted(series.items())
]
for name, series in sorted(self._histograms.items())
},
}
def reset(self) -> None:
self._counters.clear()
self._gauges.clear()
self._histograms.clear()
metrics = MetricsRegistry()