"""backend/api/dashboard.py — Unified observability snapshot (INT-2). GET /api/dashboard/snapshot Aggrega in una sola chiamata HTTP: • provider health + score (da heartbeat + benchmark) • telemetry timing p50/p90 • _BENCH_LAST_RUN (ultimo quality benchmark) • incident count + ultimi 5 • LLM router stats (routing table + provider ok/fail) • plugin registry (total/loaded/list) Auth: nessuna (zero PII, solo aggregate). I valori sensibili rimangono mascherati. """ from __future__ import annotations import time import logging from fastapi import APIRouter from fastapi.responses import JSONResponse router = APIRouter() _logger = logging.getLogger("api.dashboard") @router.get("/api/dashboard/snapshot") async def dashboard_snapshot() -> JSONResponse: """Snapshot unificato — un endpoint, tutte le metriche operative.""" t0 = time.monotonic() out: dict = { "ok": True, "generated_at": int(time.time()), "version": "1.0", } # ── 1. Provider health + scores ────────────────────────────────────────── try: from api.state import _ai_health_cache, _PROVIDER_SCORES, _BENCH_LAST_RUN cached_data = _ai_health_cache.get("data") or {} raw_providers = cached_data.get("providers", []) out["providers"] = { r.get("name", r.get("provider", "?")): { "ok": r.get("ok", False), "status": r.get("status", "unknown"), "latency_ms": r.get("latency_ms"), "model": r.get("model"), "error": r.get("error") if not r.get("ok") else None, "score": _PROVIDER_SCORES.get( r.get("name", r.get("provider", "")), None ), } for r in raw_providers } # Sintesi: quanti ok, best provider per score scores_live = {k: v for k, v in _PROVIDER_SCORES.items() if v > 0} best = max(scores_live, key=scores_live.get) if scores_live else None out["provider_summary"] = { "total": len(raw_providers), "ok": sum(1 for r in raw_providers if r.get("ok")), "best_provider": best, "best_score": scores_live.get(best) if best else None, "checked_at_ago_s": round(time.monotonic() - _ai_health_cache.get("at", 0)), } out["bench_last_run"] = { "total_score": _BENCH_LAST_RUN.get("total_score"), "categories_run": _BENCH_LAST_RUN.get("categories_run", 0), "age_s": round(time.time() - _BENCH_LAST_RUN.get("timestamp", 0)) if _BENCH_LAST_RUN.get("timestamp") else None, } except Exception as exc: out["providers"] = {"_error": str(exc)} # ── 2. Telemetry (timing + repair) ────────────────────────────────────── try: from api.state import _TIMING_STORE, _REPAIR_STATS from api.telemetry import _percentile timing: dict = {} for key, buf in _TIMING_STORE.items(): samples = list(buf) if samples: timing[key] = { "p50": _percentile(samples, 50), "p90": _percentile(samples, 90), "n": len(samples), } # Repair summary: solo contatori non-zero repair_nonzero = {k: v for k, v in _REPAIR_STATS.items() if v > 0} out["telemetry"] = { "timing": timing, "repair_summary": repair_nonzero, "repair_total": sum(_REPAIR_STATS.values()), } except Exception as exc: out["telemetry"] = {"_error": str(exc)} # ── 3. Incidents ───────────────────────────────────────────────────────── try: from api.incident_registry import _incidents # type: ignore[attr-defined] incidents_list = list(_incidents.values()) if isinstance(_incidents, dict) else [] recent = sorted( incidents_list, key=lambda i: i.get("created_at", 0), reverse=True, )[:5] out["incidents"] = { "total": len(incidents_list), "recent_5": [ { "id": i.get("id", "?"), "provider": i.get("provider"), "severity": i.get("severity"), "message": str(i.get("message", ""))[:120], "at": i.get("created_at"), } for i in recent ], } except Exception as exc: out["incidents"] = {"_error": str(exc), "total": 0} # ── 4. LLM Router stats ────────────────────────────────────────────────── try: # LLMProviderRouter è un singleton lazy — usa la classe direttamente from api.llm_router import LLMProviderRouter rtr = LLMProviderRouter() out["llm_router"] = rtr.status() except Exception as exc: out["llm_router"] = {"_error": str(exc)} # ── 5. Plugin registry ──────────────────────────────────────────────────── try: from api.plugin_system import registry as _plugin_reg # list_plugins() ritorna lista di dict con stato plugins_raw = [ { "id": pid, "version": m.version, "state": _plugin_reg._states.get(pid, "unknown"), "caps": m.capabilities, } for pid, m in _plugin_reg._plugins.items() ] out["plugins"] = { "total": len(plugins_raw), "loaded": sum(1 for p in plugins_raw if p["state"] == "loaded"), "list": plugins_raw, } except Exception as exc: out["plugins"] = {"_error": str(exc), "total": 0} # ── 6. System health (da HealthManager) ───────────────────────────────── try: from api.health_manager import HealthManager hm = HealthManager() report = await hm.get_report() out["system_health"] = { "status": report.system_health.value if report else "unknown", "active_alerts": report.active_alerts if report else [], "recovery_actions": report.recovery_actions if report else [], } except Exception as exc: out["system_health"] = {"_error": str(exc)} out["elapsed_ms"] = round((time.monotonic() - t0) * 1000, 1) return JSONResponse(out)