"""In-process AI automation metrics for Prometheus export. These counters are intentionally lightweight and dependency-free. The `/metrics` route converts snapshots into Prometheus gauges so EventRouter and AutoHeal do not need to know about Flask or prometheus_client. """ from __future__ import annotations import threading from collections import defaultdict from copy import deepcopy from typing import Any, Dict, Tuple _LOCK = threading.Lock() _COUNTERS: Dict[Tuple[str, Tuple[Tuple[str, str], ...]], int] = defaultdict(int) _LATENCY: Dict[Tuple[str, Tuple[Tuple[str, str], ...]], Dict[str, float]] = defaultdict( lambda: {"count": 0, "sum": 0.0, "max": 0.0} ) def _safe_label(value: Any, default: str = "unknown", limit: int = 80) -> str: text = str(value if value not in (None, "") else default) return text[:limit] def _key(metric: str, labels: Dict[str, Any]) -> Tuple[str, Tuple[Tuple[str, str], ...]]: return metric, tuple(sorted((k, _safe_label(v)) for k, v in labels.items())) def _inc(metric: str, labels: Dict[str, Any], amount: int = 1) -> None: with _LOCK: _COUNTERS[_key(metric, labels)] += int(amount) def _observe(metric: str, labels: Dict[str, Any], value: float) -> None: with _LOCK: bucket = _LATENCY[_key(metric, labels)] bucket["count"] += 1 bucket["sum"] += float(value) bucket["max"] = max(bucket["max"], float(value)) def record_event_router_dispatch( *, tier: str, event_type: str, delivered: bool, queued: bool = False, deduped: bool = False, silenced: bool = False, latency_ms: int | float = 0, ) -> None: if silenced: outcome = "silenced" elif deduped: outcome = "deduped" elif queued: outcome = "queued" elif delivered: outcome = "delivered" else: outcome = "failed" labels = {"tier": tier, "event_type": event_type, "outcome": outcome} _inc("event_router_dispatch_total", labels) _observe("event_router_latency_ms", {"tier": tier, "event_type": event_type}, latency_ms) def record_event_router_safe_action(action: str, status: str) -> None: _inc("event_router_safe_action_total", {"action": action, "status": status}) def record_event_router_replay(*, attempted: int = 0, sent: int = 0, failed: int = 0, dropped: int = 0) -> None: for status, value in { "attempted": attempted, "sent": sent, "failed": failed, "dropped": dropped, }.items(): if value: _inc("event_router_replay_total", {"status": status}, int(value)) def record_autoheal_action( *, action: str, error_type: str, success: bool, duration_ms: int | float = 0, ) -> None: labels = { "action": action, "error_type": error_type, "result": "success" if success else "failed", } _inc("autoheal_action_total", labels) _observe("autoheal_duration_ms", {"action": action, "error_type": error_type}, duration_ms) def snapshot() -> Dict[str, Any]: with _LOCK: return { "counters": {key: value for key, value in _COUNTERS.items()}, "latency": {key: deepcopy(value) for key, value in _LATENCY.items()}, } def reset_for_tests() -> None: with _LOCK: _COUNTERS.clear() _LATENCY.clear()