perf sentinelperf sentineldocs
FRENGitHub
Documentation / Runbook

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

À lancer en premier quel que soit le symptôme. Ça donne la photo en 10 secondes de l'état du daemon.

bash
# 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) :

bash
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'énergie

Pour 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).

Ce qui survit à la fenêtre

Trois durées de vie différentes coexistent, et le TTL de 30 s, le plus visible, est le plus court des trois. Aller chercher Tempo alors que l'archive contient déjà la réponse est l'erreur la plus fréquente.

QuoiVit dansExpire quand
Les spans (nécessaires à un explain tree)La TraceWindow en mémoiretrace_ttl_ms s'écoule, 30 s par défaut, ou éviction LRU au-delà de max_active_traces
Les findingsLe ring buffer de findingsLe ring dépasse max_retained_findings (10000). Aucun TTL : sur une flotte calme ils sont encore servis le lendemain, sur une flotte bavarde ils partent en quelques minutes
Les findings, par fenêtre, sur disqueL'archive NDJSON ([daemon.archive])Jamais, jusqu'à rotation ou purge
Les corrélations cross-trace/api/correlations et /api/export/report, en direct seulementImmédiatement. Ni archivées, ni reproductibles hors ligne, voir plus bas

"Est-ce encore là après 3 heures" n'a donc pas de réponse temporelle pour les findings, seulement une réponse en volume. Les deux gauges qui disent à quelle distance du plafond vous êtes sont perf_sentinel_stored_findings et perf_sentinel_max_retained_findings, à lire comme un ratio :

bash
curl -s http://perf-sentinel:4318/metrics \
  | grep -E '^perf_sentinel_(stored|max_retained)_findings'

Si le ratio est proche de 1 en trafic normal, les findings sont évincés en quelques minutes et seule l'archive répondra à un post-mortem. Relever le plafond, c'est la recette "élargir la fenêtre live" plus bas dans cette section, et max_retained_findings est documenté dans Configuration avec sa plage acceptée et son compromis de réglage.

Privilégiez l'archive quand elle est activée. Si [daemon.archive] est configurée, le daemon écrit un Report par fenêtre d'analyse sous forme d'une ligne NDJSON, et ce fichier répond directement à "qu'a-t-on trouvé à 14h05", indépendamment de ce que le daemon garde encore en mémoire. Ce sont les mêmes données que perf-sentinel disclose agrège.

