J’ai arrêté d’écrire mes SLOs Prometheus à la main
J'ai construit nines.arewel.com pour générer mes SLOs Prometheus en trois étapes. Demo : disponibilité de l'UI Proxmox sur heighliner, déploiement du ZIP validé sur hubble.
Grafana m’a réveillé un mardi matin avec une alerte « CPU > 85% sur heighliner ». L’alerte disait 87%, durée 8 minutes, et c’est tout. Pour savoir POURQUOI, j’ai dû ouvrir Grafana, naviguer vers les dashboards, croiser avec les logs Loki, et finalement identifier la VM coupable. 12 minutes de manipulation pour une alerte qui aurait pu me donner la réponse directement. C’est là que j’ai décidé de brancher l’API Claude sur la stack.
L’objectif n’était pas de remplacer Alertmanager ou Prometheus. Ces outils font très bien leur travail de collecte, d’évaluation et de routage. Ce que je voulais, c’est une couche d’analyse en langage naturel qui synthétise les données déjà disponibles dans la stack Hubble pour rendre les alertes et les rapports lisibles par un humain fatigué.
Trois cas d’usage concrets, dans l’ordre où je les ai implémentés.
Le script récupère les 200 dernières lignes de journal Proxmox via journalctl, les envoie à Claude API, et reçoit un résumé structuré.
#!/usr/bin/env python3
import subprocess
import anthropic
import os
from datetime import datetime
client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
def get_recent_logs(lines=200):
result = subprocess.run(
["journalctl", "-u", "pvedaemon", "-u", "pveproxy",
"-u", "pve-cluster", "--no-pager", "-n", str(lines)],
capture_output=True, text=True, check=True
)
return result.stdout
def analyze_logs(log_content: str) -> str:
message = client.messages.create(
model="claude-haiku-4-5",
max_tokens=512,
system=(
"Tu es un ingénieur SRE. Analyse ces logs Proxmox et réponds "
"en français avec exactement 3 bullets maximum. "
"Format : - [NIVEAU] description courte. "
"Niveaux : INFO, WARN, ERROR. "
"Ne répète pas les informations normales. "
"Focus sur les anomalies et les erreurs répétées."
),
messages=[{"role": "user", "content": f"Logs Proxmox récents :nn{log_content}"}]
)
return message.content[0].text
if __name__ == "__main__":
logs = get_recent_logs()
summary = analyze_logs(logs)
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
print(f"[{timestamp}] Analyse logs Proxmox :n{summary}")
Le modèle utilisé ici est claude-haiku-4-5 – plus rapide et moins cher pour ce genre de tâche de classification. Le prompt système est court et directif : 3 bullets max, en français, focus sur les anomalies. Sans cette contrainte, les premières versions du script produisaient des pavés de 15 lignes inutilisables.
Stocker ANTHROPIC_API_KEY dans /etc/environment ou dans un fichier .env chargé par le script. Jamais en dur dans le code.
C’est le plus utile des trois. Grafana peut envoyer des webhooks quand une alerte se déclenche. J’ai un petit serveur Flask qui reçoit ce webhook, récupère le contexte (métriques Prometheus, logs Loki), interroge Claude API, et envoie une notification Telegram enrichie.
#!/usr/bin/env python3
# /opt/homelab-scripts/alert-enricher.py
from flask import Flask, request, jsonify
import anthropic
import requests
import os
app = Flask(__name__)
client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
PROMETHEUS_URL = "http://hubble.arewel.com:9090"
LOKI_URL = "http://hubble.arewel.com:3100"
TELEGRAM_TOKEN = os.environ["TELEGRAM_TOKEN"]
TELEGRAM_CHAT_ID = os.environ["TELEGRAM_CHAT_ID"]
def get_prometheus_context(instance: str, metric: str) -> str:
query = f'{metric}{{instance="{instance}"}}'
response = requests.get(
f"{PROMETHEUS_URL}/api/v1/query",
params={"query": query},
timeout=5
)
data = response.json()
if data["data"]["result"]:
return str(data["data"]["result"][0]["value"][1])
return "N/A"
def get_loki_recent_errors(instance: str, minutes=15) -> str:
end_ns = int(__import__("time").time() * 1e9)
start_ns = end_ns - minutes * 60 * int(1e9)
response = requests.get(
f"{LOKI_URL}/loki/api/v1/query_range",
params={
"query": f'{{host="{instance}"}} |= "error" | last 20',
"start": start_ns,
"end": end_ns,
"limit": 20
},
timeout=5
)
results = response.json().get("data", {}).get("result", [])
if not results:
return "Aucune erreur récente dans Loki."
lines = []
for stream in results:
for _, line in stream.get("values", []):
lines.append(line[:200])
return "n".join(lines[:10])
def enrich_alert(alert_name: str, instance: str, value: str, context: dict) -> str:
prompt = (
f"Alerte : {alert_name}n"
f"Instance : {instance}n"
f"Valeur déclenchante : {value}n"
f"CPU actuel : {context.get('cpu', 'N/A')}%n"
f"Mémoire disponible : {context.get('mem_free', 'N/A')} MBn"
f"Erreurs récentes Loki :n{context.get('loki_errors', 'aucune')}"
)
message = client.messages.create(
model="claude-haiku-4-5",
max_tokens=256,
system=(
"Tu es un SRE. Donne une hypothèse courte (2-3 phrases max) "
"sur la cause probable de cette alerte, en français. "
"Commence par la cause la plus probable. "
"Conclus par une action immédiate suggérée."
),
messages=[{"role": "user", "content": prompt}]
)
return message.content[0].text
def send_telegram(text: str):
requests.post(
f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage",
json={"chat_id": TELEGRAM_CHAT_ID, "text": text, "parse_mode": "Markdown"},
timeout=10
)
@app.route("/webhook/alert", methods=["POST"])
def handle_alert():
payload = request.json
for alert in payload.get("alerts", []):
alert_name = alert["labels"].get("alertname", "Inconnu")
instance = alert["labels"].get("instance", "heighliner")
value = alert.get("annotations", {}).get("value", "?")
context = {
"cpu": get_prometheus_context(instance, "node_cpu_usage_percent"),
"mem_free": get_prometheus_context(instance, "node_memory_MemFree_bytes"),
"loki_errors": get_loki_recent_errors(instance.split(":")[0])
}
enriched = enrich_alert(alert_name, instance, value, context)
message = f"*Alerte : {alert_name}*nInstance : {instance}nValeur : {value}nn{enriched}"
send_telegram(message)
return jsonify({"status": "ok"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5001)
Ce service tourne dans un LXC dédié sur heighliner. Dans Grafana, j’ai ajouté un contact point de type Webhook pointant vers http://[IP-LXC]:5001/webhook/alert.
Le vendredi soir, un systemd timer lance un script qui récupère les métriques de la semaine depuis Prometheus, les envoie à Claude API (modèle claude-sonnet-4-6 pour celui-là, la complexité est plus élevée), et envoie le rapport par email.
#!/usr/bin/env python3
# /opt/homelab-scripts/weekly-report.py
import anthropic
import requests
import smtplib
from email.mime.text import MIMEText
from datetime import datetime, timedelta
import os
client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
PROMETHEUS_URL = "http://hubble.arewel.com:9090"
def query_prometheus_range(query: str, hours=168) -> list:
end = datetime.now()
start = end - timedelta(hours=hours)
response = requests.get(
f"{PROMETHEUS_URL}/api/v1/query_range",
params={
"query": query,
"start": start.isoformat() + "Z",
"end": end.isoformat() + "Z",
"step": "3600"
},
timeout=10
)
return response.json().get("data", {}).get("result", [])
def collect_weekly_metrics() -> str:
cpu_data = query_prometheus_range("avg(node_cpu_usage_percent{instance='heighliner'})")
mem_data = query_prometheus_range("node_memory_MemAvailable_bytes{instance='heighliner'} / 1024 / 1024 / 1024")
cpu_values = [float(v[1]) for r in cpu_data for v in r.get("values", []) if v[1] != "NaN"]
mem_values = [float(v[1]) for r in mem_data for v in r.get("values", []) if v[1] != "NaN"]
summary = []
if cpu_values:
summary.append(f"CPU moyen : {sum(cpu_values)/len(cpu_values):.1f}%, pic : {max(cpu_values):.1f}%")
if mem_values:
summary.append(f"Mémoire disponible moyenne : {sum(mem_values)/len(mem_values):.1f} GB, minimum : {min(mem_values):.1f} GB")
return "n".join(summary)
def generate_report(metrics: str) -> str:
message = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system=(
"Tu es un SRE qui rédige un rapport hebdomadaire de son homelab. "
"Rédige en français, ton factuel. "
"Structure : état général (1 phrase), points notables (3 bullets max), "
"recommandation pour la semaine suivante (1 phrase). "
"Pas de jargon inutile. Pas de faux enthousiasme."
),
messages=[{
"role": "user",
"content": f"Métriques homelab heighliner, semaine du {(datetime.now() - timedelta(days=7)).strftime('%d/%m')} au {datetime.now().strftime('%d/%m/%Y')} :nn{metrics}"
}]
)
return message.content[0].text
def send_email(subject: str, body: str):
msg = MIMEText(body, "plain", "utf-8")
msg["Subject"] = subject
msg["From"] = "homelab@heighliner.local"
msg["To"] = "you@arewel.com"
with smtplib.SMTP("localhost") as smtp:
smtp.send_message(msg)
if __name__ == "__main__":
metrics = collect_weekly_metrics()
report = generate_report(metrics)
subject = f"Rapport homelab semaine {datetime.now().strftime('%W/%Y')}"
send_email(subject, report)
print("Rapport envoyé.")
Le timer systemd correspondant déclenche ce script le vendredi à 20h00. Postfix tourne en relayage local sur heighliner pour l’envoi d’email.
Claude n’a pas accès direct aux systèmes. Tout ce que le modèle reçoit, c’est du texte que mes scripts lui envoient. Si un script récupère des données incomplètes ou malformatées, l’analyse sera bancale.
Les hallucinations existent. Lors des premières semaines, Claude a suggéré à plusieurs reprises des causes plausibles mais fausses pour des alertes. Sur un pic CPU, il a supposé qu’une VM compilait du code alors que c’était un job de backup PBS mal planifié. Je traite les diagnostics Claude comme une hypothèse de départ, pas comme un diagnostic définitif. La vérification dans Grafana/Loki reste obligatoire.
La latence est de 2 à 5 secondes pour une réponse API. Pour les rapports et les analyses de logs, c’est parfaitement acceptable. Pour les alertes temps-réel critiques, c’est un peu long – 5 secondes d’enrichissement pendant un incident, c’est supportable, mais pas idéal. Pour les alertes véritablement critiques (nœud Proxmox down, perte de quorum), Alertmanager envoie directement sur PagerDuty. L’enrichissement Claude est une couche supplémentaire d’information, pas le chemin critique.
Sur 3 semaines de test (environ 50 appels API par semaine) :
claude-haiku-4-5 pour les analyses de logs et l’enrichissement d’alertes : environ $1,50/mois. Les tokens entrants (logs, métriques) coûtent $0,80/million, les tokens sortants $4/million – pour des résumés courts, les tokens de sortie sont très limités.
claude-sonnet-4-6 pour le rapport hebdomadaire : environ $0,80/mois. Un rapport par semaine, contexte de ~500 tokens, réponse de ~300 tokens.
Total : environ $2,30/mois pour ces trois usages. Moins cher que la plupart des services SaaS de monitoring avec fonctionnalités d’IA, et j’ai le contrôle total sur ce qui est envoyé.
Le problème principal n’était pas technique, c’était le prompt. Les premiers résumés de logs étaient des pavés de 10 paragraphes qui analysaient chaque ligne individuellement. J’ai dû itérer sur le prompt système une dizaine de fois pour arriver à « 3 bullets max, focus anomalies ». La contrainte de longueur dans le prompt système est indispensable.
Le deuxième problème : les logs Proxmox contiennent beaucoup de bruit normal (démarrages de services, rotations de certificats, heartbeats de cluster). Envoyer 200 lignes brutes à Claude incluait trop de signal normal. J’ai ajouté un filtre grep -v côté script pour exclure les patterns connus avant l’envoi. Ça réduit les tokens entrants et améliore la qualité de l’analyse.
Deux problèmes réels ont été détectés plus rapidement grâce aux alertes enrichies. Le premier : un LXC qui fuyait de la mémoire progressivement sur plusieurs heures. L’alerte normale m’aurait dit « mémoire disponible < 2 GB". L'alerte enrichie m'a dit "mémoire disponible à 1.8 GB, tendance à la baisse sur 4 heures, dernière erreur Loki : OOM kill candidate pour le processus node_exporter dans le LXC 105″. J’ai redémarré le LXC et planifié une investigation.
Le second : un job PBS qui échouait silencieusement depuis plusieurs jours – les logs PBS indiquaient task error mais l’alerte Grafana ne couvrait pas ce cas. Claude l’a mentionné dans le rapport hebdomadaire en détectant des patterns d’erreur répétés dans les logs journalctl.
Je veux brancher les métriques de coût API Claude lui-même dans Prometheus – suivre le nombre de tokens consommés par semaine pour anticiper les dépenses. L’API Anthropic expose des endpoints de consommation que je n’ai pas encore intégrés.
L’autre piste : utiliser le cache de prompts Anthropic pour les rapports hebdomadaires. Le prompt système est identique chaque semaine, et avec le cache de préfixe, les tokens système pourraient ne pas être recomputés à chaque appel. Sur le volume actuel, l’économie est minime, mais c’est une bonne pratique à adopter.
Cet article est vivant - corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisis celui qui te convient.