File size: 7,107 Bytes
24480a0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
"""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)