d59aea7a0a
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
143 lines
5.8 KiB
Python
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())
|