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 dans tout incident
- Analyser une trace plus ancienne que la fenêtre live : workflow post-mortem
- Ce qui brûlait quand un service est tombé : les findings de la fenêtre d'un incident
- 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 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, adresses d'écoute, arrêt
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 en une seule probe.
Analyser une trace plus ancienne que la fenêtre live
Pourquoi vous en avez besoin. 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, donc 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
Chaque ligne ci-dessous a sa propre durée de vie, et le TTL de 30 s, le plus visible, est parmi les plus courts : seules les corrélations en direct disparaissent plus vite. Aller chercher Tempo alors que l'archive contient déjà la réponse est l'erreur la plus fréquente.
| Quoi | Vit dans | Expire quand |
|---|---|---|
| Les spans (nécessaires à un explain tree) | La TraceWindow en mémoire | trace_ttl_ms s'écoule, 30 s par défaut, ou éviction LRU au-delà de max_active_traces |
| Les findings | Le ring buffer de findings | Le 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 disque | L'archive NDJSON ([daemon.archive]) | Au-delà de max_files (12) fichiers tournés de max_size_mb (100) chacun, le plus ancien est supprimé, environ 1,3 Go aux défauts. Aucun TTL : rien n'expire par l'âge |
Les cumuls quotidiens pour disclose, sur disque (0.28.0) | <stem>-daily-YYYY-MM.ndjson, à côté de l'archive | Le fichier d'un mois est supprimé en entier dès que son dernier jour précède de plus de rollup_retention_days (400) jours le dernier jour clos. max_files n'y touche jamais |
| Les findings, dernier état et jours d'observation, par environnement | Le store SQLite du Hub (Hub 0.3.0 et suivants) | Hub:Retention s'écoule depuis la dernière observation, 180 jours par défaut |
| Les findings, chaque détection | Votre backend de logs, alimenté par le stdout du daemon | La rétention de ce backend |
| Les corrélations cross-trace | /api/correlations et /api/export/report, en direct seulement | Immé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 :
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.
# Findings d'une fenêtre donnée, directement depuis l'archive. Les lignes de
# cumul quotidien (0.28.0+) partagent le répertoire sans .report, `[]?` les saute.
jq -c --arg from 2026-07-27T14:00:00Z --arg to 2026-07-27T15:00:00Z \
'select((.ts | sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601) as $ts
| $ts >= ($from | fromdateiso8601) and $ts < ($to | fromdateiso8601))
| .report.findings[]?' \
/var/lib/perf-sentinel/archive/*.ndjsonComparez des epochs, jamais les chaînes. ts est sérialisé avec autant de décimales qu'il en faut, et "2026-07-27T14:00:00.123Z" trie en dessous de "2026-07-27T14:00:00Z" parce que . précède Z. Une comparaison de chaînes écarte donc les fenêtres de la première seconde de la plage et ramasse celles de la première seconde d'après, décalant la réponse d'au plus une seconde aux deux bouts sans le dire. fromdateiso8601 exige que la fraction soit retirée d'abord, il ne parse que %Y-%m-%dT%H:%M:%SZ.
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.
Un backend de logs garde chaque détection, si vous collectez déjà les logs du daemon. Le daemon écrit chaque finding comme une ligne JSON sur stdout et ses propres logs en texte sur stderr, donc un collecteur qui suit le conteneur garde les findings aussi longtemps que ce backend les retient. [daemon] findings_stdout = false coupe la copie sur stdout, pour un daemon dont les findings arrivent déjà dans PerfSentinelHub. Avec Loki, l'étape json distingue les deux d'elle-même, parce qu'une ligne de texte échoue à l'analyse :
sum by (signature, type, severity, service) (
count_over_time(
{namespace="<ns>", container="perf-sentinel"} | json | __error__="" | signature != "" [$__range]
)
)La requête suit le sélecteur de temps de Grafana et ne demande rien au daemon. Elle a trois coûts. Elle stocke une ligne par détection, donc un N+1 fréquent pèse sur le volume de logs comme il pèse sur le ring. Elle ne porte aucun état d'acquittement, puisqu'une ligne de log ne change plus une fois écrite. Et les limites de requête du backend (max_query_length et max_query_series pour Loki) bornent la profondeur et le nombre de signatures qu'une requête atteint, à vérifier avant de compter sur une longue plage. Le Hub est la voie prise en charge pour l'historique des findings. Cette recette convient à une flotte qui paie déjà la rétention de ses logs.
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. L'exception, depuis 0.30.0, est un daemon avec [daemon.hub_export] actif et un Hub en PerfSentinelHub 0.6.0 ou ultérieur : le daemon pousse les périodes actives de chaque paire sous forme d'épisodes, et le Hub les garde au-delà de la fenêtre.
Capturer les corrélations périodiquement. La fenêtre glissante 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 pas en manquer une.
# 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, et sans l'activer 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. Au plafond de 10000 paires, cela fait environ 2 Mo par ligne, 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'octet près à 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 la vue 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, findings les plus coûteux d'abord
perf-sentinel tempo --endpoint http://tempo:3200 --service order-svc --lookback 2h --sort impact
# 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 prise en charge si votre processus d'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 d'endpoint de rejeu, les spans ne sont écrits nulle part |
| Redémarrer le daemon pour retrouver les spans | Les 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élation | Les 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_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 = 50000Ce qui brûlait quand un service est tombé
Pourquoi vous en avez besoin. Un service a été tué par l'OOM ou a redémarré à 14h03, et la question est ce que Perf Sentinel rapportait déjà à son sujet. perf-sentinel ne détecte pas le crash : il n'a aucun signal mémoire sur un service observé, et un service qui sature continue en général d'émettre des spans, plus lentement. Votre système d'alerte vous donne le moment du crash, et Perf Sentinel vous donne les findings de la fenêtre qui le précède.
Bornez le listing aux deux bouts. Deux bornes, c'est une requête de fenêtre, donc seen_count et first_seen_ms décrivent la fenêtre plutôt que l'historique retenu entier, ce qu'un post-mortem veut.
FROM=$(( $(date -u -d '2026-07-27T13:58:00Z' +%s) * 1000 )) # BSD/macOS : date -j -f
TO=$(( $(date -u -d '2026-07-27T14:03:00Z' +%s) * 1000 ))
curl -sf "http://perf-sentinel:4318/api/findings?service=cart-svc&since_ms=$FROM&until_ms=$TO" \
| jq -c '.[] | {type: .finding.type, severity: .finding.severity, seen: .seen_count,
template: .finding.pattern.template}'Une réponse vide a deux causes possibles. Vérifiez que le ring remonte assez loin avant de conclure que rien ne brûlait :
curl -sf http://perf-sentinel:4318/api/status | jq '.oldest_finding_ms'Si cette valeur est supérieure à la borne basse de votre fenêtre, le ring a déjà évincé la fenêtre et la réponse est "inconnu", pas "propre". La recette d'archive de la section précédente est alors la voie, et si l'archivage n'a jamais été configuré la fenêtre est perdue.
Faire capturer la fenêtre par l'alerte. La fenêtre ci-dessus n'est interrogeable que tant que le ring la détient. Avec [daemon.incidents] configurée, pointez un receiver Alertmanager sur POST /api/incidents et le daemon résout et fige la fenêtre à l'instant où l'alerte part, des heures avant que quiconque n'ouvre un terminal. Des règles et des receivers prêts à l'emploi pour les deux opérateurs Kubernetes se trouvent dans examples/incident-alerts-prometheus-operator.yaml et examples/incident-alerts-victoriametrics-operator.yaml. GET /api/incidents, avec la clé d'écriture ou [daemon] read_api_key en X-API-Key, renvoie ensuite l'incident avec ses findings déjà attachés. perf-sentinel query incidents --service cart-svc --api-key-file <PATH> affiche le même listing depuis un terminal, l'en-tête de chaque incident puis ses findings. Voir API de query. Le ring est en mémoire et est rechargé au démarrage depuis [daemon.incidents] archive_path quand il est posé. Sans lui, collectez cet endpoint si l'enregistrement doit survivre au nœud.
Avec l'export vers le Hub actif et un Hub en PerfSentinelHub 0.5.0 ou suivant, le Hub garde chaque incident dès son enregistrement, donc une fois que perf_sentinel_hub_export_incidents_pending reste proche de 0 l'anneau n'a plus à couvrir l'intervalle d'un poll. Un [daemon.incidents] max_retained plus petit économise alors de la mémoire : un incident peut porter mille findings gelés, donc les 200 par défaut peuvent en retenir 200 000 dans le pire cas. Gardez-le assez grand pour query monitor et pour la passe de consolidation, qui complète un incident environ trois TTL de trace après lui.
Détecter le moment sans alerte externe. Le daemon ne juge pas si un service est vivant, mais il publie quand il en a entendu parler pour la dernière fois. time() - perf_sentinel_service_last_span_timestamp_seconds{service="cart-svc"} > 120 dit que Perf Sentinel n'en a plus reçu de span depuis deux minutes. À la différence de increase(perf_sentinel_service_io_ops_total[10m]) == 0, cela survit à un redémarrage du daemon, où tous les compteurs repartent de zéro et se lisent comme un arrêt de flotte. À lire comme un signal de trafic, pas de vie : un crash, une mise à l'échelle à zéro, un déploiement progressif et un cron silencieux se ressemblent tous, donc toute alerte dessus a besoin d'un for: plus long que l'inactivité normale de ce service.
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 expire 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 lié à
127.0.0.1(défaut). Le listener écoute 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 redirigé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 à laquelle sa config le lie.- 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 container réussit mais celui de l'extérieur expire. Si l'adresse d'écoute 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. Dans un environnement partagé, placez un reverse proxy ou une NetworkPolicy en amont. Voir le quickstart Docker dans README-FR.md 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 écoute 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 du namespace réseau (réussit) et depuis l'extérieur (échoue). Si le delta est infra, faire remonter la règle bloquante au responsable de l'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 2.x utilise
http/protobufpar défaut sur le port 4318, tout comme le starter Spring Boot. Vérifiez queOTEL_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 du service vers le 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. La propriété Spring Bootmanagement.opentelemetry.tracing.export.otlp.endpointfait exception : elle prend l'URL complète,/v1/tracescompris. - 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é, et Perf Sentinel le reflète fidèlement. Recoupez avec les métriques de votre load balancer ou HTTP.
- OTel collector indisponible. 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. Une modification de config a baissé le taux de sampling. Auditez les commits récents dans le repo de config OTel.
- Backpressure du daemon. Trois 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 les avertissements
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. Un troisième point de pression, plus discret, est l'archive de divulgation : quand son écrivain prend du retard sur les I/O disque, des fenêtres entières sont perdues alors même que leurs findings ont été analysés et servis en direct. La perte n'est visible que surperf_sentinel_archive_windows_dropped_total(parreason), jamais dans l'archive elle-même. Déclencheurs fréquents : une trace pathologique qui ralentit detect+score,max_active_tracestrop bas pour le débit courant, ou un stockage lent ou plein sous le chemin d'archive. Avant une divulgation trimestrielle (0.28.0), vérifiez jusqu'où remontent les cumuls quotidiens. Undiscloseofficiel refuse une période que l'archive n'atteint plus, doncoldest_daydoit tomber au plus tard le premier jour du trimestre. Tant qu'aucun jour UTC n'est clos, le champ est absent et la commande affichenull:bashcurl -sf http://perf-sentinel:4318/api/status | jq .disclosure_archive
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 taux de sum(rate(perf_sentinel_findings_total{severity="critical"}[5m])). Depuis 0.18.0 le compteur porte un label service et depuis 0.19.0 un label grouping, donc une alerte non agrégée se déclenche une fois par (service, grouping) : entourez-la de sum() (ou sum by (service), sum by (grouping), si vous voulez une astreinte par service ou par namespace).
Workflow de triage.
- 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.problemsdit toujours combien de problèmes distincts porte un service.seen_countcompte les détections encore retenues dans le ring buffer, il baisse donc à mesure que les plus anciennes en sortent.bashcurl -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)' - Récupérer un
trace_idd'exemplar pour chaque top pattern. Dans Grafana, le ◆ sur la métrique est cliquable. En ligne de commande :bashcurl -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 : Si évincée, basculez sur le workflow post-mortem.bash
curl -s http://perf-sentinel:4318/api/explain/<trace_id> | jq . - 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, ou une fonctionnalité 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, ou 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 bridé 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é. Depuis 0.11.2 il plafonne indépendamment quatre collections par trace : l'anneau d'événements, les contextes d'endpoint entrant retenus, les destinations de consumer retenues et l'index d'ancêtres. La même valeur retient donc une enveloppe plus grande qu'en 0.11.1. 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".
Borner la RSS de façon proactive. [daemon] memory_high_water_pct rejette l'ingest avec un statut réessayable 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 limit - mark. Sur un pod de 256 Mio à 80 % (une marge d'environ 51 Mio) un afflux 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 afflux soutenu. Dimensionnez le seuil pour que limit - mark dépasse le pic de l'empreinte fenêtre-plus-en-vol, ou relevez la limite du conteneur. Un working set qui se stabilise dans la bande de 5 points sous le seuil libère le garde-fou après 60 secondes, ou un trace_ttl_ms s'il est plus long, et le daemon journalise memory working set held at. Les exporteurs réessaient alors ce qu'ils ont mis en file, avec la seule marge limit - mark pour l'absorber, donc dimensionnez aussi cette marge pour cette rafale. Si l'avertissement se répète, le daemon bute sans cesse sur son seuil et refuse des spans à chaque cycle : relevez la limite du conteneur. Un working set qui reste au seuil ou au-dessus garde l'ingest rejeté jusqu'à ce que l'usage reflue ou que le pod redémarre, puisque admettre davantage rapprocherait de l'OOM que le garde-fou évite. PerfSentinelMemoryPressureRejecting se déclenche dans ce cas, relevez donc la limite. Si container_memory_rss reste bien en dessous de container_memory_working_set_bytes, c'est le page cache actif qui maintient le ratio, pas le tas.
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é, etpattern.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 entrée de .perf-sentinel-acknowledgments.toml.
Premiers contrôles.
# 1. Lancez avec --no-acknowledgments pour comparer. Si le finding
# apparaît ici mais pas dans le run normal, un ack lui correspond.
perf-sentinel analyze --no-acknowledgments --input traces.json --format json \
| jq '.findings[] | select(.signature == "<suspect signature>")'
# 2. Lancez avec --show-acknowledged pour voir quel ack a correspondu et
# pourquoi.
perf-sentinel analyze --show-acknowledged --input traces.json --format json \
| jq '.acknowledged_findings[] | select(.finding.signature == "<suspect signature>")'
# 3. Inspectez le fichier d'acks directement.
git log -p .perf-sentinel-acknowledgments.toml | head -80Causes probables.
- L'ack correspond comme prévu. La signature est dans le fichier. Lisez le
reasonet la PR qui l'a apporté. Si la justification ne tient plus, ouvrez une PR pour retirer l'entrée. - 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'entrée.
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é. Choisissez alors entre rafraîchir l'ack avec une nouvelle date, le rendre permanent, ou corriger le code sous-jacent.- Fuite d'un chemin de surcharge. Un job CI passe
--acknowledgments /some/other/path.tomlque vous n'attendiez pas. Cherchez le flag avec grep dans les workflows CI.
Correctif. Le fichier d'acks est versionné, donc le correctif est toujours une PR : éditer, retirer ou mettre à jour l'entrée. 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 de recherche réussit mais la boucle de récupération par trace finit avec Tempo fetch completed with failures counts={"timeout": N} et renvoie un résultat partiel ou, quand les lectures en échec sont plus nombreuses que les traces qui ont répondu, l'erreur the search matched N trace(s) but most fetches failed.
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' \
'<your-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 l'URL qui a échoué, ce qui rend 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 récupération. La recherche a réussi mais les récupérations par trace expirent. Déclencheurs courants :
--lookbacklong (24 h sur un gros service), réplicas detempo-query-frontenden nombre insuffisant, plafondmax_concurrent_queriesatteint, querier avec une petite limite mémoire (OOM-killed sous la charge de récupération), limites de ressources sur les ingesters (un ingester OOM-killed provoque des échecs de récupération 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, baisser
--fetch-concurrency, réduire--lookback(commencer à 1 h, élargir progressivement) ou basculer sur--trace-id <id>pour le rejeu d'une trace unique. Côté Tempo, mettre à l'échelletempo-query-frontendhorizontalement, remontermax_concurrent_queriessi les queriers ont de la mémoire en réserve, et vérifier les plafonds mémoire/CPU des queriers et des ingesters.
Perf Sentinel plafonne les récupérations en cours à 16 par défaut et --fetch-concurrency ou [tempo] fetch_concurrency règle ce plafond de 1 à 64. Le query-frontend découpe chaque récupération en nombreux morceaux pour les queriers, donc le défaut peut épuiser un querier avec une petite limite mémoire, sur un run de 100 traces comme sur un run de 500. Si Tempo s'effondre encore avec --fetch-concurrency 1, c'est la capacité qui bouche, pas le client. Ctrl-C pendant un run long retourne un résultat partiel avec les traces déjà complétées (voir Limites § "Ingestion Tempo"). Quand aucune trace n'a eu le temps de se compléter, la CLI renvoie Tempo fetch was interrupted by Ctrl-C before any trace completed, 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 personnalisés) 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 indisponible ou soumise à une limite de débit. 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 correspondre à l'attributservice.namedes spans entrants. Sans correspondance, pas d'attribution par service.
Impact. Le daemon se rabat 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 un rapport vide
Symptôme. Rediriger la sortie du daemon vers le dashboard HTML produit un dashboard à zéro findings sur un daemon qui tourne manifestement.
curl -s http://perf-sentinel:4318/api/export/report | jq -r '.warning_details[].kind'
# cold_startCauses probables.
- Cold start. Tant que le daemon n'a pas traité un premier batch (
events_processedettraces_analyzedtous deux au-dessus de 0), l'endpoint retourne 200 avec un rapport vide et une entréecold_startdanswarning_details. Attendez le premier batch, puis réessayez./metricsexpose les deux compteurs sousperf_sentinel_events_processed_totaletperf_sentinel_traces_analyzed_total. api_enabled = false. Si la config désactive la query API,/api/export/reportn'est pas monté etcurlretourne un 404. Réactivez[daemon] api_enabled = true.- Findings store vide, pas cold start. Sur un daemon lancé depuis longtemps qui a traité des events mais n'a aucun finding dans le ring buffer (trafic propre, ou
max_retained_findings = 0), l'endpoint retourne 200 avec un tableaufindingsvide et sans avertissementcold_start. 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 vous avez besoin d'une cohérence stricte, utilisez 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é de manière inattendue (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.
- Du daemon lui-même, seulement ce qu'il écrit sur disque : l'archive NDJSON des findings et, depuis 0.28.0, ses cumuls quotidiens quand
[daemon.archive]est défini (un arrêt brutal coûte aux cumuls au plus leurs 20 dernières fenêtres, quedisclosecompte encore depuis les lignes de fenêtre), le stockage runtime des acks (acks.jsonl, rejoué au démarrage) et l'anneau d'incidents quand[daemon.incidents] archive_pathest défini (rechargé au démarrage). Tout le reste vit en mémoire. - 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 réessaient avec un 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 limite mémoire au-dessus du pic de RSS observé. - Alertez sur
perf_sentinel_active_tracesapprochantmax_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_iddans leloadbalancingexporterdu 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 d'instance unique, 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 avecpersistence.enabled) pour que les indisponibilités ne laissent pas de trous dans l'historique de divulgation. Avec plus d'un replica, chaque pod écrit sa propre archive sur son propre PVC, doncdisclosedoit ê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 :
# 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 les nouvelles tentatives des SDKs OTel. Pour les réglages 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. Testez d'abord la config candidate dans un daemon jetable :
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 :
# 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) se rabat 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:bashcurl -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 mettre à l'échelle 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_detailsfait remonter une entréeingestion_dropsdès que le compteurchannel_fullest positif. Un consommateur qui lit/api/export/reportsans scraper Prometheus voit quand même le signal :bashcurl -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) respectivement, 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 valeurs de reason et le reste des métriques.
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 des traces OTLP et que l'adresse d'écoute 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 le nombre de requêtes rejetées). Recouper avecperf_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_tracespour 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 est retombée, car 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 indications pilotées par compteurs (délestages 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 collantes commeingestion_drops. L'indication de fenêtre de traces lit la gaugeactive_tracesen direct contre[daemon] max_active_traces, elle s'efface donc d'elle-même quand la charge retombe. La table complète des règles est dans Métriques, section "Kinds de warning : transitoire vs collant". Appliquer la suggestion, redémarrer le daemon, puis confirmer que l'indication ne revient pas sous la même charge.partial_fetch(par exécution,perf-sentinel temposeulement) : l'exécution a analysé moins de traces que sa recherche n'en a trouvé, parce que des lectures ont échoué, ont répondu 404 ou ont été annulées par Ctrl-C ou SIGTERM. Le message donne les nombres, et les chiffres décrivent les traces qui ont répondu. Baisser--fetch-concurrencysi les échecs viennent d'un querier à court de mémoire. Un 404 seul veut dire que les traces ont quitté la rétention depuis la recherche.
Le champ legacy Report.warnings: Vec<String> (0.5.16+) reste émis pour la rétrocompatibilité. Les renderers CLI et HTML préfèrent warning_details quand non vide et se rabattent sur warnings sinon. Le dashboard HTML expose warning_details dans le payload JSON embarqué (payload.report.warning_details) et l'affiche dans une bannière au-dessus des panneaux, une ligne [kind] message par avertissement.
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 modifier l'état des acks au runtime, en complément du workflow CI TOML documenté dans Acquittements. À utiliser quand un SRE de garde doit faire taire 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":"deferred to 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. Cette clé garde aussi GET /api/acks, qui accepte également [daemon] read_api_key depuis 0.20.0. GET /api/findings reste non authentifié (lectures loopback) sauf si [daemon] require_read_key = true, qui met chaque lecture derrière la clé de lecture.
Le store runtime est un JSONL append-only à ~/.local/share/perf-sentinel/acks.jsonl par défaut. Suivez-le avec tail -f pour une piste d'audit en temps réel. Le fichier est rejoué et compacté à chaque redémarrage du daemon, donc les ajouts et révocations successifs ne s'accumulent pas. Les acks TOML CI chargés au démarrage (.perf-sentinel-acknowledgments.toml par défaut) restent immuables 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 métriques exposées sur
/metrics, dont les métriques de processus 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 prises en charge, 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].