RCA (cause racine)
Quand une alerte part, Ongrid spawne un worker LLM qui pilote l'agent ReAct à graph kernel sur la persona incident-investigator, appelle des outils pour rassembler des preuves, et écrit un rapport structuré en retour dans investigation_reports.
Le rapport se rend sur la page /alerts/incidents/:id de la SPA à côté de la série en feu — le SRE humain ne doit jamais commencer « depuis un prompt vide ».
HLD-013
Le pipeline actuel atterrit les Phase 1+2 du modèle causal de HLD-013. L'approche naïve « résume ce qui est parti » (PR-2) a été remplacée une fois qu'il est devenu clair que les opérateurs voulaient le patient zéro — le processus / conteneur / ligne spécifique qui a démarré la cascade — pas un résumé du texte d'alarme.
Cycle de vie
incident.fire (isNew=true)
└─ alert.Usecase.RecordFiring
└─ Investigator.InvestigateAsync(incident)
└─ Enqueue → Gate 1-3 (severity, inflight, semaphore)
└─ repo.Create(pending row) ← UI shows "investigating…"
└─ go run(reportID, incident, dedupKey, locale)
├─ spawner.SpawnWorker(incident-investigator persona)
├─ worker drives the ReAct loop with tools
├─ Pass-2 structured extraction (cheap model)
└─ repo.MarkReady(report fields) ← UI shows readyInvestigateAsync est le seam public qu'appelle alert.Usecase — voir usecase.go:301.
Gates
Trois gates filtrent avant qu'un worker ne soit jamais spawné. Chaque rejet est persisté comme ligne status=skipped pour que la SPA montre à l'opérateur une raison plutôt que « pas démarré pour toujours ».
| Gate | Défaut | Comportement en miss |
|---|---|---|
Plancher de sévérité (Config.MinSeverity) | warning | Skip silencieux, aucune ligne écrite |
Inflight in-process (par incident_id) | toujours actif | Coalesce silencieux |
Plafond de concurrence (Config.MaxConcurrent) | 5 | Ligne skipped: concurrency limit reached (N workers in flight) |
Le plafond de concurrence défend les rate-limits du provider LLM et borne la RAM quand 100 incidents partent en même temps. Les appels au-dessus du plafond reçoivent une ligne skipped explicite, pas une file.
Le worker
Le runtime spawne un worker avec chatruntime.SpawnRequest :
// internal/manager/biz/alert/investigator/usecase.go:571
worker, err := uc.spawner.SpawnWorker(ctx, chatruntime.SpawnRequest{
AgentName: uc.cfg.AgentName, // "incident-investigator"
Prompt: prompt,
Background: false,
SessionKind: "investigation",
})Le prompt est rendu par renderAlertPrompt et inclut :
- Métadonnées d'incident (rule, severity, device_id, value, threshold, summary).
- Une instruction de démarrage explicite :
Start with correlate_incident to pull metrics + logs + traces + topology around the fire window. - Un budget dur : 10 appels d'outils max, doit commencer à écrire le rapport à l'appel #7. Ce budget vit dans le message utilisateur parce que les modèles non-frontière (GLM, DeepSeek) suivent les contraintes du message utilisateur plus fiablement que celles du message système — sans, le plafond MaxStep d'eino était atteint un run sur deux.
- Une directive de locale qui surcharge la langue implicite de la persona avec la locale d'UI de l'opérateur (voir Modèles / Routing pour comment la locale propage).
Budget d'outils + salvage
Le graphe ReAct d'eino plafonne les étapes totales. Quand un worker épuise le plafond sans écrire une réponse finale, l'investigator récupère la piste partielle :
MessageReader.ListMessages(sessionID, limit=100)tire chaque tour.- Les messages assistant + outil sont concaténés en un markdown synthétique « ce qu'on a trouvé ».
- Le salvage passe par le même extracteur Pass-2.
- Le rapport est marqué
readyavec une note de faible confiance préfixée :工作器超出最大步数预算(exceeds max steps);以下为根据已收集工具结果的局部分析,置信度偏低。
Sans salvage, l'opérateur voyait status=failed sans données utiles — le worker avait typiquement appelé 10+ outils et rassemblé la réponse, il n'avait juste jamais écrit le tour de synthèse.
Extraction structurée (Pass 2)
Le message d'assistant final du worker est du markdown — la SPA a besoin de champs structurés. Un second appel LLM bon marché extrait :
root_cause— TL;DR d'un paragraphe.affected_window— quand le span de symptôme a-t-il démarré / arrêté.pinpointed_target— le processus / conteneur / fichier spécifique qui a changé.related_alerts— incidents co-firants (viaRelatedAlertQuerier).evidence— citations sources à puces (sortie PromQL, lignes de logs, etc.).suggested_actions— prochaines étapes exécutables par opérateur.confidence+confidence_factors.tool_call_count— relu depuischat_messagespour que l'UI montre combien d'outils le worker a réellement invoqué (pas un 0 hardcodé).
Modèle + provider configurables via Config.SummarizerProvider / Config.SummarizerModel. Timeout de 30s par défaut (prompt court, réponse courte, pas de boucle d'outils).
Quand l'extracteur n'est pas câblé ou erre, le fallback utilise firstParagraphOneLine sur le markdown du worker pour remplir root_cause et livre tout le markdown verbatim comme findings_md.
Piège du bold-header
firstParagraphOneLine (usecase.go:846) saute l'échafaudage markdown pur (titres, séparateurs, titres de section entièrement en gras) pour que root_cause se lise comme une phrase, pas comme **现象**. Un bug précédent ne retirait que le ** initial et laissait une paire en fin — corrigé dans la même fonction.
Re-déclenchement manuel
POST /v1/alerts/incidents/{id}/investigation
Accept-Language: enForceEnqueue exécute le chemin manuel :
- Stoppe tout worker en cours pour cet incident (best-effort — warn et continue si le worker_id est stale post-restart).
- Hard-delete la ligne
investigation_reportsprécédente (couvre les lignes soft-deletées d'anciens force-enqueues pour que l'index uniqueincident_idne rejette pas le prochain Create). - Relâche le garde d'inflight.
- Appelle
EnqueueWithavec la locale parsée depuisAccept-Languagepour que le rapport régénéré revienne dans la langue d'UI de l'opérateur.
Le plancher de sévérité s'applique toujours — les déclenchements manuels sur des incidents de niveau info retournent des erreurs severity below floor.
Backfill au démarrage
Les installs fraîches touchent un problème de l'œuf et de la poule : la chaîne RCA structurée ne se câble que lorsqu'au moins un provider LLM est configuré, mais des incidents peuvent partir avant que l'opérateur n'ajoute un provider. BackfillUnstartedIncidents tourne au démarrage, parcourt ListIncidentsWithoutReport(since, limit), et ré-enqueue tout ce qui est parti dans la fenêtre. Les gates normaux s'appliquent toujours, donc les incidents résolus et au-dessus du plancher sont sautés.
Voir cmd/ongrid/main.go pour le câblage — il tourne au démarrage avec une fenêtre de 24h.
Voir aussi
- Alertes — ce qui part.
- Topologie — ce que
expand_topologyexpose pour la marche de blast-radius de l'investigator. - Skills —
correlate_incident,get_incident_detail,query_promql,search_logs: les outils que le worker appelle. - Modèles / Routing — comment la locale + provider par investigation est résolue.
- Modèles / Budget — le plafond global de tokens par jour qui borne le coût d'investigation.