fix(webhook): réponse HTTP 200 immédiate + ask-agent asynchrone

BrokenPipeError corrigé : send_response(200) envoyé avant subprocess.
ask-agent lancé en threading.Thread daemon — AlertManager n'attend plus.
NO_COLOR=1 + TERM=dumb pour éviter les codes ANSI dans les logs.

docs: alertmanager-webhook.md — architecture, flux, test, dépannage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
alkatrazz 2026-05-14 21:58:39 +02:00
parent c97e895b87
commit 3a013834aa
3 changed files with 384 additions and 13 deletions

View file

@ -0,0 +1,362 @@
# AlertManager Webhook — Pont Prometheus → Hermes
Service Python sur **storage-01** qui reçoit les alertes Prometheus via AlertManager
et les transmet au profil `monitor` de Hermes pour analyse IA.
---
## Architecture et flux
```
Prometheus (k8s, namespace monitoring)
│ Seuil dépassé (CPU, RAM, disk, pod crash...)
AlertManager (k8s, namespace monitoring)
│ POST http://192.168.10.1:9093/webhook
│ Content-Type: application/json
alertmanager-webhook.service (storage-01, port 9093)
│ Répond 200 immédiatement
│ Lance ask-agent en thread daemon
ask-agent monitor "Alertes Prometheus reçues à HH:MM:SS : ..."
│ POST http://127.0.0.1:4000/v1/chat/completions
LiteLLM Proxy (:4000)
│ modèle : qwen3-1.7b-monitor
llama-server-monitor (gpu-01:1237, CPU 14 threads)
│ Répond dans les logs du service
journalctl -u alertmanager-webhook
"ask-agent response: SERVICE SAIN — aucune action requise"
```
**Point clé** : le webhook répond HTTP 200 immédiatement, avant d'appeler `ask-agent`.
`ask-agent` tourne en arrière-plan (~20-60s) — AlertManager ne reste pas bloqué en attente.
---
## Fonctionnement du webhook.py
### 1. Réception de l'alerte
AlertManager envoie un payload JSON de cette forme :
```json
{
"alerts": [
{
"status": "firing",
"labels": {
"alertname": "NodeHighCPU",
"severity": "warning",
"instance": "192.168.10.1:9100"
},
"annotations": {
"summary": "CPU élevé sur storage-01",
"description": "CPU > 90% depuis 5 minutes (valeur : 93.2%)"
}
}
]
}
```
### 2. Formatage du message
Chaque alerte est formatée en texte lisible :
```
[FIRING] NodeHighCPU — severity=warning instance=192.168.10.1:9100
CPU élevé sur storage-01
CPU > 90% depuis 5 minutes (valeur : 93.2%)
```
Le message final envoyé à Hermes :
```
Alertes Prometheus reçues à 21:35:03 :
[FIRING] NodeHighCPU — severity=warning instance=192.168.10.1:9100
...
Analyse ces alertes : sont-elles critiques ? Quelles actions recommandes-tu ?
```
### 3. Exécution asynchrone
```python
# HTTP 200 envoyé immédiatement → AlertManager libéré
self.send_response(200)
self.end_headers()
self.wfile.write(b"ok")
# ask-agent lancé en thread daemon
threading.Thread(target=run_agent, daemon=True).start()
```
`ask-agent` est lancé avec `NO_COLOR=1` et `TERM=dumb` pour éviter les codes
ANSI dans les logs.
### 4. Réponse Hermes dans les logs
La réponse du modèle monitor apparaît dans `journalctl` ~20-60s après l'alerte :
```
INFO ask-agent response: SERVICE SAIN — CPU revenue à 45% après pic temporaire.
Aucune action requise. Surveiller sur 30 min.
```
---
## Service systemd
```bash
# État
sudo systemctl status alertmanager-webhook
# Logs en direct
sudo journalctl -u alertmanager-webhook -f
# Logs récents
sudo journalctl -u alertmanager-webhook -n 50 --no-pager
# Redémarrer
sudo systemctl restart alertmanager-webhook
```
Le service tourne sous l'utilisateur `alertmanager-webhook` avec des permissions minimales.
---
## Tester le webhook
### Test curl manuel
```bash
# Depuis le poste perso ou storage-01
curl -s -X POST http://192.168.10.1:9093/webhook \
-H "Content-Type: application/json" \
-d '{
"alerts": [{
"status": "firing",
"labels": {
"alertname": "TestAlert",
"severity": "warning",
"instance": "storage-01:9100"
},
"annotations": {
"summary": "Test alerte manuelle",
"description": "Vérification du pipeline webhook → Hermes"
}
}]
}'
# Réponse attendue : ok
# Suivre la réponse Hermes
ssh storage-01 "sudo journalctl -u alertmanager-webhook -f"
# Attendre ~30-60s → la réponse ask-agent apparaît
```
### Vérifier la connexion AlertManager → webhook
```bash
# Alertes actives dans AlertManager
curl -s http://alertmanager.lab.local/api/v2/alerts | python3 -m json.tool
# Récepteurs configurés dans AlertManager
curl -s http://alertmanager.lab.local/api/v2/receivers | python3 -m json.tool
# Statut du webhook dans AlertManager (UI)
# http://alertmanager.lab.local/#/status → section "receivers"
```
### Déclencher une vraie alerte
```bash
# Forcer une alerte Watchdog à passer par hermes-monitor (test de bout en bout)
# En modifiant temporairement la route dans values.yaml (à faire sur branche test)
# Ou créer un fichier flag d'alerte manuellement dans Prometheus
# (déconseillé en prod)
```
---
## Configuration AlertManager
Le webhook est enregistré dans `k8s/infra/monitoring/values.yaml` comme receiver AlertManager :
```yaml
alertmanager:
config:
route:
receiver: 'null' # receiver par défaut (Watchdog)
routes:
- matchers:
- alertname = "Watchdog"
receiver: 'null'
- receiver: 'hermes-monitor' # toutes les autres alertes → Hermes
receivers:
- name: 'null'
- name: 'hermes-monitor'
webhook_configs:
- url: 'http://192.168.10.1:9093/webhook'
send_resolved: true # envoie aussi les alertes résolues
```
`send_resolved: true` — quand une alerte passe de `firing` à `resolved`, AlertManager
envoie un nouveau payload avec `"status": "resolved"` — Hermes est informé de la résolution.
---
## Déploiement et mise à jour
### Fichiers
| Fichier | Emplacement | Rôle |
|---|---|---|
| `webhook.py` | `ansible/roles/alertmanager_webhook/files/webhook.py` | Code source (versionné) |
| `webhook.py` (déployé) | `/opt/alertmanager-webhook/webhook.py` (storage-01) | Instance active |
| Rôle Ansible | `ansible/roles/alertmanager_webhook/` | Déploiement, service systemd |
### Redéployer après modification
```bash
# Depuis le poste perso
scp ansible/roles/alertmanager_webhook/files/webhook.py \
storage-01:/tmp/webhook.py
ssh storage-01 "sudo cp /tmp/webhook.py /opt/alertmanager-webhook/webhook.py \
&& sudo systemctl restart alertmanager-webhook \
&& sudo systemctl status alertmanager-webhook --no-pager"
```
Ou via Ansible (une fois le rôle `hermes_user` en place) :
```bash
cd /mnt/nvme0n1/nvme0n1p3/Projets/lab
.venv/bin/ansible-playbook -i ansible/inventory.yml ansible/playbooks/site.yml \
--tags alertmanager_webhook
```
---
## Modifier le message envoyé à Hermes
Le prompt est construit dans `webhook.py`, fonction `do_POST` :
```python
message = "\n".join(lines) + "\n\nAnalyse ces alertes : sont-elles critiques ? Quelles actions recommandes-tu ?"
```
Pour personnaliser l'instruction donnée à Hermes, modifier cette ligne et redéployer.
---
## Ajouter un filtre d'alertes
Pour que certaines alertes n'arrivent pas à Hermes (trop bruyantes),
ajouter une route spécifique avant `hermes-monitor` dans `values.yaml` :
```yaml
routes:
- matchers:
- alertname = "Watchdog"
receiver: 'null'
- matchers:
- severity = "info" # silencer les alertes info
receiver: 'null'
- receiver: 'hermes-monitor' # tout le reste → Hermes
```
`git push` → ArgoCD applique en ~3 minutes.
---
## Dépannage
### BrokenPipeError dans les logs
**Symptôme** : `BrokenPipeError: [Errno 32] Broken pipe` dans `journalctl`.
**Cause** : ancienne version du webhook qui envoyait le HTTP 200 *après* `ask-agent`
(bloquant ~54s → AlertManager fermait la connexion avant).
**Solution** : vérifier que le webhook.py déployé correspond bien à la version actuelle
(200 envoyé immédiatement, `ask-agent` en thread).
```bash
ssh storage-01 "grep -n 'send_response\|threading' /opt/alertmanager-webhook/webhook.py"
# Doit afficher send_response AVANT threading.Thread
```
### Codes ANSI dans les logs (output illisible)
**Symptôme** : `ask-agent response: 790G'*%G+>,<...` (caractères parasites).
**Cause** : `ask-agent` utilise des couleurs terminal capturées brutes par subprocess.
**Solution** : vérifier que `NO_COLOR=1` est passé à subprocess :
```bash
ssh storage-01 "grep 'NO_COLOR' /opt/alertmanager-webhook/webhook.py"
# Doit afficher : "NO_COLOR": "1"
```
### Pas de réponse Hermes dans les logs (timeout)
`ask-agent monitor` a expiré après 120s. Causes possibles :
```bash
# LiteLLM répond ?
ssh storage-01 "curl -s http://localhost:4000/health \
-H 'Authorization: Bearer lm-studio'"
# llama-server-monitor tourne ?
ssh gpu-01 "systemctl status llama-server-monitor --no-pager"
# Logs ask-agent (stderr capturé)
ssh storage-01 "sudo journalctl -u alertmanager-webhook -n 30 --no-pager" \
| grep -E "stderr|error|timeout"
```
### Le webhook ne reçoit pas les alertes AlertManager
```bash
# 1. Le service écoute sur le bon port ?
ssh storage-01 "ss -tlnp | grep 9093"
# 2. nftables autorise le pod CIDR ?
ssh storage-01 "sudo nft list ruleset | grep 9093"
# Doit contenir : 10.42.0.0/16
# 3. AlertManager atteint storage-01 ?
kubectl logs -n monitoring \
alertmanager-kube-prometheus-stack-alertmanager-0 \
| grep -i webhook | tail -20
```
### Hermes répond mais l'analyse est mauvaise
Le profil `monitor` utilise Qwen3-1.7B (petit modèle CPU). Pour des analyses
plus poussées, modifier `ask-agent` pour utiliser `brain` (Claude Sonnet) sur les
alertes critiques — ou ajuster le SOUL.md du profil monitor.
```bash
# SOUL.md du profil monitor
ssh storage-01 "cat /srv/data/hermes/profiles/monitor/SOUL.md"
```
---
## Points d'attention
| Sujet | Détail |
|---|---|
| Réponse asynchrone | Hermes répond dans les logs 20-60s après l'alerte — pas de retour vers AlertManager |
| Alertes résolues | `send_resolved: true` — Hermes reçoit aussi les résolutions, utile pour confirmer le retour à la normale |
| Port 9093 | nftables doit autoriser `10.42.0.0/16` (pod CIDR) ET `192.168.10.0/24` sur ce port |
| Modèle monitor | Qwen3-1.7B CPU (~21 tok/s) — rapide mais limité. Convient pour des verdicts courts (OK/WARNING/ERROR) |
| Pas de stockage | Les analyses Hermes ne sont pas persistées — elles apparaissent uniquement dans `journalctl` |
| Watchdog | L'alerte Watchdog (heartbeat interne Prometheus) est routée vers `null` — ne pas la router vers Hermes (spam toutes les minutes) |

