Runbook d'incident
Guide opérationnel pour perf-sentinel en production. Chaque section est autonome : partez du symptôme qui correspond au vôtre, déroulez la liste des premiers contrôles, puis escaladez.
Si vous configurez perf-sentinel pour la première fois, consultez Intégration. Pour la référence de l'API HTTP, voir API de query. Pour les options de configuration, voir Configuration. Pour la liste de ce que le daemon ne garantit pas, voir Limites.
Sommaire
- Aide-mémoire diagnostic : commandes à lancer en premier
- Analyser une trace plus ancienne que la fenêtre live : workflow post-mortem
- Daemon en cours mais inaccessible depuis les clients
- Aucune trace ingérée
- Chute soudaine du volume d'ingestion
- Spike de findings critiques
- Référence de dimensionnement (mesurée)
- Pression mémoire ou OOM du daemon
- Quality gate CI en échec inattendu
- Investigation d'un acknowledgment inattendu
perf-sentinel temporenvoie 404 ou timeout- Exemplars absents dans Grafana
- Scraper d'énergie bloqué
/api/correlationsrenvoie vide/api/export/reportretourne 503 ou un rapport vide- Crash ou redémarrage du daemon
- Appliquer un changement de config
Aide-mémoire diagnostic
À lancer en premier quel que soit le symptôme. Ça donne la photo en 10 secondes de l'état du daemon.
# Le daemon est-il vivant ? HTTP 200 avec un corps de métriques = oui.
curl -sf http://perf-sentinel:4318/metrics | head -n 20
# Résumé de statut : uptime, traces actives, findings stockés, version
curl -s http://perf-sentinel:4318/api/status | jq .
# Santé d'ingestion d'un coup d'œil
curl -s http://perf-sentinel:4318/metrics \
| grep -E '^perf_sentinel_(events|traces|active)_'
# Findings critiques récents
curl -s 'http://perf-sentinel:4318/api/findings?severity=critical&limit=20' \
| jq '.[].finding | {finding_type, service, trace_id}'Logs du daemon avec verbosité ciblée (le daemon utilise la variable d'env RUST_LOG standard) :
RUST_LOG=sentinel_core::daemon=info # cycle de vie, bind, shutdown
RUST_LOG=sentinel_core::ingest=debug # chemin de réception OTLP, events rejetés
RUST_LOG=sentinel_core::detect=debug # pipeline de détection
RUST_LOG=sentinel_core::score=debug # scoring green, scrapers d'énergiePour les probes Kubernetes, utilisez l'endpoint dédié GET /health (toujours exposé, indépendamment de [daemon] api_enabled), qui retourne 200 OK avec {"status":"ok","version":"..."}. Plus léger que /metrics et garanti sans lock interne. Il n'y a pas d'endpoint /ready séparé : le daemon accepte l'ingestion dès le premier tick, donc liveness et readiness se confondent.
Analyser une trace plus ancienne que la fenêtre live
Pourquoi cette section existe. Le daemon garde les traces en mémoire pendant 30 secondes (trace_ttl_ms, défaut). Une fois évincée :
GET /api/explain/{trace_id}renvoie{"error": "trace not found in daemon memory"}GET /api/findings/{trace_id}renvoie toujours les findings (conservés dans le ring buffer jusqu'àmax_retained_findings = 10000), mais les spans eux-mêmes ont disparu, aucun explain tree ne peut être reconstruit depuis le daemon seul.
Pour tout ce qui est plus ancien, la source de vérité est votre backend de traces (typiquement Grafana Tempo).
Workflow en quatre étapes.
1. Alerte → Panel Grafana, spike sur perf_sentinel_findings_total
2. Clic exemplar → Grafana ouvre la trace dans Tempo via le label `trace_id`
3. Copie trace_id → depuis Tempo ou la charge utile de l'alerte
4. Rejeu → perf-sentinel tempo --endpoint <url> --trace-id <id>L'étape 4 fait passer la trace historique par le même pipeline normalize → correlate → detect → score → explain que le daemon. Vous obtenez les mêmes findings et le même explain tree, mais sur une trace du passé.
Invocations courantes.
# Expliquer une trace identifiée
perf-sentinel tempo --endpoint http://tempo:3200 --trace-id abc123def456
# Balayer un service sur une fenêtre quand le trace_id n'est pas encore connu
perf-sentinel tempo --endpoint http://tempo:3200 --service order-svc --lookback 2h
# Artefact post-mortem pour un ticket ou une PR
perf-sentinel tempo --endpoint http://tempo:3200 --trace-id abc123 --format json > incident.jsonLa sortie SARIF (--format sarif) est supportée si votre process incident utilise GitHub Code Scanning.
Solution de repli : Tempo indisponible. Si Tempo n'est pas joignable mais que vous avez un dump d'une autre source (export Jaeger/Zipkin, bucket S3 archivé, capture OTLP), passez le fichier directement :
perf-sentinel explain --input traces-dump.json --trace-id abc123def456
perf-sentinel analyze --input traces-dump.jsonCe qui ne marchera PAS.
| Tentative | Pourquoi |
|---|---|
curl /api/explain/<trace_id> sur le daemon live | Trace évincée après 30 s |
curl /api/findings pour reconstruire un explain tree | Le store garde les findings, pas les spans |
| Attendre que le daemon "refasse remonter" la trace | Pas de persistance, pas d'endpoint de rejeu |
| Redémarrer le daemon pour retrouver l'état | Rien n'est persisté sur disque |
Prérequis.
- Rétention Tempo couvrant la fenêtre de l'incident.
block_retentionpar défaut est de 14 jours mais varie selon le déploiement. - Sampling. Si la trace a été écartée à l'ingestion par un head- ou tail-based sampling, elle a disparu de Tempo aussi. Envisagez un sampling 100 % sur les traces en erreur.
- Propagation du
trace_id. Alertes et logs doivent porter le label. Les exemplars OpenMetrics surperf_sentinel_findings_totaletperf_sentinel_io_waste_ratioen sont la source la plus directe.
Option : élargir la fenêtre live. Si les post-mortems dans le TTL sont fréquents, on échange de la RAM contre du contexte :
[daemon]
max_active_traces = 50000 # plafond dur à 1_000_000
trace_ttl_ms = 300000 # 5 minutes au lieu de 30 secondes
max_retained_findings = 50000Daemon en cours mais inaccessible depuis les clients
Symptôme. Le processus du daemon tourne (container up, unité systemd active, les logs indiquent Starting daemon: gRPC=...:4317, HTTP=...:4318) mais curl http://<host>:4318/health depuis l'extérieur du processus timeout ou est refusé (connection refused).
Premiers contrôles.
# Depuis l'intérieur du container / pod / host qui fait tourner le daemon
# (ça doit toujours marcher) :
curl -sf http://localhost:4318/health
# Depuis l'endroit où vous voulez réellement l'atteindre (c'est celui qui échoue) :
curl -v http://<host>:4318/health
# L'adresse de bind est loguée explicitement au démarrage :
docker logs perf-sentinel 2>&1 | grep 'Starting daemon'
# Attendu : gRPC=0.0.0.0:4317 pour être joignable de l'extérieur. Tout ce
# qui contient 127.0.0.1 est loopback-only et refusera les connexions
# venant d'en dehors du processus.Causes probables.
- Daemon bindé sur
127.0.0.1(défaut). Le listener bind sur l'interface loopback pour des raisons de sécurité. À l'intérieur d'un container, la loopback n'est joignable que depuis le même container : undocker run -p 4318:4318publie un port au niveau host mais le listener dans le container n'accepte pas la connexion forwardée. Même pattern sur une VM accédée via SSH port-forward ou sur un pod Kubernetes derrière un Service ClusterIP. --network hostcombiné à des flags-p. En mode host network, le container partage le namespace réseau de l'hôte ; les-psont ignorés et Docker émetWARNING: Published ports are discarded when using host network mode. Le daemon n'est joignable que sur l'IP sur laquelle sa config le bind.- Mapping de port inversé ou incomplet.
docker ps --format '{{.Ports}}'montre le mapping effectif. Pattern attendu sur un run local de dev :0.0.0.0:4317-4318->4317-4318/tcp. - Firewall host, NetworkPolicy ou Security Group cloud qui rejette le trafic. Le
curldepuis l'intérieur du namespace réseau réussit mais celui de l'extérieur timeout. Si la bind address est0.0.0.0et que les logs du daemon n'indiquent pas d'erreur, le delta est environnemental.
Correctif.
- Cause (1) : lancer avec
watch --listen-address 0.0.0.0, ou fixer[daemon] listen_address = "0.0.0.0"dans.perf-sentinel.toml. Le daemon émettra un warning non-loopback au démarrage, c'est attendu ; placez un reverse proxy ou une NetworkPolicy en amont si c'est un environnement partagé. Voir le quickstart Docker dans README et les topologies sidecar/collector dans Intégration. - Cause (2) : retirer les flags
-pen mode--network host(ils sont ignorés) et s'assurer que le daemon bind sur0.0.0.0. Ou revenir au réseau bridge par défaut +-pexplicites. - Cause (3) : recréer le container avec l'ordre
-p HOST:CONTAINERcorrect. - Cause (4) : comparer
curldepuis l'intérieur (réussit) et depuis l'extérieur (échoue). Si le delta est infra, faire remonter la règle bloquante au owner infra.
Aucune trace ingérée
Symptôme. perf_sentinel_events_processed_total et perf_sentinel_traces_analyzed_total restent à zéro. /api/status renvoie active_traces: 0.
Premiers contrôles.
# Le daemon écoute-t-il sur les ports attendus ?
kubectl logs deploy/perf-sentinel | grep -i "listening on"
# Attendu : "OTLP gRPC listening on 0.0.0.0:4317"
# "OTLP HTTP listening on 0.0.0.0:4318"
# Depuis un container de service, peut-on joindre le daemon ?
curl -sf http://perf-sentinel:4318/metricsCauses probables, par ordre.
- Adresse de bind. Le daemon écoute par défaut sur
127.0.0.1, injoignable depuis d'autres containers. Mettezlisten_address = "0.0.0.0"dans.perf-sentinel.tomlet redémarrez. - Protocole mal aligné. L'OTel Java Agent utilise gRPC par défaut sur le port 4317. Vérifiez que
OTEL_EXPORTER_OTLP_PROTOCOLcorrespond au port visé :grpc→ 4317,http/protobuf→ 4318. - Politique réseau. Un
NetworkPolicyKubernetes ou un security group peut bloquer le trafic cross-namespace. Désactivez temporairement ou autorisez explicitement le chemin service → daemon. - Service non instrumenté. Vérifiez
OTEL_SDK_DISABLED=falseet que le service produit bien des spans (la plupart des SDKs OTel ont des compteurs internes ou des logs debug). - Faute de frappe sur l'endpoint OTLP.
OTEL_EXPORTER_OTLP_ENDPOINTdoit êtrehttp://<host>:4318. Pas de suffixe/v1/traces, le SDK l'ajoute. - Les spans arrivent mais aucun n'est analysable.
perf_sentinel_otlp_spans_received_totalqui monte pendant queevents_processed_totalreste plat signifie que le daemon reçoit des spans mais que chacun est filtré (pas dedb.statement, pas dehttp.url). Examinezperf_sentinel_otlp_spans_filtered_totalparreason: unmissing_db_statementdominant pointe vers des drivers configurés pour omettre le texte des requêtes (voir Limites et les réglages par langage dans Instrumentation).
Vérification après correctif. Déclenchez une requête via un service instrumenté et observez :
watch -n 1 'curl -s http://perf-sentinel:4318/metrics | grep events_processed_total'Le compteur doit incrémenter en quelques secondes.
Chute soudaine du volume d'ingestion
Symptôme. rate(perf_sentinel_events_processed_total[5m]) tombe brutalement ou à zéro alors que le daemon est toujours vivant (uptime continue d'augmenter).
Premiers contrôles.
# Confirmer que le daemon est encore vivant, élimine le crash
curl -s http://perf-sentinel:4318/api/status | jq '{uptime_seconds, active_traces}'Causes probables.
- Trafic amont effondré. Le trafic réel vers vos services a chuté ; perf-sentinel reflète fidèlement la réalité. Recoupez avec les métriques de votre load balancer ou HTTP.
- OTel collector down. Si un collector central est entre les services et perf-sentinel, vérifiez d'abord sa santé et ses métriques de réception.
- Changement de sampling. Un bump de config a baissé le taux de sampling. Auditez les commits récents dans le repo de config OTel.
- Backpressure du daemon. Deux points de pression distincts. Si l'ingestion dépasse la boucle de réception, le canal OTLP se remplit et les events sont rejetés : cherchez
channel full(RUST_LOG=sentinel_core::ingest=debug) etperf_sentinel_otlp_rejected_total{reason="channel_full"}. Si la détection ne suit pas, la file du worker d'analyse se remplit et des lots entiers sont délestés : surveillezperf_sentinel_analysis_queue_depthetperf_sentinel_analysis_shed_batches_total. Déclencheurs fréquents : une trace pathologique qui ralentit detect+score, oumax_active_tracestrop bas pour le débit courant.
Traitez de haut en bas par élimination. Les cas 1 et 2 représentent la grande majorité.
Spike de findings critiques
Symptôme. Alerte sur le rate de perf_sentinel_findings_total{severity="critical"}.
Workflow de triage.
- Grouper par service et type.
``bash curl -s 'http://perf-sentinel:4318/api/findings?severity=critical&limit=200' \ | jq '[.[].finding | {finding_type, service}] | group_by(.service, .finding_type) | map({key: "\(.[0].service)/\(.[0].finding_type)", count: length}) | sort_by(-.count)' ``
- Récupérer un
trace_idd'exemplar pour chaque top pattern. Dans Grafana, le ◆ sur la métrique est cliquable ; en ligne de commande :
``bash curl -s http://perf-sentinel:4318/metrics \ | grep -E 'findings_total|io_waste_ratio' # Les lignes se terminent par "# {trace_id=\"...\"}", copiez cet id ``
- Expliquer la trace tant qu'elle est dans la fenêtre live de 30 secondes :
``bash curl -s http://perf-sentinel:4318/api/explain/<trace_id> | jq . ``
Si évincée, basculez sur le workflow post-mortem.
- Corréler entre services si l'incident traverse plusieurs équipes :
``bash curl -s http://perf-sentinel:4318/api/correlations | jq 'sort_by(-.confidence)[:10]' ``
Causes racines courantes.
- N+1 SQL : lazy loading de l'ORM ; une feature récente qui itère sur une collection sans
JOIN FETCH/selectinload/Include. - Saturation de pool : pool de connexions sous-dimensionné, ou une dépendance aval qui a ralenti.
- Requête lente : index manquant ; un seuil de volume de données franchi (ce qui tournait en 50 ms à 10 k lignes tourne en 2 s à 10 M).
Référence de dimensionnement (mesurée)
Mesuré sur 0.8.7 avec la rampe de saturation du simulation lab (limit-saturation-curve : 64 services, mix d'anti-patterns réaliste, ~9 spans par trace, OTLP HTTP) contre la config par défaut sur un pod 500m CPU / 256Mi :
| Charge offerte | Débit soutenu | RSS max | Comportement |
|---|---|---|---|
| jusqu'à ~400 traces/s (~2 600 événements/s) | linéaire, sans perte | < 100 MiB | propre |
| 800-1600 traces/s offerts | plateau ~2 500 événements/s | < 150 MiB | émetteurs mis en backpressure via la concurrence bornée des requêtes OTLP, zéro shed, zéro restart |
Leviers de montée en charge, dans l'ordre : relever la limite CPU (le plateau est borné CPU sur le décodage protobuf plus la détection), puis [daemon] ingest_queue_capacity / analysis_queue_capacity pour absorber les rafales. Sous saturation soutenue, le cgroup entier est throttlé par quanta, donnez donc de la marge à la sonde liveness (timeoutSeconds: 5, failureThreshold: 5) : un budget de sonde trop serré redémarre un daemon fonctionnel en backpressure. Les topologies larges (centaines de valeurs service.name) sont bornées par le contrôle d'admission des paires du corrélateur et le plafond de 1024 services de la métrologie, observables via perf_sentinel_correlator_pairs_evicted_total et perf_sentinel_service_io_ops_overflow_total.
Pression mémoire ou OOM du daemon
Symptôme. Le RSS grimpe avec le temps ; OOMKill Kubernetes ; active_traces ou stored_findings proche des plafonds configurés.
Premiers contrôles.
curl -s http://perf-sentinel:4318/api/status | jq '{active_traces, stored_findings, uptime_seconds}'
# Comparez avec max_active_traces (défaut 10000) et max_retained_findings (défaut 10000) de la config.Causes probables.
- Trafic au-dessus des valeurs par défaut. 10 000 traces actives est dimensionné pour une charge modérée. Les services à fort débit remplissent plus vite que l'éviction ne purge.
- TTL élargi. Si vous avez augmenté
trace_ttl_mspour la commodité post-mortem, chaque trace vit plus longtemps en mémoire. - Traces pathologiques. Une seule trace avec des milliers de spans consomme de la RAM.
max_events_per_trace(défaut 1000) plafonne, vérifiez qu'il n'a pas été augmenté. Le texte SQL surdimensionné venant d'un émetteur hostile ou verbeux est aussi borné par champ à l'ingestion (64 KiB par cible), mais 1000 événements de ce type dans une trace s'additionnent : baissezmax_events_per_traceoumax_active_tracessi un émetteur déviant est suspecté. - Croissance du correlator.
[daemon.correlation] max_tracked_pairs(défaut 10 000) borne le graphe cross-trace. Le relever multiplie la mémoire par le nombre de paires.perf_sentinel_correlator_pairs_evicted_totalest le signal que le plafond agit : un taux soutenu signifie que la topologie dépasse le plafond (les corrélations sont recyclées, pas fuitées). - Findings store gonflé par une boucle de détection emballée. Rare mais à vérifier via
stored_findingsvsmax_retained_findings.
Correctif.
[daemon]
max_active_traces = 5000 # fenêtre plus petite
trace_ttl_ms = 30000 # retour au défaut
api_enabled = false # désactive l'API de requêtage si non utilisée
max_retained_findings = 0 # court-circuite le ring buffer des findings
[daemon.correlation]
enabled = false # skip le correlator pour les daemons mono-serviceMettre max_retained_findings = 0 est le levier le plus efficace pour libérer la RAM quand l'API de requêtage n'est pas consommée. Voir Limites § "La mémoire n'est pas libérée par api_enabled = false seul".
Redémarrez le daemon pour appliquer. Pas de hot reload, voir Appliquer un changement de config.
Quality gate CI en échec inattendu
Symptôme. perf-sentinel analyze --ci ou perf-sentinel tempo --ci sort avec le code 1. Build rouge.
Premiers contrôles.
La sortie JSON contient un bloc quality_gate structuré :
perf-sentinel analyze --ci --input traces.json --format json \
| jq '.quality_gate.rules[] | select(.passed == false)'Exemple de sortie :
{ "rule": "n_plus_one_sql_critical_max", "threshold": 0, "actual": 2, "passed": false }Causes probables.
- Régression légitime. Un changement récent a introduit de nouveaux N+1 ou fait grimper le waste ratio. Inspectez
findings[]dans le même JSON :source_endpointlocalise le chemin code ;pattern.templatemontre le SQL/HTTP normalisé ;pattern.occurrencesdonne l'ampleur. - Seuil trop strict.
.perf-sentinel.tomlpeut avoir des tolérances à zéro qui échouent dès qu'un finding préexistant est là. Pour les projets legacy, envisagez un baseline à cliquet (resserrer progressivement plutôt qu'en une fois). - Données de test qui ont grandi. Un dataset plus large dans les tests d'intégration peut franchir un seuil de détection (un N+1 à 5 occurrences ne se déclenche qu'au-delà d'un certain nombre d'itérations).
Correctif. Ajustez soit le code, soit le seuil, pas les deux sous pression. Si le finding est réel, corrigez le code. Si le seuil est mal calibré, mettez à jour .perf-sentinel.toml et committez le changement pour qu'il soit relu.
Note. Il n'existe pas de seuils de détection par service à ce jour ; les valeurs [detection] s'appliquent globalement à tous les services du fichier de traces.
Investigation d'un acknowledgment inattendu
Symptôme. Un finding que vous attendiez en CI est absent, ou une quality gate qui aurait dû échouer passe. Vous suspectez une entry de .perf-sentinel-acknowledgments.toml.
Premiers checks.
# 1. Lancez avec --no-acknowledgments pour comparer. Si le finding
# apparaît ici mais pas dans le run normal, un ack le matche.
perf-sentinel analyze --no-acknowledgments --input traces.json --format json \
| jq '.findings[] | select(.signature == "<signature suspecte>")'
# 2. Lancez avec --show-acknowledged pour voir quel ack a matché et
# pourquoi.
perf-sentinel analyze --show-acknowledged --input traces.json --format json \
| jq '.acknowledged_findings[] | select(.finding.signature == "<signature suspecte>")'
# 3. Inspectez le fichier d'acks directement.
git log -p .perf-sentinel-acknowledgments.toml | head -80Causes probables.
- L'ack matche comme prévu. La signature est dans le fichier. Lisez le
reasonet la PR qui l'a apporté. Si la rationale ne tient plus, ouvrez une PR pour retirer l'entry. - Mauvaise normalisation de template. La signature dans le fichier ne correspond plus au template courant. Fréquent après un refacto SQL (changement d'ordre de paramètres, renommages d'alias). Réextrayez la signature actuelle via la sortie JSON et mettez à jour l'entry.
expires_atpérimé. Un ack avecexpires_at = "2025-12-31"a cessé de s'appliquer le 2026-01-01. Le finding qui réapparaît est celui qui était supprimé. Décision : rafraîchir l'ack avec une nouvelle date, le rendre permanent, ou corriger le code sous-jacent.- Path d'override en fuite. Un job CI passe
--acknowledgments /un/autre/chemin.tomlque vous n'attendiez pas. Greppez les workflows CI pour le flag.
Correctif. Le fichier d'acks est versionné, donc le correctif est toujours une PR : éditer, retirer ou mettre à jour l'entry. Ne contournez jamais les acks en CI en ajoutant --no-acknowledgments à un job permanent, la trace d'audit est le git log du fichier.
Pour le workflow d'ack complet, voir Acquittements.
perf-sentinel tempo renvoie 404 ou timeout
Symptôme. Soit chaque invocation échoue avec Tempo returned HTTP 404 for https://.../api/search?..., soit l'étape search réussit mais la boucle de fetch par trace finit avec Tempo fetch completed with failures counts={"timeout": N} et renvoie un résultat partiel (ou vide).
Premiers contrôles.
# Vérifier que l'endpoint est bien une query-frontend Tempo, pas Grafana
# ou un composant interne de Tempo. 200 = OK, 404 = mauvais endpoint.
curl -s -o /dev/null -w 'HTTP %{http_code}\n' \
'<votre-endpoint>/api/search?limit=1'
# Côté Tempo, surveiller la charge de la query-frontend
kubectl logs -n observability deploy/tempo-query-frontend --tail=50 \
| grep -E 'error|timeout|queue'Causes probables.
- Mauvais composant en déploiement microservices. Dans les déploiements Helm
tempo-distributed, l'API HTTP de requête est servie exclusivement partempo-query-frontend. Pointer--endpointsurtempo-querier(worker interne, pas d'API publique) outempo-ingester(chemin d'écriture uniquement) renvoie 404 sur chaque/api/search. Le message 404 émis par perf-sentinel inclut désormais l'URL qui a échoué pour rendre la mauvaise configuration visible d'un coup d'œil. - Endpoint qui pointe sur Grafana au lieu de Tempo. Grafana écoute sur 3000 par défaut, l'API HTTP de Tempo sur 3200.
http://grafana:3000/api/searchn'a pas de route correspondante et retourne 404. - Préfixe de reverse proxy oublié. Si Tempo est derrière un ingress avec un préfixe de path (ex.
https://observability.example.com/tempo/...),--endpointdoit inclure ce préfixe. - Tempo dégradé sous charge de fetch. Le search a réussi mais les fetches par trace timeout. Déclencheurs courants :
--lookbacklong (24 h sur un gros service),tempo-query-frontendsous-provisionnée, plafondmax_concurrent_queriesatteint, limites de ressources sur les ingesters (un ingester OOM-killed provoque des échecs de fetch en cascade).
Correctif.
- Causes (1), (2), (3) : pointer
--endpointsur la vraie URL de query-frontend, validée par lecurlci-dessus. - Cause (4) : côté perf-sentinel, réduire
--lookback(commencer à 1 h, élargir progressivement) ou basculer sur--trace-id <id>pour un replay trace unique. Côté Tempo, scalertempo-query-frontendhorizontalement, remontermax_concurrent_queries, et vérifier les caps mémoire/CPU des ingesters.
Perf-sentinel plafonne les fetches in-flight à 16 en parallèle par défaut : le client n'inonde pas lui-même Tempo. Si Tempo s'effondre quand même sur un run de 100 traces, c'est la capacité qui bouche, pas le client. Ctrl-C pendant un run long retourne désormais un résultat partiel avec les traces déjà complétées (voir Limites § "Ingestion Tempo") ; la CLI renvoie Tempo fetch was interrupted by Ctrl-C before any trace completed quand aucune trace n'a eu le temps de se compléter, distinct du NoTracesFound générique.
Exemplars absents dans Grafana
Symptôme. Les panels affichent les valeurs de métriques mais le marqueur ◆ d'exemplar est absent, ou cliquer dessus ne saute pas vers Tempo.
Premiers contrôles.
# Métriques brutes : chercher "# {trace_id=\"...\"}" en fin de ligne
curl -s http://perf-sentinel:4318/metrics \
| grep -E 'findings_total|io_waste_ratio'Si les annotations sont présentes dans la sortie brute mais que Grafana ne les rend pas, c'est un problème de config côté Grafana ou Prometheus. Si absentes, perf-sentinel n'a encore enregistré aucun exemplar.
Causes probables.
- Aucun finding encore. Les exemplars ne sont posés qu'à la détection. Un daemon à zéro finding n'en a aucun. Déclenchez du trafic sur un chemin qui produit un N+1 ou une requête lente.
- Stockage d'exemplars Prometheus non activé. Prometheus doit être lancé avec
--enable-feature=exemplar-storage. Vérifiez sur la page des flags Prometheus. - Datasource Grafana pas liée à Tempo. Dans Grafana → Connections → datasource Prometheus → Exemplars, configurez un exemplar avec
datasourceUidpointant vers votre datasource Tempo etlabelName: trace_id. trace_idépuré. perf-sentinel filtre les valeurs d'exemplar à[a-zA-Z0-9_-]et tronque à 64 caractères. Des formats de trace ID inhabituels (UUIDs avec accolades, encodages custom) peuvent être déformés. Voirsanitize_exemplar_valuedansreport/metrics.rs.
Scraper d'énergie bloqué
Symptôme. perf_sentinel_scaphandre_last_scrape_age_seconds ou perf_sentinel_cloud_energy_last_scrape_age_seconds grimpe de façon monotone au-delà de l'intervalle de scrape configuré. Les scrapers sains remettent cette gauge près de zéro après chaque scrape réussi.
Premiers contrôles.
curl -s http://perf-sentinel:4318/metrics | grep scrape_age_secondsActiver les logs de scoring pour voir l'échec réel :
RUST_LOG=sentinel_core::score=debug
# Chercher "scaphandre scrape failed" ou "cloud_energy scrape failed"Causes probables.
- Permissions du container Scaphandre. Les compteurs RAPL nécessitent
CAP_SYS_RAWIO, le mode privileged, ou un hostPath vers/sys/class/powercap. Sans ça, les scrapes échouent au niveau des privilèges. - Endpoint injoignable. Vérifiez l'URL dans
[green.scaphandre] endpoint. Le réseau entre perf-sentinel et l'exporteur Scaphandre doit être ouvert. - API d'énergie cloud down ou rate-limitée. Si vous utilisez Electricity Maps ou une API de cloud provider, vérifiez son statut et votre quota API.
- Nom de service qui ne correspond pas. Les clés
[green.cloud.services.<name>]doivent matcher l'attributservice.namedes spans entrants. Sans correspondance, pas d'attribution par service.
Impact. Le daemon retombe sur le modèle proxy I/O pour les estimations d'énergie. Les chiffres CO₂ restent directionnels mais perdent leur précision de mesure. Ce n'est pas un incident chaud ; à corriger lors de la prochaine fenêtre de maintenance sauf si la précision compte pour un rapport spécifique.
/api/correlations renvoie vide
Symptôme. Les panels de corrélations cross-trace sont vides alors même que plusieurs services produisent des findings.
Premiers contrôles.
curl -s http://perf-sentinel:4318/api/correlations | jq 'length'
# 0 = aucune corrélation n'a passé les seuilsCauses probables.
- Correlator désactivé. Le défaut est
[daemon.correlation] enabled = false. Activez-le. - Seuils trop stricts. Défauts :
min_co_occurrences = 5: il faut 5 incidents conjoints avant qu'une paire soit considéréemin_confidence = 0.7: 70 % de confiance sur la corrélationlag_threshold_ms = 5000: fenêtre de 5 secondes entre cause et effet
Des pics de trafic courts accumulent rarement 5 co-occurrences. Baissez pour dev/staging, gardez conservateur en prod.
- Services légitimement indépendants. Des services découplés sains ne produisent aucune corrélation. L'absence n'est pas toujours un bug.
Correctif.
[daemon.correlation]
enabled = true
min_co_occurrences = 3
min_confidence = 0.6
lag_threshold_ms = 10000
max_tracked_pairs = 20000Redémarrez le daemon pour appliquer.
/api/export/report retourne 503 ou un rapport vide
Symptôme. Piper le daemon vers le dashboard HTML échoue avec HTTP 503, ou produit un dashboard à zéro findings sur un daemon qui tourne manifestement.
curl -s http://perf-sentinel:4318/api/export/report | perf-sentinel report --input - --output /tmp/report.html
# HTTP 503: {"error": "daemon has not yet processed any events"}Causes probables.
- Cold start. L'endpoint retourne 503 tant que
events_processed > 0n'est pas vrai, volontairement : rendre un dashboard avec des compteurs à zéro sur un daemon qui n'a pas encore vu son premier batch OTLP serait trompeur. Attends le premier batch, puis réessaie.GET /api/statusexpose le compteurevents_processedlive. api_enabled = false. Si la config désactive la query API,/api/export/reportn'est pas monté etcurlretourne un 404, pas un 503. Réactive[daemon] api_enabled = true.- Findings store vide, pas cold start. Sur un daemon long-running qui a traité des events mais qui n'a aucun finding dans le ring buffer (trafic clean, ou
max_retained_findings = 0), l'endpoint retourne 200 avec un tableaufindingsvide. Le dashboard résultant affiche un état "No findings", ce qui est correct.
Note opérationnelle. Le snapshot n'est pas atomique entre findings et correlations : les deux collections peuvent être décalées d'un batch (findings de la génération N, correlations de N+1). Pour un dashboard post-mortem c'est acceptable. Si tu as besoin d'une cohérence stricte, utilise analyze --input traces.json sur un fichier de traces capturé à la place.
Crash ou redémarrage du daemon
Symptôme. Le processus du daemon s'est arrêté (OOM kernel, panic, éviction du pod, rollout de déploiement).
Ce qui est perdu.
- Toutes les traces de la fenêtre glissante (jusqu'à
max_active_traces). - Tous les findings retenus (jusqu'à
max_retained_findings). - L'état de corrélation cross-trace.
- Le compteur d'uptime est réinitialisé.
Ce qui survit.
- Rien du daemon lui-même. Pas de persistance disque.
- Prometheus conserve les métriques déjà scrapées (les compteurs historiques sont saufs).
- Tempo conserve les traces, à condition que vous les y envoyiez aussi.
Récupération.
- Démarrer un nouveau daemon avec la même config.
- Attendre que les collectors / SDKs OTel se reconnectent. Les clients OTel retry avec backoff exponentiel. Comptez jusqu'à ~60 secondes avant que l'ingestion ne reprenne pleinement.
- Pour les incidents survenus pendant l'interruption, utilisez le workflow post-mortem contre Tempo.
Prévention.
- Kubernetes
restartPolicy: Always+ marge de limit mémoire au-dessus du RSS pic observé. - Alertez sur
perf_sentinel_active_tracesapprochantmax_active_traces. La pression montante précède souvent l'OOM. - Pour la HA, lancez plusieurs replicas derrière un load balancer. Chaque replica a un état indépendant (pas de corrélation cross-replica), mais l'ingestion devient redondante face à une panne single-instance.
Appliquer un changement de config
Le daemon ne recharge pas .perf-sentinel.toml à chaud. Toute édition de config nécessite un redémarrage :
# Kubernetes
kubectl rollout restart deployment/perf-sentinel
# systemd
systemctl restart perf-sentinel
# Docker
docker restart perf-sentinelComptez une brève interruption de l'ingestion (quelques secondes à une minute) pilotée par le comportement de retry des SDKs OTel. Pour les tuning non urgents, profitez d'une fenêtre de déploiement normale.
Valider avant le rollout. Le daemon parse le TOML au démarrage et quitte avec une erreur claire sur entrée malformée. Smoke-testez la config candidate dans un daemon jetable d'abord :
perf-sentinel watch --config /path/to/candidate-config.toml
# Sort immédiatement sur erreur de parsing en imprimant la ligne fautive.Une fois qu'il démarre proprement, déployez en production.
Inspecter les endpoints HTTP du daemon
L'image du daemon est distroless et n'inclut ni curl, ni wget, ni shell. kubectl exec ... -- curl http://localhost:14318/... échoue avec executable file not found. Utilisez kubectl port-forward plus votre curl local pour l'inspection HTTP ad-hoc :
# Dans un terminal : forward le port HTTP du daemon en local.
kubectl port-forward -n <namespace> deploy/perf-sentinel-daemon 14318:14318
# Dans un autre terminal : inspectez les endpoints depuis l'hôte.
curl -sH "Accept: application/openmetrics-text;version=1.0.0" \
http://localhost:14318/metrics | tail -5
curl -s http://localhost:14318/api/status | jq
curl -s http://localhost:14318/api/export/report | jq '.warnings, .green_summary'La liveness probe kubelet utilise un check TCP sur le port HTTP, pas un appel curl, donc l'image distroless n'affecte ni la liveness ni la readiness.
L'endpoint /metrics négocie le content type depuis le header Accept du client. Envoyer application/openmetrics-text force OpenMetrics 1.0 avec le terminateur # EOF et les annotations exemplars. Un Accept absent ou */* (curl par défaut, vmagent par défaut) retombe sur le comportement legacy 0.5.15 (OpenMetrics quand des exemplars sont présents, plain Prometheus sinon). Un Accept: text/plain strict (sans */*) force plain Prometheus 0.0.4 sans exemplars, protégeant les scrapers pré-OpenMetrics.
Diagnostiquer les drops OTLP
Symptôme : les spans envoyés par les clients ne sont pas visibles dans /api/export/report ni dans le dashboard, et le SDK client ne signale aucune erreur.
- Récupérer
perf_sentinel_otlp_rejected_totaldepuis/metricset lire la valeur parreason:
``bash curl -s http://daemon:4318/metrics | grep '^perf_sentinel_otlp_rejected_total' ``
channel_fullélevé : le daemon est CPU-bound ou en backpressure. Vérifierprocess_cpu_seconds_total(rate) etprocess_resident_memory_bytescontre les limites du pod. Augmenter les limites CPU ou mémoire, ou scaler horizontalement.parse_errorélevé : les clients envoient de l'OTLP malformé. Vérifier la version du SDK client et la compatibilité protobuf contre la spec OTLP.unsupported_media_typeélevé : les clients utilisent la variante OTLP encodée en JSON ou un mauvaisContent-Type. perf-sentinel n'accepte queapplication/x-protobuf.Report.warning_detailssurface une entréeingestion_dropsdès que le compteurchannel_fullest positif. Un consumer qui lit/api/export/reportsans scraper Prometheus voit quand même le signal :
``bash curl -s http://daemon:4318/api/export/report | jq '.warning_details' ``
- Les réponses 413 (HTTP) et
RESOURCE_EXHAUSTED(gRPC) pour les payloads trop gros sont interceptées en amont par tower-http (RequestBodyLimitLayer) et tonic (max_decoding_message_size) avant que le handler applicatif ne tourne. Elles ne sont pas comptabilisées parperf_sentinel_otlp_rejected_total. Vérifier les access logs du proxy ou de la gateway.
Voir Métriques pour le catalogue complet des reasons et le reste de la surface metrics.
Lire les warnings du Report
Report.warning_details (depuis 0.5.19) est un vecteur d'entrées {kind, message} exposées par le daemon dans le payload report côté opérateur. Chaque entrée a un kind stable (utile pour l'alerting et l'agrégation cross-run) et un message lisible qui peut inclure des valeurs dynamiques telles que des compteurs.
Kinds et cycle de vie
cold_start(transitoire) : le daemon n'a pas encore traité d'événements, renvoyé parGET /api/export/reportjusqu'au premier batch. Pré-0.5.16 ce signal était un statut 503, qui faisait échouer les probes Kubernetes. Attendre le premier tick d'éviction (par défaut 15s, moitié detrace_ttl_ms). Si le warning persiste au-delà de 60-120 secondes en environnement déployé, vérifier que l'application émet réellement des traces OTLP et que l'adresse de listen est accessible. Le warning disparaît automatiquement au premier batch, aucune action opérateur n'est requise.ingestion_drops(collant) : au moins une requête OTLP a été rejetée depuis le démarrage à cause de la saturation du canal. Le message reporte le count. Cross-checker avecperf_sentinel_otlp_rejected_total{reason="channel_full"}pour la même valeur, puis envisager d'augmenter l'allocation CPU du daemon ou[daemon] max_active_traces. Le warning persiste jusqu'au redémarrage du daemon même après que la backpressure soit retombée, le compteur sous-jacent est cumulatif depuis le démarrage du process.tuning(mixte, depuis 0.8.7) : le conseiller de réglages du daemon. Chaque entrée nomme un réglage de config dont la valeur actuelle paraît sous-dimensionnée pour la charge observée, avec l'ajustement suggéré dans le message (par exemple "raise `[daemon] analysis_queue_capacity` (currently 1024) or give the daemon more CPU"). Les hints pilotés par compteurs (sheds de la file d'analyse, rejets d'ingestion, débordement du plafond de services, évictions de paires de corrélation, rétention nulle de spans analysables) sont collants commeingestion_drops. Le hint de fenêtre de traces lit la gaugeactive_tracesen direct contre[daemon] max_active_traces, il s'efface donc de lui-même quand la charge retombe. La table complète des règles est dans Métriques. Appliquer la suggestion, redémarrer le daemon, puis confirmer que le hint ne revient pas sous la même charge.
Le champ legacy Report.warnings: Vec<String> (0.5.16+) reste émis pour la backward compat. Les renderers CLI et HTML préfèrent warning_details quand non vide, fallback sur warnings sinon. Le dashboard HTML expose warning_details dans le payload JSON embarqué (payload.report.warning_details), un banner dédié dans l'UI dashboard est dans la roadmap.
Quand on acquitte des findings via l'API ack du daemon (depuis 0.5.20), aucun kind de warning n'est affecté par les acks, ils reflètent l'état du daemon, pas la sortie de détection.
Acquitter des findings au runtime
Depuis 0.5.20 le daemon expose trois endpoints pour muter l'état des acks au runtime, en complément du workflow CI TOML documenté dans Acquittements. À utiliser quand un SRE de garde doit silencer un finding sans attendre un cycle PR sur le repo applicatif.
# Acquitter (différé au prochain trimestre)
SIG="n_plus_one_sql:order-svc:_api_v1_orders:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
curl -fsS -X POST "http://127.0.0.1:4318/api/findings/${SIG}/ack" \
-H "Content-Type: application/json" \
-H "X-User-Id: alice@example.com" \
-d '{"reason":"différé sur TICKET-1234","expires_at":"2026-08-01T00:00:00Z"}'
# Lister les acks runtime actifs
curl -fsS "http://127.0.0.1:4318/api/acks" | jq .
# Vérifier que le finding est filtré
curl -fsS "http://127.0.0.1:4318/api/findings" | jq 'length'
# Révoquer
curl -fsS -X DELETE "http://127.0.0.1:4318/api/findings/${SIG}/ack" \
-H "X-User-Id: alice@example.com"Quand le daemon est configuré avec une clé d'API ([daemon.ack] api_key), ajouter -H "X-API-Key: <secret>" aux appels POST et DELETE. GET /api/acks et GET /api/findings restent non authentifiés par design (lectures loopback).
Le store runtime est un JSONL append-only à ~/.local/share/perf-sentinel/acks.jsonl par défaut. Le tailer pour un audit trail temps réel (tail -f). Le fichier est rejoué et compacté à chaque redémarrage du daemon, donc le churn ne s'accumule pas. Les acks TOML CI chargés au startup (.perf-sentinel-acknowledgments.toml par défaut) restent immutables côté API, voir API de query > "Interop TOML et JSONL" pour les règles de résolution de conflit.
Voir aussi
- Métriques : référence exhaustive de toutes les metrics exposées sur
/metrics, dont les nouvelles process metrics et le compteur de rejet OTLP (depuis 0.5.19). - Limites : ce que le daemon ne persiste pas et ne garantit pas.
- API de query : référence
/api/findings,/api/explain,/api/correlations,/api/status. - Intégration : mise en place de bout en bout, quatre topologies supportées, intégration Tempo et Jaeger. Voir Instrumentation pour le câblage OTLP par langage et CI pour les recettes d'intégration CI.
- Configuration : référence complète
[daemon],[detection],[green],[daemon.correlation].