Que fait Observabilité et Instrumentation ?
Vue d'ensemble
Un code que vous ne pouvez pas observer est un code que vous ne pouvez pas exploiter. L'observabilité est la capacité de répondre à « que fait le système et pourquoi ? » de l'extérieur, en utilisant la télémétrie émise par le code. L'instrumentation n'est pas un ajout après le lancement — elle s'écrit en même temps que la fonctionnalité, comme les tests. Si une fonctionnalité est livrée sans télémétrie, le premier bug signalé par un utilisateur devient une fouille archéologique au lieu d'une simple requête.
Quand l'utiliser
- Construire toute fonctionnalité qui tournera en production
- Ajouter un nouveau service, endpoint, tâche planifiée ou intégration externe
- Un incident de production a mis trop de temps à être diagnostiqué (« nous n'avons pas pu déterminer ce qui s'est passé »)
- Mettre en place ou revoir des règles d'alerting
- Revoir une PR qui ajoute des E/S, des tentatives, des files d'attente ou des appels inter-services
À NE PAS utiliser pour :
- Diagnostiquer une défaillance en cours — utilisez la compétence
debugging-and-error-recovery(l'observabilité est ce qui rendra cette compétence rapide la prochaine fois) - Profiler et optimiser une lenteur mesurée — utilisez la compétence
performance-optimization - Les listes de contrôle de surveillance le jour du lancement et les déclencheurs de rollback — voir la compétence
shipping-and-launch; cette compétence couvre l'instrumentation qui les alimente
Processus
1. Définir « fonctionne » avant d'instrumenter
De la télémétrie sans question, c'est du bruit. Avant d'ajouter une instrumentation, écrivez 2 à 4 questions qu'un ingénieur d'astreinte se posera à propos de cette fonctionnalité :
FONCTIONNALITÉ : reprise du paiement en caisse
QUESTIONS POUR L'ASTREINTE :
1. Quelle fraction des paiements réussit du premier coup par rapport à après une tentative ?
2. Lorsqu'un paiement échoue définitivement, pourquoi ? (erreur du fournisseur ? délai d'attente ? validation ?)
3. Le fournisseur de paiement est-il plus lent que d'habitude ?
→ Chaque signal ci-dessous doit aider à répondre à l'une de ces questions.
Si vous ne pouvez pas nommer les questions, vous n'êtes pas prêt à instrumenter — vous enregistrerez tout et n'apprendrez rien.
2. Choisir le bon signal pour chaque question
| Signal | Répond à | Profil de coût | Exemple |
|---|---|---|---|
| Log structuré | « Que s'est-il passé dans ce cas spécifique ? » | Par événement ; augmente avec le trafic | payment_failed avec le code d'erreur du fournisseur |
| Métrique | « À quelle fréquence / vitesse, en agrégé ? » | Fixe par série ; peu coûteux à interroger | latence p99 des appels au fournisseur |
| Trace | « Où le temps a-t-il été passé entre les services ? » | Par requête ; généralement échantillonné | Un paiement lent, décomposé par étape |
Règle empirique : les métriques vous disent que quelque chose ne va pas, les traces vous disent où, les logs vous disent pourquoi.
3. Journalisation structurée
Enregistrez des événements, pas des phrases. Chaque ligne de log est un objet JSON avec un nom d'événement stable et des champs lisibles par machine :
// MAUVAIS : interpolation de chaîne — non interrogeable, incohérent
logger.info(`Paiement ${id} échoué pour l'utilisateur ${userId} après ${n} tentatives`);
// BON : nom d'événement stable + champs structurés
logger.warn({
event: 'payment_failed',
paymentId: id,
provider: 'stripe',
errorCode: err.code,
attempt: n,
}, 'paiement échoué');
Niveaux de log — utilisez-les de manière cohérente :
| Niveau | Signification | Action d'astreinte |
|---|---|---|
error | Invariant rompu ; quelqu'un doit peut-être agir | Enquêter |
warn | Dégradé mais géré (tentative réussie, solution de repli utilisée) | Surveiller les tendances |
info | Événement métier significatif (commande passée, tâche terminée) | Aucune |
debug | Détail de diagnostic | Désactivé en production par défaut |
Les IDs de corrélation sont obligatoires. Générez (ou acceptez) un ID de requête à la frontière du système et attachez-le à chaque ligne de log, span et appel sortant. Sans lui, vous ne pouvez pas reconstruire une seule requête à partir de logs entrelacés :
// Express : logger enfant par requête, ID propagé en aval
app.use((req, res, next) => {
req.id = req.headers['x-request-id'] ?? crypto.randomUUID();
req.log = logger.child({ requestId: req.id });
res.setHeader('x-request-id', req.id);
next();
});
Lorsque plusieurs points d'entrée écrivent dans un même log, nommez le point d'entrée. Un ID de corrélation identifie une exécution ; il ne dit pas quel chemin de code l'a lancée. La même tâche atteinte par un planificateur, un endpoint de rejeu et une CLI manuelle produit des lignes interchangeables dans un même réceptacle : attribuer une ligne revient à éliminer — en recoupant l'historique du planificateur, la table des processus, un journal de déploiement — et cet argument ne tient que tant que ces enregistrements externes existent encore. Estampez le point d'entrée là où l'exécution commence, à côté de l'ID de corrélation, et propagez les deux de la même manière :
// Une seule aide pour chaque point d'entrée : le propre logger de l'exécution porte les deux champs.
// `entryPoint`, pas `source` — ECS réserve `source.*` pour les champs réseau.
export const runLog = (entryPoint: 'scheduler' | 'replay_endpoint' | 'cli', runId: string) =>
logger.child({ entryPoint, requestId: runId });
// tick du planificateur -> runLog('scheduler', crypto.randomUUID())
// POST /jobs/:id/replay -> runLog('replay_endpoint', req.id)
// invocation CLI -> runLog('cli', process.env.RUN_ID ?? crypto.randomUUID())
Les deux champs doivent traverser les mêmes frontières que l'ID de corrélation — métadonnées de file d'attente, en-têtes HTTP — sinon un travailleur déduit le point d'entrée et devine. Un champ qui se contente de corréler avec un point d'entrée est un indice, pas une attribution : tout ce qui peut invoquer la tâche peut la reproduire.
N'enregistrez jamais de secrets, jetons, mots de passe ou PII complète. C'est une règle stricte de la compétence security-and-hardening — les pipelines de télémétrie sont une voie classique de fuite de données. Listez les champs autorisés ; n'enregistrez pas les corps de requête entiers.
4. Métriques
Pour les services pilotés par requêtes, instrumentez RED sur chaque endpoint et chaque dépendance externe : Rate (requêtes/s), Erreurs (taux d'échec), Duration (histogramme de latence, pas de moyenne). Pour les ressources (files d'attente, pools, hôtes), utilisez USE : Utilisation, Saturation, Erreurs.
Comme pour le tracing, la voie neutre vis-à-vis du fournisseur est l'API métriques OpenTelemetry (même SDK et contexte que l'étape 5). L'exemple ci-dessous utilise prom-client de Prometheus — un choix de backend courant, pas le seul ; les règles RED/USE et de cardinalité sont identiques dans les deux cas.
import { Histogram } from 'prom-client';
const httpDuration = new Histogram({
name: 'http_request_duration_seconds',
help: 'Durée des requêtes HTTP',
labelNames: ['method', 'route', 'status_class'], // '2xx', pas '200'
buckets: [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
});
La cardinalité est le mode de défaillance. Chaque combinaison unique d'étiquettes est une série temporelle distincte. Les étiquettes doivent provenir d'ensembles petits et fixes (modèle de route, classe de statut, nom du fournisseur). N'utilisez jamais d'IDs utilisateur, d'URL brutes, de messages d'erreur ou d'autres valeurs non bornées comme étiquettes — cela appartient aux logs et aux traces.
OK comme étiquette : route="/api/tasks/:id" status_class="5xx" provider="stripe"
JAMAIS une étiquette : user_id, email, request_id, URL complète, texte du message d'erreur
Ne suivez jamais les moyennes, toujours les percentiles : une moyenne cache le 1 % d'utilisateurs qui vivent une mauvaise expérience. Utilisez des histogrammes et lisez p50/p95/p99.
5. Tracing distribué
Utilisez OpenTelemetry — c'est le standard neutre vis-à-vis du fournisseur, et l'auto-instrumentation couvre HTTP, gRPC et les clients DB courants avec quasiment zéro code :
// tracing.ts — doit être importé avant tout le reste
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
const sdk = new NodeSDK({
serviceName: 'checkout-service',
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
Ajoutez des spans manuels uniquement autour d'unités de travail internes significatives (par exemple, applyDiscounts, chargeProvider) et attachez les attributs que l'astreinte pourra filtrer. Propagez le contexte à travers chaque frontière asynchrone — en-têtes HTTP, métadonnées de message de file d'attente — sinon la trace meurt à l'écart. Échantillonnez head-based à un faible taux par défaut ; conservez 100 % des erreurs si votre backend supporte l'échantillonnage de queue.
6. Alerting
Déclenchez une alerte sur les symptômes ressentis par les utilisateurs, pas sur les causes :
SYMPTÔME (page justifiée) : CAUSE (tableau de bord, pas de page) :
taux d'erreur > 1 % pendant 5 min CPU à 85 %
latence p99 > 2 s un pod redémarré
âge de la file d'attente > 10 min disque à 70 %
Les alertes basées sur les causes se déclenchent quand rien ne va mal et manquent les défaillances que vous n'avez pas prévues. Les alertes basées sur les symptômes se déclenchent exactement quand les utilisateurs sont affectés, quelle que soit la cause.
Règles pour chaque alerte que vous créez :
- Elle doit être actionnable. Si la réponse est « ignorez-la, elle se guérit toute seule », supprimez l'alerte.
- Elle renvoie à un runbook — même trois lignes : ce que cela signifie, première requête à exécuter, chemin d'escalade.
- Elle a un seuil et une durée justifiés par le SLO ou par des données historiques, pas par une estimation.
- Utilisez deux niveaux de gravité seulement : page (visible pour l'utilisateur, agissez maintenant) et ticket (dégradation, agissez cette semaine). Un troisième niveau devient du bruit qui habitue les gens à tout ignorer.
Rédaction de runbooks
La règle 2 ci-dessus exige que chaque alerte renvoie à un runbook. Le travail d'un runbook est de répondre à trois questions sans obliger le lecteur à réfléchir : ce qui se passe, quoi vérifier en premier, et qui appeler si cela ne résout pas le problème. Stockez dans docs/runbooks/ nommé d'après l'alerte.
Runbook minimum viable (trois lignes) :
# Runbook : Taux d'erreur élevé sur /api/tasks
**Signification :** Le pool de connexions DB est probablement épuisé, ou un déploiement défectueux.
**Première vérification :** `SELECT count(*) FROM pg_stat_activity WHERE backend_type = 'client backend';`
— si le nombre > limite du pool, voir l'Étape 2. (Remplacez par l'équivalent pour votre base de données.)
**Escalade à :** #db-oncall ou rotation d'astreinte engineering.
Quand dépasser trois lignes : ajoutez des étapes uniquement lorsque la première vérification ne suffit pas à décider. Un runbook en cinq étapes qui couvre les trois causes les plus courantes est meilleur qu'un document de vingt étapes qui couvre tous les cas extrêmes et qui est parcouru en diagonale.
Maintenez les runbooks à jour. Mettez à jour le runbook en clôturant chaque incident où il a été utilisé — un runbook obsolète crée une fausse confiance. Si une étape était erronée ou manquante, corrigez-la avant de marquer l'incident comme résolu.
7. Vérifier la télémétrie elle-même
L'instrumentation est du code ; elle peut être erronée. Avant de considérer le travail comme terminé, déclenchez les chemins et examinez la sortie réelle :
- Provoquez une erreur en staging → trouvez-la dans les logs par
requestId, confirmez que les champs sont structurés (pas[object Object]) - Envoyez du trafic de test → confirmez que les séries de métriques apparaissent avec les étiquettes attendues et des valeurs cohérentes
- Suivez une requête à travers les services dans l'interface de tracing → pas de spans cassés
- Déclenchez chaque nouvelle alerte une fois (baissez temporairement le seuil) → confirmez qu'elle atteint le bon canal et que le lien vers le runbook fonctionne
Rationalisations courantes
| Rationalisation | Réalité |
|---|---|
| « J'ajouterai les logs après que ça fonctionne » | « Après » devient « après le premier incident », qui est le moment le plus coûteux pour découvrir que vous êtes aveugle. Instrumentez en construisant. |
| « Plus de logs = plus d'observabilité » | Le bruit non structuré rend les incidents plus lents, pas plus rapides. Trois événements interrogeables battent trois cents lignes de prose. |
| « console.log suffit pour l'instant » | La sortie non structurée ne peut être filtrée, corrélée ni alertée. Le logger structuré coûte cinq minutes supplémentaires une fois. |
| « Nous pouvons simplement regarder les tableaux de bord quand quelque chose se casse » | Les tableaux de bord construits sans questions définies vous montrent tout sauf la réponse. Commencez par les questions d'astreinte. |
| « Alertez sur tout ce qui est important, nous ajusterons plus tard » | Un pager bruyant habitue les gens à l'ignorer. L'ajustement n'arrive jamais ; la vraie page manquée, oui. |
| « L'ID utilisateur comme étiquette de métrique facilite le débogage » | Cela fait aussi s'effondrer votre backend de métriques. Les recherches à haute cardinalité appartiennent aux logs et aux traces. |
| « Le tracing est excessif pour nos deux services » | Deux services signifient déjà des questions de latence inter-services que les logs ne peuvent pas répondre. L'auto-instrumentation rend le coût négligeable. |
Drapeaux rouges
- Une PR de fonctionnalité avec des tentatives, des files d'attente ou des appels externes et zéro nouvelle télémétrie
- Des lignes de log construites par interpolation de chaîne au lieu de champs structurés
- Absence d'ID de corrélation/requête — chaque ligne de log est un orphelin
- Un flux de log alimenté par un planificateur, un webhook et des exécutions manuelles, sans champ indiquant lequel a produit la ligne
- Des métriques étiquetées avec des IDs utilisateur, des URL brutes ou du texte de message d'erreur (bombe de cardinalité)
- Latence suivie comme une moyenne sans percentiles
- Alertes qui se déclenchent quotidiennement et sont acquittées sans action
- Alertes sur les causes (CPU, mémoire) qui pagent des humains alors que le taux d'erreur visible par l'utilisateur n'est pas surveillé
- Secrets, jetons ou corps de requête complets apparaissant dans les logs
- « Ça marche sur ma machine » comme seule preuve qu'une fonctionnalité de production est saine
Vérification
Après avoir instrumenté une fonctionnalité, confirmez :
- Les questions d'astreinte pour cette fonctionnalité sont écrites, et chaque signal en correspond à une
- Toute la sortie des logs est structurée (JSON), avec des noms d'événement stables et un ID de corrélation sur chaque ligne
- Chaque réceptacle de log écrit par plus d'un point d'entrée porte un champ de point d'entrée, défini là où l'exécution commence et propagé avec l'ID de corrélation plutôt que déduit en aval
- Aucun secret, jeton ou PII non expurgé dans aucune ligne de log (vérifiez ponctuellement la sortie réelle)
- Des métriques RED existent pour chaque nouvel endpoint et chaque dépendance externe, avec des ensembles d'étiquettes bornés
- La latence est un histogramme ; p95/p99 sont interrogeables
- Une seule requête peut être suivie de bout en bout dans l'interface de tracing sans spans cassés
- Chaque nouvelle alerte est basée sur un symptôme, a un lien vers un runbook, et a été testée une fois
- Une défaillance provoquée en staging a été localisée via la télémétrie seule, sans lire le source
Pour la version condensée de cette liste, incluant la porte d'instrumentation pré-lancement, voir ../../references/observability-checklist.md.