bash
# Findings d'une fenêtre donnée, directement depuis l'archive
jq -c 'select(.ts >= "2026-07-27T14:00:00Z" and .ts < "2026-07-27T15:00:00Z") | .report.findings[]' \
  /var/lib/perf-sentinel/archive/*.ndjson

L'archivage est désactivé par défaut (archive non défini). S'il n'a jamais été configuré, rien n'a été écrit et le rejeu Tempo est la seule voie restante.

Les corrélations cross-trace ne sont pas récupérables après coup. Le Report archivé porte un tableau correlations vide par construction, et l'analyze batch ne produit jamais de corrélations, donc un rejeu Tempo ne les reconstruira pas non plus. Si une corrélation compte pour un incident, elle doit être capturée pendant que le daemon la détient encore. Une fois la fenêtre glissante du corrélateur passée, cette sortie est définitivement perdue.

Capturer les corrélations périodiquement. C'est la fenêtre glissante qui rend un scrape périodique fiable : une corrélation reste active pendant [daemon.correlation] window_ms (600000, soit 10 minutes par défaut), donc interroger l'API à la moitié de cet intervalle ne peut structurellement pas en manquer une.

bash
# CronJob ou sidecar, toutes les 5 minutes
curl -sf http://perf-sentinel:4318/api/correlations \
  | jq -c --arg ts "$(date -u +%FT%TZ)" '{ts: $ts, correlations: .}' \
  >> "$CORRELATIONS_LOG"

Utilisez plutôt GET /api/export/report si vous voulez findings et corrélations dans un même instantané cohérent.

Deux points à connaître avant de s'appuyer dessus. [daemon.correlation] enabled vaut false par défaut, sans quoi il n'y a jamais rien à capturer. Et /api/correlations tronque sa réponse à 1000 entrées alors que le corrélateur en suit jusqu'à max_tracked_pairs (10000) : la troncature garde les paires de plus forte confiance, mais une flotte très corrélée ne sera pas capturée exhaustivement. Surveillez perf_sentinel_correlator_pairs_evicted_total pour savoir si vous touchez aussi le plafond interne.

Archiver nativement les corrélations ne serait pas le changement de quatre lignes que la structure du code laisse croire. Le corrélateur porte un état glissant alors que l'archive écrit une ligne par fenêtre d'analyse : chaque ligne répéterait le même ensemble, soit environ 2 Mo par ligne au plafond de 10000 paires, de quoi remplir les 100 Mo par défaut de max_size_mb en une cinquantaine de lignes, presque uniquement de la duplication. Une implémentation correcte archiverait les corrélations nouvellement qualifiées en delta, pas l'instantané courant.

Un rejeu n'est pas identique à l'observation live. Le champ confidence est estampillé par l'appelant du pipeline : les findings produits par le daemon en production portent daemon_production, alors que les mêmes traces rejouées via tempo ou analyze reviennent en ci_batch ou local_batch. Un consommateur qui pondère la sévérité sur ce champ lira le rejeu comme un signal plus faible pour un trafic identique. Les signatures d'acquittement, elles, ne bougent pas : elles n'incluent pas le trace_id, donc un ack enregistré sur un finding live correspond toujours à son jumeau rejoué.

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.

bash
# 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.json

La 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 :

bash
perf-sentinel explain --input traces-dump.json --trace-id abc123def456
perf-sentinel analyze --input traces-dump.json

Ce qui ne marchera PAS.

TentativePourquoi
curl /api/explain/<trace_id> sur le daemon liveTrace évincée après 30 s
curl /api/findings pour reconstruire un explain treeLe store garde les findings, pas les spans
Attendre que le daemon "refasse remonter" la tracePas d'endpoint de rejeu, les spans ne sont écrits nulle part
Redémarrer le daemon pour retrouver les spansLes spans vivent en mémoire seulement. L'archive NDJSON persiste les findings quand [daemon.archive] est définie, jamais les spans
Rejouer depuis Tempo pour retrouver une corrélationLes corrélations sont propres au daemon et ne sont pas archivées, l'analyse batch n'en produit aucune

Prérequis.

  • Rétention Tempo couvrant la fenêtre de l'incident. block_retention par 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 sur perf_sentinel_findings_total et perf_sentinel_io_waste_ratio en 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 :

toml
[daemon]
max_active_traces     = 50000    # plafond dur à 1_000_000
trace_ttl_ms          = 300000   # 5 minutes au lieu de 30 secondes
max_retained_findings = 50000

Daemon 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.

bash
# 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.

  1. 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 : un docker run -p 4318:4318 publie 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.
  2. --network host combiné à des flags -p. En mode host network, le container partage le namespace réseau de l'hôte ; les -p sont ignorés et Docker émet WARNING: Published ports are discarded when using host network mode. Le daemon n'est joignable que sur l'IP sur laquelle sa config le bind.
  3. 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.
  4. Firewall host, NetworkPolicy ou Security Group cloud qui rejette le trafic. Le curl depuis l'intérieur du namespace réseau réussit mais celui de l'extérieur timeout. Si la bind address est 0.0.0.0 et 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 -p en mode --network host (ils sont ignorés) et s'assurer que le daemon bind sur 0.0.0.0. Ou revenir au réseau bridge par défaut + -p explicites.
  • Cause (3) : recréer le container avec l'ordre -p HOST:CONTAINER correct.
  • Cause (4) : comparer curl depuis 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.

bash
# 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/metrics

Causes probables, par ordre.

  1. Adresse de bind. Le daemon écoute par défaut sur 127.0.0.1, injoignable depuis d'autres containers. Mettez listen_address = "0.0.0.0" dans .perf-sentinel.toml et redémarrez.
  2. Protocole mal aligné. L'OTel Java Agent utilise gRPC par défaut sur le port 4317. Vérifiez que OTEL_EXPORTER_OTLP_PROTOCOL correspond au port visé : grpc → 4317, http/protobuf → 4318.
  3. Politique réseau. Un NetworkPolicy Kubernetes ou un security group peut bloquer le trafic cross-namespace. Désactivez temporairement ou autorisez explicitement le chemin service → daemon.
  4. Service non instrumenté. Vérifiez OTEL_SDK_DISABLED=false et que le service produit bien des spans (la plupart des SDKs OTel ont des compteurs internes ou des logs debug).
  5. Faute de frappe sur l'endpoint OTLP. OTEL_EXPORTER_OTLP_ENDPOINT doit être http://<host>:4318. Pas de suffixe /v1/traces, le SDK l'ajoute.
  6. Les spans arrivent mais aucun n'est analysable. perf_sentinel_otlp_spans_received_total qui monte pendant que events_processed_total reste plat signifie que le daemon reçoit des spans mais que chacun est filtré (pas de db.statement, pas de http.url). Examinez perf_sentinel_otlp_spans_filtered_total par reason : un missing_db_statement dominant 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 :

bash
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.

bash
# 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.

  1. 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.
  2. 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.
  3. Changement de sampling. Un bump de config a baissé le taux de sampling. Auditez les commits récents dans le repo de config OTel.
  4. 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) et perf_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 : surveillez perf_sentinel_analysis_queue_depth et perf_sentinel_analysis_shed_batches_total. Déclencheurs fréquents : une trace pathologique qui ralentit detect+score, ou max_active_traces trop 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.

  1. Grouper par service et type. Depuis 0.10.0 cet endpoint retourne une ligne par problème distinct, pas une par détection : classer sur le nombre de lignes classe donc par variété plutôt que par volume, et un service dont le pic tient à un seul pattern récurrent sur 200 traces finirait dernier. Classez sur seen_count, comme ci-dessus, pour classer par fréquence de déclenchement ; problems dit toujours combien de problèmes distincts porte un service. seen_count compte les détections encore retenues dans le ring buffer, il baisse donc à mesure que les plus anciennes en sortent.
    bash
    curl -s 'http://perf-sentinel:4318/api/findings?severity=critical&limit=200' \
      | jq '[.[] | {finding_type: .finding.finding_type, service: .finding.service, seen: (.seen_count // 1)}]
           | group_by(.service, .finding_type)
           | map({key: "\(.[0].service)/\(.[0].finding_type)",
                  problems: length,
                  detections: (map(.seen) | add)})
           | sort_by(-.detections)'
  2. Récupérer un trace_id d'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
  3. Expliquer la trace tant qu'elle est dans la fenêtre live de 30 secondes : Si évincée, basculez sur le workflow post-mortem.
    bash
    curl -s http://perf-sentinel:4318/api/explain/<trace_id> | jq .
  4. 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 offerteDébit soutenuRSS maxComportement
jusqu'à ~400 traces/s (~2 600 événements/s)linéaire, sans perte< 100 MiBpropre
800-1600 traces/s offertsplateau ~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.

bash
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.

  1. 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.
  2. TTL élargi. Si vous avez augmenté trace_ttl_ms pour la commodité post-mortem, chaque trace vit plus longtemps en mémoire.
  3. 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 : baissez max_events_per_trace ou max_active_traces si un émetteur déviant est suspecté.
  4. 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_total est 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).
  5. Findings store gonflé par une boucle de détection emballée. Rare mais à vérifier via stored_findings vs max_retained_findings.

Correctif.

toml
[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-service

Mettre 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".

Borner la RSS de façon proactive. [daemon] memory_high_water_pct rejette l'ingest avec un statut retryable dès que le ratio de working set du cgroup franchit le seuil, plafonnant la RSS quel que soit le trafic (voir Configuration). Il échantillonne à cadence fixe d'une seconde, donc son efficacité dépend de la marge entre la limite et le seuil : les traces admises entre deux échantillons doivent tenir sous limite - seuil. Sur un pod de 256 Mio à 80 % (une marge d'environ 51 Mio) un flood soutenu peut encore provoquer un OOM avant que le rejet ne prenne effet, tandis qu'une montée graduelle est tenue. Une marge plus large (40 % de 256 Mio, ou 80 % de 512 Mio) tient aussi un flood soutenu. Dimensionnez le seuil pour que limite - seuil dépasse le pic fenêtre-plus-en-vol, ou relevez la limite du conteneur.

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é :

bash
perf-sentinel analyze --ci --input traces.json --format json \
  | jq '.quality_gate.rules[] | select(.passed == false)'

Exemple de sortie :

json
{ "rule": "n_plus_one_sql_critical_max", "threshold": 0, "actual": 2, "passed": false }

Causes probables.

  1. 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_endpoint localise le chemin code ; pattern.template montre le SQL/HTTP normalisé ; pattern.occurrences donne l'ampleur.
  2. Seuil trop strict. .perf-sentinel.toml peut 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).
  3. 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.

bash
# 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 -80

Causes probables.

  1. L'ack matche comme prévu. La signature est dans le fichier. Lisez le reason et la PR qui l'a apporté. Si la rationale ne tient plus, ouvrez une PR pour retirer l'entry.
  2. 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.
  3. expires_at périmé. Un ack avec expires_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.
  4. Path d'override en fuite. Un job CI passe --acknowledgments /un/autre/chemin.toml que 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.

bash
# 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.

  1. Mauvais composant en déploiement microservices. Dans les déploiements Helm tempo-distributed, l'API HTTP de requête est servie exclusivement par tempo-query-frontend. Pointer --endpoint sur tempo-querier (worker interne, pas d'API publique) ou tempo-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.
  2. 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/search n'a pas de route correspondante et retourne 404.
  3. 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/...), --endpoint doit inclure ce préfixe.
  4. Tempo dégradé sous charge de fetch. Le search a réussi mais les fetches par trace timeout. Déclencheurs courants : --lookback long (24 h sur un gros service), tempo-query-frontend sous-provisionnée, plafond max_concurrent_queries atteint, limites de ressources sur les ingesters (un ingester OOM-killed provoque des échecs de fetch en cascade).

Correctif.

  • Causes (1), (2), (3) : pointer --endpoint sur la vraie URL de query-frontend, validée par le curl ci-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, scaler tempo-query-frontend horizontalement, remonter max_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.

bash
# 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.

  1. 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.
  2. Stockage d'exemplars Prometheus non activé. Prometheus doit être lancé avec --enable-feature=exemplar-storage. Vérifiez sur la page des flags Prometheus.
  3. Datasource Grafana pas liée à Tempo. Dans Grafana → Connections → datasource Prometheus → Exemplars, configurez un exemplar avec datasourceUid pointant vers votre datasource Tempo et labelName: trace_id.
  4. 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. Voir sanitize_exemplar_value dans report/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.

bash
curl -s http://perf-sentinel:4318/metrics | grep scrape_age_seconds

Activer les logs de scoring pour voir l'échec réel :

bash
RUST_LOG=sentinel_core::score=debug
# Chercher "scaphandre scrape failed" ou "cloud_energy scrape failed"

Causes probables.

  1. 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.
  2. Endpoint injoignable. Vérifiez l'URL dans [green.scaphandre] endpoint. Le réseau entre perf-sentinel et l'exporteur Scaphandre doit être ouvert.
  3. 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.
  4. Nom de service qui ne correspond pas. Les clés [green.cloud.services.<name>] doivent matcher l'attribut service.name des 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.

bash
curl -s http://perf-sentinel:4318/api/correlations | jq 'length'
# 0 = aucune corrélation n'a passé les seuils

Causes probables.

  1. Correlator désactivé. Le défaut est [daemon.correlation] enabled = false. Activez-le.
  2. Seuils trop stricts. Défauts :
    • min_co_occurrences = 5 : il faut 5 incidents conjoints avant qu'une paire soit considérée
    • min_confidence = 0.7 : 70 % de confiance sur la corrélation
    • lag_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.
  3. 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.

toml
[daemon.correlation]
enabled            = true
min_co_occurrences = 3
min_confidence     = 0.6
lag_threshold_ms   = 10000
max_tracked_pairs  = 20000

Redé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.

bash
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.

  1. Cold start. L'endpoint retourne 503 tant que events_processed > 0 n'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/status expose le compteur events_processed live.
  2. api_enabled = false. Si la config désactive la query API, /api/export/report n'est pas monté et curl retourne un 404, pas un 503. Réactive [daemon] api_enabled = true.
  3. 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 tableau findings vide. 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.

  1. Démarrer un nouveau daemon avec la même config.
  2. 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.
  3. 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_traces approchant max_active_traces. La pression montante précède souvent l'OOM.
  • Pour la HA, ne lancez plusieurs replicas que derrière un routage conscient des traces (hachage cohérent par trace_id dans le loadbalancingexporter du Collector OTel). Un Service en round-robin éclate les spans d'une même trace entre les pods, ce qui dégrade silencieusement la détection N+1. Chaque replica garde un état indépendant et il n'y a pas de corrélation cross-replica, le gain est donc la redondance face à une panne single-instance, pas une analyse partagée.
  • La continuité de collecte compte pour disclose : chaque redémarrage laisse un trou dans les fenêtres archivées qui alimentent la divulgation carbone. Persistez l'archive (mode StatefulSet avec persistence.enabled) pour que les indisponibilités ne percent pas de trous dans l'historique de divulgation. Avec plus d'un replica, chaque pod écrit sa propre archive sur son propre PVC, donc disclose doit être lancé par pod et les sorties réconciliées.

Appliquer un changement de config

Le daemon ne recharge pas .perf-sentinel.toml à chaud. Toute édition de config nécessite un redémarrage :

bash
# Kubernetes
kubectl rollout restart deployment/perf-sentinel

# systemd
systemctl restart perf-sentinel

# Docker
docker restart perf-sentinel

Comptez 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 :

bash
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 construite FROM scratch et ne contient que le binaire, donc ni curl, ni wget, ni shell, ni bundle de certificats CA. kubectl exec ... -- curl http://localhost:4318/... échoue avec executable file not found. Utilisez kubectl port-forward plus votre curl local pour l'inspection HTTP ad-hoc. Le chart nomme le Deployment d'après la release, soit perf-sentinel pour une release de ce nom, et le conteneur écoute sur 4318 :

bash
# Dans un terminal : forward le port HTTP du daemon en local.
kubectl port-forward -n <namespace> deploy/perf-sentinel 14318:4318

# 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'

Les probes de liveness et de readiness du chart sont des httpGet sur /health, que le kubelet exécute lui-même sans aucun binaire dans le conteneur, donc l'image vide n'affecte ni l'une ni l'autre. /health répond indépendamment de [daemon] api_enabled.

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.

  1. Récupérer perf_sentinel_otlp_rejected_total depuis /metrics et lire la valeur par reason :
    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érifier process_cpu_seconds_total (rate) et process_resident_memory_bytes contre 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 mauvais Content-Type. perf-sentinel n'accepte que application/x-protobuf.
  2. Report.warning_details surface une entrée ingestion_drops dès que le compteur channel_full est positif. Un consumer qui lit /api/export/report sans scraper Prometheus voit quand même le signal :
    bash
    curl -s http://daemon:4318/api/export/report | jq '.warning_details'
  3. 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 par perf_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é par GET /api/export/report jusqu'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é de trace_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 ou du garde-fou mémoire (une entrée de warning par cause, chaque message nomme sa cause et son count). Cross-checker avec perf_sentinel_otlp_rejected_total{reason="channel_full"} ou {reason="memory_pressure"} pour la même valeur, puis envisager d'augmenter l'allocation CPU du daemon ou [daemon] max_active_traces pour la saturation, ou la limite mémoire du conteneur pour la pression mémoire. 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 comme ingestion_drops. Le hint de fenêtre de traces lit la gauge active_traces en 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.

bash
# 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].