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
93 lines
3.0 KiB
Python
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()
|