Files
PythagorasGoal/scripts/live/monitor_health.py
T

143 lines
5.8 KiB
Python

#!/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": [<chiave alert>], "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())