En-tête traceparent : le guide complet du W3C Trace Context
L’en-tête traceparent, la brique de base du tracing distribué, tient sur une seule ligne d’ASCII et transporte l’identité d’une requête à travers chaque service qu’elle traverse. Dans sa version actuelle, il fait exactement 55 caractères et se compose de quatre champs séparés par des tirets :
00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
│ │ │ │
│ │ │ └─ trace-flags (2 hex, 1 octet)
│ │ └─ parent-id (16 hex, 8 octets)
│ └─ trace-id (32 hex, 16 octets)
└─ version (2 hex, 1 octet)
Deux de ces champs se comportent différemment au fil du parcours. Le trace-id reste identique à chaque saut : c’est le nom de la requête, depuis le proxy d’entrée jusqu’au dernier appel de base de données. Le parent-id change à chaque saut, parce qu’il désigne le span qui vous a appelé, pas la requête. Confondre les deux explique une bonne part des tickets « mes traces sont bizarres ».
Voilà pour l’anatomie. Le reste est plus délicat, parce qu’un tableau des champs ne le montre pas : ce qui rend un en-tête invalide, ce qu’un récepteur conforme fait lorsqu’il en reçoit un, et où l’en-tête disparaît discrètement entre deux services qui prétendent tous deux gérer le tracing. Si vous avez un en-tête réel sous les yeux, collez-le dans le décodeur traceparent gratuit : il découpe les champs, déplie l’octet de flags bit par bit et nomme la règle sur laquelle bute un en-tête cassé.
L’en-tête traceparent en un coup d’œil
L’en-tête traceparent est un en-tête HTTP unique qui transporte une trace distribuée d’un service à l’autre. Il contient quatre champs hexadécimaux séparés par des tirets (version, trace-id, parent-id et trace-flags) et, dans sa version actuelle, il fait exactement 55 caractères. Le trace-id nomme la requête entière ; le parent-id nomme le span qui vous a appelé.
| Champ | Chiffres hex | Octets | Ce qu’il identifie | Change à chaque saut ? |
|---|---|---|---|---|
version | 2 | 1 | Le format que suit le reste. Toujours 00 aujourd’hui | Non |
trace-id | 32 | 16 | La requête entière, de bout en bout | Non |
parent-id | 16 | 8 | Le span appelant (l’ID de span de votre appelant) | Oui |
trace-flags | 2 | 1 | Un champ de 8 bits ; le bit 0 est sampled | Rarement |
Ajoutez trois tirets à ces 52 chiffres hexadécimaux et vous obtenez 55 caractères. Ce nombre vaut la peine d’être mémorisé : un en-tête de version 00 d’une longueur différente est invalide, et la longueur est ce qui se vérifie le plus vite à l’œil nu.
Tout, dans l’en-tête, est de l’hexadécimal en minuscules. Pas « hexadécimal, insensible à la casse » : en minuscules. La grammaire de la recommandation W3C Trace Context n’admet que 0-9 et a-f, rien d’autre, et c’est pourquoi un trace ID en majuscules dont la valeur est par ailleurs parfaitement correcte finit quand même à la poubelle en aval.
Champ par champ
Chaque champ a sa largeur, ses valeurs invalides et sa manière de casser bien à lui.
version — pourquoi ce n’est pas toujours « juste 00 »
Aujourd’hui l’octet de version vaut 00, et il vaudra 00 encore un moment. Mais ff est explicitement interdit : la spécification le réserve comme valeur invalide, si bien qu’un en-tête qui commence par ff est mort-né, quel que soit ce qui suit.
La règle intéressante concerne les versions que vous n’avez jamais vues. Un parseur qui fait if (version !== '00') reject() a tort, et il a tort d’une manière coûteuse. La spécification demande aux récepteurs de tenter l’analyse quand la version est supérieure et que l’en-tête est au moins aussi long que le format connu : lisez les champs que vous reconnaissez, tolérez les données supplémentaires en fin de chaîne, et continuez. Refuser revient à faire de votre service la frontière où la trace s’arrête et où une nouvelle commence, dès l’instant où quelqu’un en amont met sa version à jour.
// Faux : votre service devient l'endroit où les traces vont mourir
if (version !== '00') throw new Error('bad traceparent');
// Juste : analysez le préfixe que vous comprenez
if (version !== '00' && header.length >= 55) {
// lire version, trace-id, parent-id, trace-flags ; ignorer le reste
}
trace-id — 16 octets, l’identité de la requête entière
Trente-deux chiffres hexadécimaux en minuscules, constants pendant toute la durée de vie de la trace. Quel que soit le service qui l’a généré au départ, chaque saut le recopie tel quel. Quand vous cherchez une trace dans votre backend d’observabilité, c’est cette chaîne que vous collez.
Deux règles gouvernent sa valeur : il doit faire 32 chiffres hexadécimaux, et il ne doit pas être entièrement à zéro. 00000000000000000000000000000000 ne signifie pas « une trace qui n’a pas encore de données » : la spécification la désigne comme valeur invalide et exige des récepteurs qu’ils ignorent l’en-tête tout entier. En pratique, un trace ID tout à zéro trahit un SDK qui ne s’est jamais initialisé, ou un middleware qui insère un bouche-trou faute de contexte réel à transmettre.
Un trace-id fait 128 bits, la même largeur qu’un UUID, et ce n’est pas un UUID. Il n’y a ni bits de version, ni bits de variante, ni tirets, ni structure d’aucune sorte : seize octets opaques. Vous n’en extrairez pas une v4, et un UUID dont on a retiré les tirets n’est pas pour autant un trace-id valide, parce que les quartets de version et de variante rendent son aléa non uniforme. Pour voir ce qu’un UUID réserve réellement dans ces 128 bits, ce que code vraiment un UUID détaille la disposition, et le générateur UUID montre les bits de version et de variante à leur place.
parent-id — 8 octets, le span qui vous a appelé
Seize chiffres hexadécimaux, réécrits à chaque saut. Le nom sème plus de confusion que le champ n’en mérite : la spécification W3C l’appelle parent-id, OpenTelemetry appelle ces mêmes 8 octets un span ID, et c’est la même chose vue de deux côtés. Du point de vue de votre service, c’est le parent ; du point de vue de l’appelant, c’est l’ID du span qu’il vient de créer pour la requête sortante.
Ainsi, quand le service A appelle le service B, A place son propre span ID dans l’emplacement parent-id. B crée ensuite un span enfant, et lorsque B appelle C, c’est le span ID de B qui prend la place. Le trace-id, lui, n’est jamais touché. Voilà tout l’algorithme de propagation.
Les parent-id entièrement à zéro sont invalides eux aussi, pour la même raison que les trace-id : 0000000000000000 signifie que l’appelant n’a pas fourni de span réel, et l’en-tête doit être jeté plutôt qu’honoré à moitié.
trace-flags — on dirait un booléen, ce sont huit bits
Presque tous les en-têtes que vous croiserez se terminent par 01, il est donc naturel de lire ce champ comme un oui/non. C’est un octet, et ses bits sont attribués ainsi :
- bit 0, masque
0x01—sampled - bit 1, masque
0x02—random-trace-id, ajouté dans Trace Context Level 2 - bits 2 à 7 — réservés ; ignorez-les en réception, remettez-les à zéro sur les requêtes sortantes
Voici ce que donnent les combinaisons :
| Hex | Binaire | sampled | random-trace-id | flags === 0x01 est-il vrai ? |
|---|---|---|---|---|
00 | 00000000 | false | false | false |
01 | 00000001 | true | false | true |
02 | 00000010 | false | true | false |
03 | 00000011 | true | true | false ← le bug |
Relisez la dernière ligne. Une trace dont les flags valent 03 est échantillonnée. Tout code qui compare l’octet entier à 01 la déclare non échantillonnée, en silence, et uniquement pour la fraction du trafic où le drapeau Level 2 se trouve positionné. C’est la pire forme de panne possible, parce qu’elle ressemble à un problème de taux d’échantillonnage plutôt qu’à un bug d’analyse.
const flags = parseInt(traceFlags, 16);
// Faux : traite un champ de bits comme une énumération
const sampled = traceFlags === '01';
// Juste
const sampled = (flags & 0x01) !== 0;
const randomTraceId = (flags & 0x02) !== 0;
Qu’affirme au juste random-trace-id ? Que les 7 octets les plus à droite du trace-id, au minimum, ont été générés avec un aléa uniforme. Cela paraît théorique jusqu’à ce qu’on pense à l’échantillonnage cohérent : si un système en aval veut conserver 1 % des traces et que chaque service doit s’accorder indépendamment sur les mêmes 1 %, il peut prendre ces octets modulo quelque chose au lieu de hacher d’abord l’identifiant. Le drapeau est la promesse, faite par l’amont, que l’opération est sûre.
Ce qui rend un traceparent invalide
Les bases de connaissances des éditeurs s’attardent rarement là-dessus, alors qu’un en-tête rejeté est le point de départ de la plupart des enquêtes. Voici l’ensemble complet des motifs de rejet pour un en-tête de version 00 :
| Symptôme | Règle | Résultat |
|---|---|---|
00-4BF92F35...-01 | La grammaire n’admet que de l’hexadécimal minuscule | Invalide — la valeur est bonne, l’en-tête est rejeté |
ff-... | La version ff est interdite par la spécification | Invalide |
trace-id valant 00000000000000000000000000000000 | Un trace-id tout à zéro est une valeur explicitement invalide | Invalide |
parent-id valant 0000000000000000 | Un parent-id tout à zéro est une valeur explicitement invalide | Invalide |
| trace-id ne faisant pas 32 chiffres hexadécimaux | Largeur fixe | Invalide |
| parent-id ne faisant pas 16 chiffres hexadécimaux | Largeur fixe | Invalide |
| trace-flags ne faisant pas 2 chiffres hexadécimaux | Largeur fixe | Invalide |
En-tête ne faisant pas exactement 55 caractères, version 00 | Les données en trop ne sont légales que sous une version future | Invalide |
Tout caractère hors de 0-9a-f et des tirets | Ce n’est pas de l’hexadécimal | Invalide |
La conséquence de ce rejet est rarement écrite noir sur blanc :
Un récepteur conforme ne répare pas un en-tête traceparent invalide et ne le transmet pas. Il le jette et démarre une trace entièrement neuve, avec un trace-id fraîchement généré.
Autrement dit, le symptôme affiché à l’écran n’est pas une trace cassée. Ce sont deux courtes traces déconnectées : l’une s’arrête net au service qui a émis le mauvais en-tête, l’autre semble commencer de nulle part, au service qui l’a reçu. Rien n’est signalé comme une erreur où que ce soit. Prises isolément, les deux traces ont l’air en parfaite santé. On y passe des après-midi à chercher le chaînon manquant entre elles, alors que la réponse tient à un middleware qui a mis une chaîne hexadécimale en majuscules, ou à un en-tête construit à la main qui fait 54 caractères.
La longueur et la casse sont les deux modes de défaillance qu’on ne repère pas à l’œil. Collez l’en-tête dans le décodeur : il nomme la règle exacte qui a été enfreinte au lieu de vous faire compter des chiffres.
tracestate : l’en-tête compagnon que tout le monde rate
traceparent transporte l’identité standard. L’en-tête tracestate transporte ce que chaque éditeur veut y ajouter, sous forme de membres key=value séparés par des virgules :
tracestate: rojo=00f067aa0ba902b7,congo=t61rcWkgMzE
Une implémentation qui ne reconnaît pas une clé doit la retransmettre telle quelle. C’est tout l’objectif de conception : les éditeurs peuvent faire voyager un état propriétaire sur une trace standard sans que chaque saut ait besoin de le comprendre.
La grammaire, cela dit, a du mordant, et trois de ses règles expliquent des symptômes bien réels en production.
32 membres de liste, c’est un plafond dur. Ce n’est pas un conseil, c’est la grammaire : list = list-member 0*31( OWS "," OWS list-member ). Un tracestate à 33 membres n’est pas un tracestate avec une entrée en trop, c’est un en-tête invalide, et les récepteurs sont en droit de tout jeter. Voilà la réponse à un symptôme qui, sinon, tient de la magie : des données d’éditeur présentes en périphérie, présentes deux sauts plus loin, et complètement disparues au cinquième. Chaque saut ajoutait son propre membre, la liste a dépassé 32, et à partir de là l’en-tête entier était supprimé au lieu d’être tronqué.
Les valeurs font de 1 à 256 caractères et ne peuvent jamais être vides. La production de la valeur se termine par un caractère non blanc obligatoire : vendor= n’est donc pas « une clé sans valeur », c’est une erreur de syntaxe. ASCII imprimable uniquement, et jamais de virgule ni de signe égal à l’intérieur de la valeur.
La grammaire des clés a changé entre Level 1 et Level 2. Level 1 définissait les clés par une production tenant@vendor, où @ servait de séparateur structurel. Level 2 l’a remplacée par une classe de caractères à plat : une clé commence par une lettre minuscule ou un chiffre, puis se poursuit avec a-z, 0-9, _, -, *, / et @. Sous Level 2, @ est un caractère ordinaire, les clés peuvent commencer par un chiffre, et a@b@c est une clé parfaitement légale que la production Level 1 refuserait. Si vous avez un proxy qui valide selon Level 1 et un service qui émet des clés Level 2, chacun accepte ce que l’autre rejette, et l’en-tête disparaît à un saut bien précis.
Deux autres règles méritent d’être connues. Les clés en double sont purement et simplement invalides. Et lorsque vous modifiez le parent-id du traceparent, vous devez déplacer votre propre entrée tracestate en tête de liste : la liste est ordonnée du plus récent au plus ancien. Sauter cette remise en tête laisse un état d’éditeur périmé à un endroit où un lecteur le prendra pour l’état courant.
Enfin, la règle bienveillante : les membres vides sont légaux. Quand un équipement intermédiaire retire une entrée, il laisse souvent la virgule derrière lui, ce qui donne rojo=1,,congo=2. La spécification l’autorise explicitement : un parseur doit donc ignorer le membre vide et continuer plutôt que de déclarer l’en-tête malformé. La vue tracestate du décodeur liste chaque membre avec sa validation individuelle et un compteur courant face à la limite de 32 membres, ce qui va généralement plus vite que de compter les virgules.
Le parcours des en-têtes de tracing distribué : une requête, quatre sauts
Suivez une requête à travers un proxy d’entrée, un service API et deux services en aval :
Client
│ (pas de traceparent — la périphérie est la racine)
▼
Proxy d'entrée génère le trace-id 4bf9…4736, span 00f0…02b7
│ traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
▼
Service API le lit, crée le span a1b2c3d4e5f60718
│ traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-a1b2c3d4e5f60718-01
▼
Service commandes le lit, crée le span 9f8e7d6c5b4a3928
│ traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-9f8e7d6c5b4a3928-01
▼
Service inventaire
Chaque saut fait les trois mêmes choses : lire l’en-tête entrant, remplacer le parent-id par son propre span ID pour chaque appel sortant, et transmettre le trace-id et les flags inchangés. Quand il n’y a aucun en-tête entrant, comme pour le client ci-dessus, le service qui reçoit devient la racine : il génère un trace-id et prend la décision d’échantillonnage pour tout ce qui suit en aval.
Vous pouvez injecter un en-tête à la main pour tester une chaîne de bout en bout :
curl -sS -o /dev/null -w '%{http_code}\n' \
-H 'traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' \
-H 'tracestate: rojo=00f067aa0ba902b7,congo=t61rcWkgMzE' \
https://example.com/api
Rejouez sur la préproduction un en-tête capturé en production et vous verrez le même trace-id apparaître dans votre backend. Le générateur de commande cURL assemble les options à votre place si vous ajoutez de l’authentification ou un corps de requête, et l’aide-mémoire curl couvre les options d’en-tête et de verbosité dont vous aurez besoin pendant le débogage.
Pour voir ce qu’un service a réellement reçu, plutôt que ce que vous croyez avoir envoyé, lancez un serveur d’écho jetable et faites pointer un saut vers lui :
python3 - <<'PY'
from http.server import BaseHTTPRequestHandler, HTTPServer
class Echo(BaseHTTPRequestHandler):
def do_GET(self):
for name, value in self.headers.items():
print(f"{name}: {value}")
self.send_response(200)
self.end_headers()
self.wfile.write(b"ok\n")
HTTPServer(("127.0.0.1", 8080), Echo).serve_forever()
PY
Ensuite, lancez curl -H 'traceparent: …' http://127.0.0.1:8080/ et lisez ce qui ressort de l’autre côté. La moitié des enquêtes « le proxy mange mon en-tête » se terminent ici.
trace-flags sampled : une décision en amont, pas un accusé de réception
Un bit sampled de trace-flags à 1 signifie que le service en amont a décidé d’enregistrer cette trace. Il ne garantit pas que les données sont arrivées jusqu’à votre backend.
L’échantillonnage en tête (head-based) prend cette décision à la racine, avant que quoi que ce soit ne se produise, et la propage vers le bas : peu coûteux, cohérent entre services, et aveugle, puisqu’il ne peut pas savoir que la requête était sur le point d’échouer. L’échantillonnage en queue (tail-based) met les spans en mémoire tampon jusqu’à la fin de la trace avant de décider : il peut donc conserver toutes les traces contenant une erreur, au prix d’une rétention en mémoire et de l’obligation que les spans de tous les services atterrissent dans le même collecteur.
Avec l’échantillonnage en queue, une trace peut arriver marquée 01 à chaque saut et être malgré tout abandonnée à la fin. Les limitations de débit et les quotas d’export peuvent aussi la faire disparaître. Un 01 en périphérie sans trace dans l’interface n’est donc pas forcément un bug de propagation : regardez les métriques d’abandon du collecteur avant d’aller inspecter les en-têtes.
Le cas inverse compte davantage au quotidien. Si les flags entrants valent 00, l’appelant a exécuté son échantillonneur et a choisi de ne pas enregistrer. Rien n’est mal configuré chez vous, et auditer votre propre échantillonneur est du temps perdu : cherchez plutôt quel service en amont a pris cette décision.
Convertir entre formats de propagation
W3C Trace Context a gagné, mais quantité de systèmes parlent encore quelque chose de plus ancien, et les passerelles traduisent de l’un vers l’autre. Voici le même exemple de traceparent écrit dans quatre formats :
| Format | En-tête(s) | Valeur pour notre exemple |
|---|---|---|
| W3C | traceparent | 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 |
| B3 unique | b3 | 4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-1 |
| B3 multiple | X-B3-TraceId, X-B3-SpanId, X-B3-Sampled | 4bf92f3577b34da6a3ce929d0e0e4736, 00f067aa0ba902b7, 1 |
| Datadog | x-datadog-trace-id, x-datadog-parent-id, tag _dd.p.tid | 11803532876627986230, 67667974448284343, 4bf92f3577b34da6 |
| AWS X-Ray | X-Amzn-Trace-Id | Root=1-4bf92f35-77b34da6a3ce929d0e0e4736;Parent=00f067aa0ba902b7;Sampled=1 |
Datadog : la coupure 64 bits hauts / 64 bits bas
Les identifiants de Datadog sont antérieurs aux trace ID de 128 bits, et c’est dans la couche de compatibilité que la plupart des conversions dérapent. x-datadog-trace-id transporte les 64 bits de poids faible sous forme de chaîne décimale. Les 64 bits de poids fort voyagent séparément, en hexadécimal, dans le tag _dd.p.tid, lui-même transporté par l’en-tête x-datadog-tags.
const traceparent = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01';
const [, traceId, parentId] = traceparent.split('-');
const datadogTraceId = BigInt('0x' + traceId.slice(16)).toString(10);
const higher64Hex = traceId.slice(0, 16);
const datadogParentId = BigInt('0x' + parentId).toString(10);
console.log('x-datadog-trace-id:', datadogTraceId); // 11803532876627986230
console.log('x-datadog-tags:', higher64Hex); // _dd.p.tid=4bf92f3577b34da6
console.log('x-datadog-parent-id:', datadogParentId); // 67667974448284343
L’erreur classique consiste à convertir les 128 bits en un seul nombre décimal :
BigInt('0x' + traceId).toString(10);
// 100985939111033328018442752961257817910 — ne correspond à rien dans l'interface
Cette valeur n’est pas une erreur de calcul. C’est le rendu décimal correct de la mauvaise quantité, et c’est pourquoi elle passe la revue de code avant de ne correspondre, discrètement, à aucune trace.
Le deuxième piège est la précision numérique. Un identifiant de 64 bits dépasse Number.MAX_SAFE_INTEGER, qui vaut 9007199254740991 : tout chemin de code qui laisse un trace ID devenir un nombre JavaScript en corrompt les chiffres de poids faible. Gardez les trace ID sous forme de chaînes et ne sortez BigInt que lorsque vous devez faire des calculs ; un identifiant qui arrive sans guillemets dans du JSON est déjà abîmé au moment où vous le voyez.
AWS X-Ray : l’horodatage qui n’existe pas
Un trace ID X-Ray ressemble à 1-{8 hex}-{24 hex}, et les 8 premiers chiffres hexadécimaux sont l’heure de création en secondes epoch. La conversion depuis W3C est mécanique :
const traceId = '4bf92f3577b34da6a3ce929d0e0e4736';
const epochHex = traceId.slice(0, 8); // 4bf92f35
const epochSeconds = parseInt(epochHex, 16); // 1274621749
const xrayId = `1-${epochHex}-${traceId.slice(8)}`;
// 1-4bf92f35-77b34da6a3ce929d0e0e4736
// X-Amzn-Trace-Id: Root=1-4bf92f35-77b34da6a3ce929d0e0e4736;Parent=00f067aa0ba902b7;Sampled=1
new Date(epochSeconds * 1000).toISOString(); // 2010-05-23T13:35:49.000Z
Regardez cette date. L’en-tête d’exemple de la spécification se décode en mai 2010, ce qui est manifestement absurde. Un trace-id W3C ne contient aucun horodatage. Seize octets aléatoires produiront volontiers un epoch d’apparence plausible dès qu’on lit les quatre premiers comme un seul entier, et ce nombre ne veut rien dire à moins que l’identifiant vienne réellement de X-Ray. Décoder une date depuis un trace-id quelconque, c’est lire un nombre aléatoire et le croire.
Quand l’identifiant vient bel et bien de X-Ray, la conversion est utile : passez ces huit chiffres hexadécimaux dans le convertisseur de timestamp Unix pour obtenir une date lisible, et le guide des timestamps Unix couvre les pièges secondes/millisecondes et de fuseau horaire qui s’ensuivent.
B3 : l’héritage Zipkin
B3 vient de Zipkin et c’est le format que l’on croise dans les maillages de services plus anciens. La forme à en-tête unique s’écrit traceId-spanId-sampled, où le champ sampled vaut 1 ou 0 plutôt qu’un octet hexadécimal : le bit random-trace-id de Level 2 n’a donc nulle part où aller et se perd purement et simplement dans la traduction. La forme multi-en-têtes répartit les mêmes valeurs entre X-B3-TraceId, X-B3-SpanId et X-B3-Sampled.
La complication historique porte sur la largeur. Les trace ID B3 peuvent faire 64 bits, soit 16 chiffres hexadécimaux au lieu de 32. Convertir un ID B3 de 64 bits vers W3C consiste à compléter à gauche avec des zéros pour atteindre 32 chiffres ; convertir en sens inverse oblige à décider si l’on tronque. Compléter à gauche est sans risque, tronquer ne l’est pas, car deux traces qui ne diffèrent que par leurs octets de poids fort se retrouvent confondues en une seule.
Où traceparent se perd en production
Tout ce qui précède suppose que l’en-tête arrive. Souvent, il n’arrive pas, et il se volatilise à quatre endroits en particulier.
Le navigateur l’abandonne sur les appels cross-origin
Symptôme : les traces du frontend existent, celles du backend aussi, et rien ne les relie. Ou bien la requête cross-origin échoue franchement, avec une erreur CORS.
Cause : traceparent est un en-tête personnalisé ; l’ajouter rend la requête non simple et déclenche une requête préliminaire OPTIONS. Si la réponse du serveur à cette requête préliminaire ne liste pas l’en-tête dans Access-Control-Allow-Headers, le navigateur bloque la vraie requête. De son côté, l’instrumentation navigateur d’OpenTelemetry refuse d’injecter les en-têtes de trace dans les requêtes cross-origin tant que vous ne lui indiquez pas quelles origines sont autorisées.
Correctif : côté serveur, renvoyez Access-Control-Allow-Headers: traceparent, tracestate pour la requête préliminaire. Côté SDK navigateur, réglez propagateTraceHeaderCorsUrls sur un motif correspondant aux origines de votre API. Les deux sont nécessaires : l’un sans l’autre vous laisse le même symptôme. Si la requête préliminaire revient avec un statut inattendu, vérifiez-le dans l’aide-mémoire des codes HTTP avant de conclure que le problème vient de l’en-tête.
Proxys, WAF et répartiteurs de charge suppriment les en-têtes inconnus
Symptôme : l’en-tête est présent quand vous interrogez le service directement avec curl, et absent quand la même requête passe par la passerelle.
Cause : une transmission fondée sur une liste d’autorisation. Beaucoup de configurations de proxy, de jeux de règles WAF et de répartiteurs de charge managés ne transmettent que les en-têtes qu’ils reconnaissent, et traceparent ne figure pas dans la liste par défaut. Certains maillages réécrivent aussi l’en-tête : ils génèrent leur propre trace-id et jettent le vôtre.
Correctif : procédez par dichotomie avec le serveur d’écho vu plus haut, en le plaçant derrière chaque saut tour à tour pour voir quelle couche fait tomber l’en-tête. Autorisez ensuite explicitement traceparent et tracestate dans les règles de transmission de cette couche. Si le proxy est nginx, notez que c’est le bloc qui traite une route qui décide des en-têtes transmis, et que ce bloc n’est pas toujours celui que vous croyez : les règles de priorité des location nginx expliquent pourquoi une configuration d’en-tête peut sembler totalement ignorée.
Les files de messages n’ont pas d’en-têtes HTTP
Symptôme : la trace s’arrête à l’instant où une requête devient une tâche de fond.
Cause : il n’y a pas de requête HTTP à cette frontière, donc rien sur quoi propager l’en-tête. Kafka dispose d’en-têtes d’enregistrement, SQS d’attributs de message, et l’instrumentation HTTP ne remplit ni l’un ni l’autre à votre place.
Correctif : injectez le contexte dans le message côté producteur et extrayez-le côté consommateur. Tous les SDK OpenTelemetry exposent inject et extract précisément pour cela, et le format sur le fil reste la même chaîne W3C ; seul le porteur passe d’une table d’en-têtes HTTP à des métadonnées de message. La documentation des propagateurs OpenTelemetry décrit l’interface de porteur langage par langage.
La casse, et ce que HTTP/2 met réellement en minuscules
Symptôme : une discussion embrouillée en revue de code pour savoir si Traceparent est acceptable.
Cause : deux règles distinctes se retrouvent fusionnées en une seule. Les noms d’en-tête HTTP/1.1 sont insensibles à la casse, et HTTP/2 exige qu’ils soient encodés en minuscules sur le fil. Cela concerne le nom. Indépendamment, l’hexadécimal contenu dans la valeur de l’en-tête doit être en minuscules, parce que la grammaire W3C l’impose, et aucune version du protocole ne corrigera cela pour vous.
Correctif : envoyez le nom sous la forme traceparent et ne mettez jamais la valeur en majuscules. Une passerelle qui normalise les noms d’en-tête ne normalisera pas vos chiffres hexadécimaux, et un trace-id en majuscules traverse sans encombre toutes les couches de transport avant d’être rejeté par l’application qui finit par l’analyser.
Faut-il faire confiance à un traceparent entrant ?
Un traceparent qui arrive depuis l’internet public est une entrée contrôlée par l’utilisateur : une chaîne choisie par un client anonyme, que la plupart des services acceptent sans y réfléchir à deux fois.
Trois risques concrets en découlent. Le greffage de trace d’abord : un attaquant qui envoie un trace-id observé ailleurs voit sa requête recousue dans une trace existante, ce qui pollue le graphe et peut exposer des temps d’exécution internes à quiconque sait lire cette trace. La combustion de quota ensuite : coder 01 en dur force l’échantillonnage sur chaque requête, et une inondation même modeste se transforme en facture d’ingestion très salée ou, pire, évince les traces dont vous aviez réellement besoin. Et la corrélation entre locataires : réutiliser un même trace-id sur des requêtes de locataires différents relie des enregistrements que votre outillage traite ensuite comme une seule opération logique.
La position pragmatique consiste à accepter en périphérie sans faire confiance. Validez la grammaire et rejetez les en-têtes malformés au lieu de les laisser filer vers l’intérieur. Pour le trafic non authentifié, rejouez votre propre décision d’échantillonnage plutôt que d’honorer le drapeau entrant, afin qu’aucun client externe ne puisse bloquer votre échantillonneur sur « toujours enregistrer ». Pour le trafic authentifié, honorer la décision de l’appelant convient généralement, puisque vous savez à qui vous avez affaire.
Et considérez le trace-id comme public. Ce n’est pas un secret et ça ne l’a jamais été : il apparaît dans les journaux, dans les pages d’erreur, dans les en-têtes de réponse et dans les captures d’écran collées aux tickets de support. N’y encodez jamais un identifiant d’utilisateur, un nom de locataire ni quoi que ce soit de signifiant, et ne vous en servez jamais comme clé d’autorisation. C’est un identifiant de corrélation, et il ne devrait jamais être autre chose.
FAQ
Quelle est la différence entre traceparent et tracestate ?
traceparent transporte l’identité normalisée (trace-id, parent-id et flags d’échantillonnage) et toute implémentation doit savoir le comprendre. tracestate transporte un état propre à chaque éditeur, que les implémentations qui ne le connaissent pas retransmettent tel quel. Les deux sont liés : quand le traceparent est invalide, la spécification exige que le tracestate soit ignoré lui aussi.
Pourquoi ma trace repart-elle de zéro au milieu de la chaîne d’appels ?
Une trace repart de zéro au milieu de la chaîne presque toujours parce qu’un saut a reçu un en-tête qui ne passait pas la grammaire, l’a jeté et a généré un nouveau trace-id. De l’hexadécimal en majuscules, un trace-id tout à zéro et un en-tête qui ne fait pas exactement 55 caractères produisent tous cet effet. Si l’en-tête est bien formé, les suspects suivants sont un proxy qui le supprime et une requête préliminaire cross-origin qui échoue.
Faut-il configurer CORS pour envoyer traceparent depuis un navigateur ?
Oui, la configuration CORS est obligatoire. C’est un en-tête personnalisé : il rend la requête non simple et déclenche une requête préliminaire, et le serveur doit lister traceparent dans Access-Control-Allow-Headers. L’instrumentation navigateur d’OpenTelemetry réclame en plus la configuration de propagateTraceHeaderCorsUrls, car elle n’injecte pas les en-têtes de trace en cross-origin par défaut.
Comment propager le contexte de trace à travers Kafka ou SQS ?
Écrivez la valeur du traceparent dans un en-tête d’enregistrement Kafka ou un attribut de message SQS côté producteur, puis relisez-la côté consommateur pour restaurer le contexte. Les SDK OpenTelemetry exposent inject et extract pour cela dans tous les langages. Le format ne change pas ; seul le porteur diffère d’une table d’en-têtes HTTP.
Peut-on exposer un trace ID sans risque dans les journaux ou les réponses ?
Oui, un trace ID peut être exposé sans risque. C’est un identifiant aléatoire, sans identité intégrée ni pouvoir d’autorisation. Il corrèle en revanche des enregistrements entre systèmes : n’y encodez donc jamais d’identifiant d’utilisateur ou de nom de locataire, et ne l’acceptez jamais comme preuve de quoi que ce soit. Traitez-le comme une clé de corrélation publique et vous pourrez le journaliser, le renvoyer et le partager sans risque.
Qui génère l’en-tête traceparent ?
Le premier service qui traite une requête arrivée sans en-tête. C’est en général un proxy d’entrée, une passerelle API ou un SDK navigateur, et il devient la racine de la trace : il génère le trace-id, crée le premier span, prend la décision d’échantillonnage. Tous les sauts suivants ne réécrivent plus que le parent-id.
L’en-tête traceparent est-il obligatoire ?
Non. Il est facultatif au niveau du protocole, et une requête sans en-tête reste parfaitement valide : le service qui la reçoit devient simplement la racine d’une nouvelle trace. Il n’est obligatoire qu’au sens pratique, puisque sans lui le travail réparti entre deux services ne peut pas être recollé en une seule trace.
traceparent ajoute-t-il un surcoût mesurable ?
Pas de façon significative. Un traceparent fait 55 octets, et un tracestate en ajoute typiquement quelques centaines de plus, ce qui reste négligeable à côté d’une poignée de main TLS ou de n’importe quelle charge utile réelle. Le vrai coût du tracing tient à l’export et au stockage des spans échantillonnés, pas au transport des en-têtes de tracing distribué sur le fil.