#!/usr/bin/env python """monitor_health.py โ€” i forward-monitor stanno registrando? Riporta e allerta, non tocca nulla. Tre gate pre-registrati (STATARB 27/09, XSR01 23/10, DVOLSPREAD 24/10) si decideranno leggendo serie forward che, tranne una, **nessuno sorvegliava**. Un monitor fermo produce silenzio, e il silenzio in una serie di ritorni si legge come "zero", cioe' come una misura. Non lo e'. Vedi `src/live/monitor_health.py` per le due misure (coda + buchi interni), le cadenze per monitor e la soglia di copertura 0.80 (la stessa del veto d'integrita' di DVOLSPREAD). uv run python scripts/live/monitor_health.py # report uv run python scripts/live/monitor_health.py --quiet # stampa/allerta solo se scatta uv run python scripts/live/monitor_health.py --solo-orari # (#30) solo i sorveglianti a # max_age<=6h (cuscino_watch, venue_watch, collect_chain), notifica solo il NUOVO # rispetto a data/live/monitor_health_detto.json โ€” pensato per girare ogni ora # (cron_cuscino.sh), non per sostituire il giro giornaliero completo. """ from __future__ import annotations import json import sys from datetime import datetime, timezone from pathlib import Path ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) from src.live.monitor_health import ( # noqa: E402 MIN_COVERAGE, alert_nuovi, alerts_from, check_all, marcatore_aggiornato, monitors_orari, ) from src.live.notifier import notify # noqa: E402 # (#30) marcatore "gia' detto" del giro orario: {"detti": [], "ts": ...}. Vive fuori # da src/ perche' e' I/O, non parte della misura pura. DETTO = ROOT / "data" / "live" / "monitor_health_detto.json" def _carica_detti(path: Path = DETTO) -> set[str]: if not path.exists(): return set() try: return set(json.loads(path.read_text()).get("detti", [])) except Exception: return set() def _salva_detti(detti: set[str], path: Path = DETTO) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps( {"detti": sorted(detti), "ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")}, indent=2)) def main_orari() -> int: """(#30) Giro orario: solo i sorveglianti a max_age<=6h, notifica solo il NUOVO rispetto al marcatore. Exit 2 se c'e' un alert (anche gia' detto), 0 altrimenti โ€” il giro giornaliero resta l'unico posto dove "nessun alert nuovo" vuol dire davvero "tutto OK".""" quiet = "--quiet" in sys.argv rows = check_all(ROOT, monitors=monitors_orari()) alerts = alerts_from(rows) detti = _carica_detti(DETTO) # DETTO letto qui, non nel default del parametro (monkeypatch) nuovi = alert_nuovi(rows, detti) aggiornato = marcatore_aggiornato(rows, detti) if not quiet or alerts: print("=" * 92) print(" MONITOR HEALTH โ€” giro orario (sorveglianti con max_age <= 6h)") print("=" * 92) for r in rows: cov = f"{r['coverage']:.0%}" if r["coverage"] is not None else "-" age = f"{r['age_h']:.0f}h" if r["age_h"] is not None else "-" print(f" {r['name']:<20}{r['status']:>10}{age:>13}{cov:>10}") inviato = True if nuovi: msgs = alerts_from(nuovi) inviato = notify("๐Ÿฉบ MONITOR HEALTH โ€” un sorvegliante orario non sta registrando", {f"alert {i+1}": m for i, m in enumerate(msgs)}) for m in msgs: print(f" ALERT (nuovo): {m}") if not inviato: print(" โš ๏ธ invio FALLITO โ€” marcatore NON aggiornato: il prossimo giro ritenta") if not nuovi or inviato: _salva_detti(aggiornato, DETTO) return 2 if alerts else 0 def main() -> int: if "--solo-orari" in sys.argv: return main_orari() rows = check_all(ROOT) alerts = alerts_from(rows) if "--quiet" not in sys.argv or alerts: print("=" * 92) print(" MONITOR HEALTH โ€” i forward-monitor stanno ancora registrando?") print("=" * 92) print(f"\n {'monitor':<20}{'stato':>10}{'barre':>8}{'attese':>8}" f"{'copert.':>10}{'eta ultima':>13} gate che ne dipende") for r in rows: cov = f"{r['coverage']:.0%}" if r["coverage"] is not None else "-" exp = f"{r['expected']}" if r["expected"] is not None else "-" age = f"{r['age_h']:.0f}h" if r["age_h"] is not None else "-" gate = (r["gate"] or "").split(" โ€” ")[0] print(f" {r['name']:<20}{r['status']:>10}{r['n_bars']:>8}{exp:>8}" f"{cov:>10}{age:>13} {gate}") print(f"\n soglia di copertura {MIN_COVERAGE:.0%} (= veto d'integrita' di DVOLSPREAD) ยท " f"'NUOVO' = troppo giovane per un giudizio, non 'OK'") for r in rows: if r["why"] and r["status"] != "OK": print(f" {r['name']}: {r['why']}") if alerts: notify("๐Ÿฉบ MONITOR HEALTH โ€” un forward-monitor non sta registrando", {f"alert {i+1}": a for i, a in enumerate(alerts)}) for a in alerts: print(f" ALERT: {a}") return 2 return 0 USO = """uso: monitor_health.py [--quiet] [--solo-orari] monitor_health.py โ€” i forward-monitor stanno registrando? Riporta e allerta, non tocca nulla. uv run python scripts/live/monitor_health.py # report uv run python scripts/live/monitor_health.py --quiet # stampa/allerta solo se scatta uv run python scripts/live/monitor_health.py --solo-orari # (#30) solo i sorveglianti a max_age<=6h, notifica solo il NUOVO rispetto al marcatore Dettaglio nel docstring in testa al file.""" if __name__ == "__main__": from src.live.cli import valida valida("monitor_health.py", USO, flag=("--quiet", "--solo-orari"), con_valore=()) raise SystemExit(main())