| 441 |
441 |
for service, block in mine.items(): |
| 442 |
442 |
if service not in SERVICES: |
| 443 |
443 |
continue # service inconnu de la topologie: visible au dashboard, pas d'action |
|
444 |
+ to_create: list[tuple[str, str, dict[str, Any]]] = [] |
| 444 |
445 |
for conn in block.get("connectors", []): |
| 445 |
446 |
source, status = conn["source"], conn["status"] |
| 446 |
447 |
inc = active_incident(c, service, source) |
| 457 |
458 |
(service, source)).fetchone() |
| 458 |
459 |
if ab and now() - ab["updated"] < POLICY.get("abandoned_retry_hours", 168) * 3600: |
| 459 |
460 |
continue |
| 460 |
|
− iid = uuid.uuid4().hex[:10] |
| 461 |
|
− c.execute("INSERT INTO incidents(id,service,source,status_detected,state,created,updated,detail) " |
| 462 |
|
− "VALUES(?,?,?,?,?,?,?,?)", |
| 463 |
|
− (iid, service, source, status, "open", now(), now(), jdump(conn))) |
| 464 |
|
− incident_event(iid, service, source, "open", f"détecté {status}") |
|
461 |
+ to_create.append((source, status, conn)) |
| 465 |
462 |
elif status in trigger and inc is not None and inc["state"] in ("open", "cooldown"): |
| 466 |
463 |
# Ne pas rafraîchir watching/fixing: `updated` sert de chrono |
| 467 |
464 |
# à la fenêtre de surveillance et au cooldown. |
| 474 |
471 |
elif inc["state"] in ("open", "cooldown"): |
| 475 |
472 |
set_incident(c, inc["id"], state="self_healed", resolved=now()) |
| 476 |
473 |
incident_event(inc["id"], service, source, "self_healed", "revenu à ok sans intervention") |
|
474 |
+ elif status == "retired" and inc is not None and inc["state"] in ("open", "cooldown", "watching"): |
|
475 |
+ # api-ka a retiré la source de la supervision (désactivée |
|
476 |
+ # côté app ou disparue du journal) : plus rien à réparer. |
|
477 |
+ set_incident(c, inc["id"], state="abandoned") |
|
478 |
+ incident_event(inc["id"], service, source, "abandoned", "source retirée de la supervision api-ka") |
|
479 |
+ |
|
480 |
+ # Damper d'événement de masse : des centaines de sources qui |
|
481 |
+ # basculent d'un coup signalent une panne GLOBALE de l'app (sync |
|
482 |
+ # mort, /api/stats cassé), pas autant de pannes individuelles — |
|
483 |
+ # missionner source par source serait long et coûteux pour rien |
|
484 |
+ # (2026-08-24 : 794 incidents jobka créés après ~17 h d'arrêt de |
|
485 |
+ # job-ka-sync). On n'ouvre rien : la panne d'app se voit au |
|
486 |
+ # dashboard (bloc _app) et les sources encore cassées une fois la |
|
487 |
+ # vague retombée sous le seuil recevront leurs incidents. |
|
488 |
+ limit = POLICY.get("mass_incident_threshold", 20) |
|
489 |
+ if len(to_create) > limit: |
|
490 |
+ msg = (f"vague de {len(to_create)} connecteurs en panne sur {service} " |
|
491 |
+ f"(seuil {limit}) — panne globale probable de l'app, aucun incident créé") |
|
492 |
+ print(f"[poll] {msg}", flush=True) |
|
493 |
+ hub.publish_sync({"kind": "log", "level": "error", "msg": msg, "ts": now()}) |
|
494 |
+ continue |
|
495 |
+ for source, status, conn in to_create: |
|
496 |
+ iid = uuid.uuid4().hex[:10] |
|
497 |
+ c.execute("INSERT INTO incidents(id,service,source,status_detected,state,created,updated,detail) " |
|
498 |
+ "VALUES(?,?,?,?,?,?,?,?)", |
|
499 |
+ (iid, service, source, status, "open", now(), now(), jdump(conn))) |
|
500 |
+ incident_event(iid, service, source, "open", f"détecté {status}") |
| 477 |
501 |
|
| 478 |
502 |
|
| 479 |
503 |
async def tick() -> None: |