View file

@ -233,8 +233,10 @@ curl -s http://192.168.10.20:1237/metrics | grep llamacpp
## AlertManager → Hermes monitor ## AlertManager → Hermes monitor
AlertManager envoie les alertes en POST vers `http://192.168.10.1:9093/webhook`. AlertManager envoie les alertes en POST vers `http://192.168.10.1:9093/webhook`.
Le service `alertmanager-webhook` (Python, storage-01) reçoit les alertes et appelle `ask-agent monitor`. Le service `alertmanager-webhook` (Python, storage-01) reçoit les alertes, répond 200 immédiatement,
Hermes (profil `monitor`) analyse et répond dans ses logs. et lance `ask-agent monitor` en arrière-plan. Hermes (profil `monitor`) analyse et répond dans ses logs.
**Documentation complète** : [admin/ia/alertmanager-webhook.md](../ia/alertmanager-webhook.md)
```bash ```bash
# Vérifier le webhook # Vérifier le webhook

View file

@ -7,6 +7,7 @@ import json
import os import os
import subprocess import subprocess
import logging import logging
import threading
from http.server import HTTPServer, BaseHTTPRequestHandler from http.server import HTTPServer, BaseHTTPRequestHandler
from datetime import datetime from datetime import datetime
@ -63,21 +64,27 @@ class WebhookHandler(BaseHTTPRequestHandler):
log.info(f"Forwarding {len(firing)} firing + {len(resolved)} resolved alerts to ask-agent monitor") log.info(f"Forwarding {len(firing)} firing + {len(resolved)} resolved alerts to ask-agent monitor")
try:
result = subprocess.run(
["/usr/local/bin/ask-agent", "monitor", message],
capture_output=True, text=True, timeout=120
)
log.info(f"ask-agent response: {result.stdout[:200]}")
except subprocess.TimeoutExpired:
log.error("ask-agent timed out")
except Exception as e:
log.error(f"ask-agent error: {e}")
self.send_response(200) self.send_response(200)
self.end_headers() self.end_headers()
self.wfile.write(b"ok") self.wfile.write(b"ok")
def run_agent():
try:
env = {**os.environ, "NO_COLOR": "1", "TERM": "dumb"}
result = subprocess.run(
["/usr/local/bin/ask-agent", "monitor", message],
capture_output=True, text=True, timeout=120, env=env
)
log.info(f"ask-agent response: {result.stdout[:200]}")
if result.returncode != 0:
log.error(f"ask-agent stderr: {result.stderr[:200]}")
except subprocess.TimeoutExpired:
log.error("ask-agent timed out")
except Exception as e:
log.error(f"ask-agent error: {e}")
threading.Thread(target=run_agent, daemon=True).start()
def log_message(self, format, *args): def log_message(self, format, *args):
pass # silence HTTP access logs pass # silence HTTP access logs