Skip to content
Terug naar blog
Tutorials

traceparent-header uitgelegd: complete gids voor W3C Trace Context

De traceparent-header veld voor veld: wat elk hex-segment betekent, wanneer hij ongeldig is en waarom traces afbreken. Gratis online decoder.

13 min leestijd

traceparent-header uitgelegd: complete gids voor W3C Trace Context

De traceparent-header is de W3C-standaard voor distributed tracing: één regel ASCII die de identiteit van een request meedraagt langs elke dienst die het aanraakt. In de huidige versie is hij precies 55 tekens lang en bestaat hij uit vier velden, gescheiden door streepjes:

00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
│  │                                │                │
│  │                                │                └─ trace-flags (2 hex, 1 byte)
│  │                                └─ parent-id     (16 hex, 8 bytes)
│  └─ trace-id                                       (32 hex, 16 bytes)
└─ version                                           (2 hex, 1 byte)

Twee van die velden gedragen zich anders zodra het request onderweg is. De trace-id blijft op elke hop identiek: dat is de naam van het request, van de edge proxy tot en met de laatste databaseaanroep. De parent-id verandert juist bij elke hop, want die benoemt de span die jou aanriep, niet het request. Die twee door elkaar halen verklaart een flink deel van alle tickets in de trant van “mijn traces kloppen niet”.

Dat is de anatomie. Lastiger is wat de veldtabel je niet vertelt: waardoor een header ongeldig wordt, wat een spec-conforme ontvanger daarmee doet, en waar hij stilletjes verdwijnt tussen twee diensten die allebei beweren tracing te ondersteunen. Heb je een echte header voor je, plak hem dan in de gratis traceparent-decoder en lees mee; die splitst de velden, klapt de flags-byte bit voor bit uit en benoemt welke regel een kapotte header overtreedt.

De traceparent-header in één oogopslag

De traceparent-header is één HTTP-header die een distributed trace van dienst naar dienst meeneemt. Hij bevat vier hexadecimale velden, gescheiden door streepjes (version, trace-id, parent-id en trace-flags), en telt in de huidige versie precies 55 tekens. De trace-id benoemt het hele request; de parent-id wijst de span aan die jou aanriep.

VeldHex-tekensBytesWat het aanduidtVerandert per hop?
version21Welk formaat de rest volgt. Vandaag altijd 00Nee
trace-id3216Het hele request, van begin tot eindNee
parent-id168De aanroepende span (de span-ID van je aanroeper)Ja
trace-flags21Een 8-bits veld; bit 0 is sampledZelden

Tel bij die 52 hex-tekens drie streepjes op en je komt op 55 tekens. Dat getal is het onthouden waard, want een header met versie 00 en een afwijkende lengte is ongeldig, en lengte is het snelst met het blote oog te controleren.

Alles in de header is hexadecimaal in kleine letters. Niet “hexadecimaal, hoofdletterongevoelig” — kleine letters. De grammatica in de W3C Trace Context-aanbeveling laat 0-9 en a-f toe en verder niets. Daarom gooit een dienst stroomafwaarts een trace-ID in hoofdletters alsnog weg, hoe correct de waarde verder ook is.

Veld voor veld

Elk veld heeft zijn eigen breedte, zijn eigen ongeldige waarden en zijn eigen manier om mis te gaan.

version — waarom het niet altijd “gewoon 00” is

Vandaag is de versiebyte 00, en dat blijft voorlopig zo. Maar ff is uitdrukkelijk verboden: de specificatie reserveert die waarde als ongeldig, dus een header die met ff begint, is bij aankomst al dood, wat er verder ook achter staat.

De interessante regel gaat over versies die je nog nooit hebt gezien. Een implementatie die if (version !== '00') reject() doet, zit fout, en op een dure manier. De spec vraagt ontvangers om de header alsnog uit te lezen als de versie hoger is en de header minstens zo lang is als het bekende formaat: lees de velden die je herkent, tolereer extra data aan het eind en ga verder. Weiger je in plaats daarvan, dan wordt jouw dienst de grens waar de trace stopt en een nieuwe begint, zodra iemand stroomopwaarts upgradet.

// Wrong: makes your service the place traces go to die
if (version !== '00') throw new Error('bad traceparent');

// Right: parse the prefix you understand
if (version !== '00' && header.length >= 55) {
  // read version, trace-id, parent-id, trace-flags; ignore the rest
}

trace-id — 16 bytes, de identiteit van het hele request

Tweeëndertig hex-tekens in kleine letters, constant zolang de trace leeft. Welke dienst hem aan het begin ook genereerde, elke hop geeft hem ongewijzigd door. Zoek je in je observability-backend naar een trace, dan is dit de string die je plakt.

De trace-id moet 32 hex-tekens tellen en mag niet uit alleen nullen bestaan. 00000000000000000000000000000000 is geen “trace die nog geen data heeft”; de specificatie benoemt die waarde als ongeldig en verplicht ontvangers om de hele header te negeren. In de praktijk betekent een trace-ID van alleen nullen dat een SDK nooit geïnitialiseerd is, of dat een middleware een placeholder invult omdat er geen echte context was om door te geven.

Een trace-id is 128 bits breed, net zo breed als een UUID, en is toch geen UUID. Er zijn geen versiebits, geen variantbits, geen streepjes en geen enkele structuur: zestien ondoorzichtige bytes. Je kunt er geen v4 uit lezen, en omgekeerd is een UUID zonder streepjes ook niet automatisch een geldige trace-id, omdat de versie- en variantnibbles zorgen dat de willekeur niet uniform verdeeld is. Wil je zien wat een UUID binnen die 128 bits werkelijk vastlegt, dan loopt wat een UUID echt encodeert de indeling na, en toont de UUID-generator de versie- en variantbits op hun plek.

parent-id — 8 bytes, de span die jou aanriep

Zestien hex-tekens, bij elke hop opnieuw geschreven. De naam zorgt voor meer verwarring dan het veld verdient: de W3C-spec noemt het parent-id, OpenTelemetry noemt diezelfde 8 bytes een span-ID, en het is hetzelfde ding vanuit twee kanten bekeken. Vanuit jouw dienst gezien is het de ouder; vanuit de aanroeper gezien is het de ID van de span die hij zojuist voor het uitgaande request aanmaakte.

Roept dienst A dus dienst B aan, dan zet A zijn eigen span-ID in het parent-id-veld. B maakt vervolgens een child span, en als B dienst C aanroept, zet B daar zijn eigen span-ID neer. De trace-id blijft de hele weg onaangeroerd. Dat is het complete propagatiealgoritme.

Een parent-id van alleen nullen is net zo ongeldig, en om dezelfde reden: 0000000000000000 betekent dat de aanroeper geen echte span heeft meegegeven, en dan hoort de header weggegooid te worden in plaats van half gehonoreerd.

trace-flags — lijkt een boolean, is eigenlijk acht bits

Bijna elke header die je ooit ziet eindigt op 01, dus het is logisch om het veld als ja/nee te lezen. Het is een byte, en de bits zijn toegewezen:

  • bit 0, masker 0x01sampled
  • bit 1, masker 0x02random-trace-id, toegevoegd in Trace Context Level 2
  • bits 2–7 — gereserveerd; negeer ze bij ontvangst en zet ze op nul bij uitgaande requests

De combinaties op een rij:

HexBinairsampledrandom-trace-idGeldt flags === 0x01?
0000000000falsefalsefalse
0100000001truefalsetrue
0200000010falsetruefalse
0300000011truetruefalse ← de bug

Lees die laatste rij nog eens. Bij een trace met flags 03 staat de sampled-bit gewoon aan. Code die de hele byte met 01 vergelijkt, concludeert geruisloos dat de trace niet wordt vastgelegd, en alleen voor dat deel van het verkeer waar de Level 2-flag toevallig aan staat. Dat is een akelige storingsvorm, want het lijkt op een probleem met je sampling-percentage in plaats van op een fout in het uitlezen van de header.

const flags = parseInt(traceFlags, 16);

// Wrong: treats a bit field as an enumeration
const sampled = traceFlags === '01';

// Right
const sampled       = (flags & 0x01) !== 0;
const randomTraceId = (flags & 0x02) !== 0;

Wat beweert random-trace-id eigenlijk? Dat minstens de meest rechtse 7 bytes van de trace-id met uniforme willekeur zijn gegenereerd. Dat klinkt academisch tot je aan consistente sampling denkt: wil een stroomafwaarts systeem 1% van de traces bewaren en moet elke dienst onafhankelijk tot dezelfde 1% komen, dan kan het die bytes modulo iets nemen in plaats van eerst een hash van de ID te berekenen. De flag is de belofte van bovenstrooms dat dat veilig is.

Waardoor een traceparent ongeldig wordt

De kennisbanken van leveranciers slaan dit meestal over, terwijl het precies de vraag is waarmee mensen bij support aankloppen. Dit zijn alle afwijzingsgronden voor een header met versie 00:

SymptoomRegelGevolg
00-4BF92F35...-01De grammatica laat alleen kleine letters toeOngeldig — de waarde klopt, de header wordt geweigerd
ff-...Versie ff is door de spec verbodenOngeldig
trace-id is 00000000000000000000000000000000Een trace-id van alleen nullen is met naam als ongeldig aangemerktOngeldig
parent-id is 0000000000000000Een parent-id van alleen nullen is met naam als ongeldig aangemerktOngeldig
trace-id telt geen 32 hex-tekensVaste breedteOngeldig
parent-id telt geen 16 hex-tekensVaste breedteOngeldig
trace-flags telt geen 2 hex-tekensVaste breedteOngeldig
De header is niet precies 55 tekens bij versie 00Extra data aan het eind mag alleen onder een toekomstige versieOngeldig
Een teken buiten 0-9a-f en de streepjesNiet hexadecimaalOngeldig

Dan het gevolg:

Een spec-conforme ontvanger repareert een ongeldige traceparent-header niet en geeft hem ook niet door. Hij gooit de header weg en begint een gloednieuwe trace met een vers gegenereerde trace-id.

Wat je op je scherm ziet, is dus geen kapotte trace. Het zijn twee losgekoppelde korte traces: een die abrupt eindigt bij de dienst die de foute header verstuurde, en een die uit het niets lijkt te beginnen bij de dienst die hem ontving. Nergens staat een fout gemarkeerd. Los van elkaar zien beide traces er gezond uit. Mensen zijn halve middagen kwijt aan het zoeken naar de ontbrekende schakel, terwijl het antwoord is dat een middleware een hex-string naar hoofdletters heeft omgezet, of dat een met de hand gebouwde header 54 tekens lang bleek.

Lengte en hoofdlettergebruik zijn de twee foutvormen die je er niet uit staart. Plak de header in de decoder en die benoemt precies welke regel is overtreden, in plaats van dat jij tekens gaat tellen.

tracestate: de bijbehorende header die iedereen verkeerd doet

traceparent draagt de gestandaardiseerde identiteit. De tracestate-header draagt alles wat een leverancier daarnaast kwijt wil, als leden in de vorm key=value, gescheiden door komma’s:

tracestate: rojo=00f067aa0ba902b7,congo=t61rcWkgMzE

Een implementatie die een key niet herkent, moet die ongewijzigd doorgeven. Dat is het hele ontwerpdoel: leveranciers kunnen eigen state meeliften op een gestandaardiseerde trace zonder dat elke hop hem hoeft te begrijpen.

Die grammatica heeft wel tanden. Drie regels daarvan verklaren echte productiesymptomen.

32 list-members is een harde bovengrens. Dat is geen advies, dat is de grammatica: list = list-member 0*31( OWS "," OWS list-member ). Een tracestate met 33 leden is geen tracestate met één extra vermelding, het is een ongeldige header, en ontvangers mogen het geheel weggooien. Dit verklaart een symptoom dat anders op magie lijkt: leveranciersdata die aanwezig is aan de rand, aanwezig is na twee hops en bij hop vijf volledig verdwenen. Elke hop plakte er zijn eigen lid achteraan, de lijst ging over de 32 heen, en vanaf dat moment werd de hele header weggegooid in plaats van ingekort.

Waarden zijn 1 tot 256 tekens lang en mogen nooit leeg zijn. De productieregel voor de waarde eindigt op een verplicht niet-blanco teken, dus vendor= is geen “key zonder waarde”, maar een syntaxfout. Alleen printbare ASCII, en nooit een komma of een isgelijkteken binnen de waarde.

De grammatica voor keys is tussen Level 1 en Level 2 veranderd. Level 1 definieerde keys via een tenant@vendor-productie, waarin @ een structureel scheidingsteken was. Level 2 verving dat door een vlakke tekenklasse: een key begint met een kleine letter of een cijfer en gaat verder met a-z, 0-9, _, -, *, / en @. Onder Level 2 is @ een gewoon teken, mogen keys met een cijfer beginnen en is a@b@c een volstrekt legale key die de productie uit Level 1 zou afwijzen. Heb je een proxy die tegen Level 1 valideert en een dienst die Level 2-keys uitstuurt, dan accepteert de ene kant wat de andere afwijst, en verdwijnt de header bij precies één hop.

Nog twee regels om te kennen. Dubbele keys zijn zonder meer ongeldig. En zodra je de parent-id in de traceparent aanpast, moet je je eigen tracestate-vermelding vooraan in de lijst zetten, want de lijst staat op volgorde van meest recent. Sla je die stap over, dan blijft verouderde state van een leverancier staan op een plek waar een lezer hem als actueel behandelt.

Eén regel is juist vergevingsgezind: lege list-members zijn toegestaan. Verwijdert een middlebox een vermelding, dan laat hij de komma vaak staan, wat rojo=1,,congo=2 oplevert. De spec staat dat uitdrukkelijk toe, dus hoort een implementatie het lege lid over te slaan en door te gaan in plaats van de header ongeldig te verklaren. Het tracestate-overzicht in de decoder toont elk lid met zijn eigen validatie en een lopende telling tegen de limiet van 32 leden, wat meestal sneller gaat dan komma’s tellen.

Zo reizen distributed-tracing-headers: één request, vier hops

Volg één request door een edge proxy, een API-dienst en twee stroomafwaartse diensten:

Client
  │  (no traceparent — the edge is the root)

Edge proxy      generates trace-id 4bf9…4736, span 00f0…02b7
  │  traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

API service     reads it, creates span a1b2c3d4e5f60718
  │  traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-a1b2c3d4e5f60718-01

Orders service  reads it, creates span 9f8e7d6c5b4a3928
  │  traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-9f8e7d6c5b4a3928-01

Inventory service

Elke hop doet dezelfde drie dingen: de binnenkomende header lezen, bij elke uitgaande aanroep de parent-id vervangen door zijn eigen span-ID, en de trace-id en de flags ongewijzigd doorgeven. Is er helemaal geen binnenkomende header, zoals bij de client hierboven, dan is de ontvangende dienst de root: die genereert een trace-id en neemt de samplingbeslissing voor alles wat erna komt.

Je kunt met de hand een header injecteren om een keten van begin tot eind te testen:

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

Speel een uit productie opgevangen header af tegen staging en je ziet dezelfde trace-id opduiken in je backend. De cURL Command Generator stelt de opties voor je samen als je auth of een body toevoegt, en het curl-spiekbriefje behandelt de header- en verbose-opties die je tijdens het debuggen wilt hebben.

Wil je zien wat een dienst werkelijk ontving in plaats van wat jij denkt te hebben verstuurd, start dan een wegwerp-echoserver en laat één hop daarheen wijzen:

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

Doe daarna curl -H 'traceparent: …' http://127.0.0.1:8080/ en lees wat er aan de andere kant uit komt. De helft van alle onderzoeken naar “de proxy eet mijn header op” eindigt hier.

trace-flags sampled: een beslissing van bovenstrooms, geen ontvangstbewijs

Een sampled-bit van 1 in trace-flags betekent dat de bovenstroomse dienst besloten heeft deze trace vast te leggen. Het is geen garantie dat de data je backend heeft bereikt.

Head-based sampling neemt die beslissing bij de root, voordat er iets gebeurd is, en geeft die naar beneden door: goedkoop, consistent tussen diensten en blind — het kan niet weten dat het request op het punt stond te mislukken. Tail-based sampling buffert spans tot de trace klaar is en beslist dan pas, waardoor het elke trace met een fout kan bewaren, ten koste van spans die in het geheugen blijven staan en van de eis dat de spans van elke dienst in dezelfde collector belanden.

Bij tail-based sampling kan een trace bij elke hop met 01 binnenkomen en aan het eind alsnog worden weggegooid. Rate limits en exportquota kunnen hem net zo goed laten vallen. Dus 01 aan de rand en geen trace in de UI is niet per se een propagatiebug; kijk eerst naar de drop-metrics van de collector zelf voordat je de headers induikt.

Het omgekeerde geval is in de dagelijkse praktijk belangrijker. Zijn de binnenkomende flags 00, dan heeft de aanroeper zijn sampler uitgevoerd en besloten niets vast te leggen. Er is niets verkeerd geconfigureerd in jouw dienst, en je eigen sampler doorlichten is verspilde tijd; de vraag is welke bovenstroomse dienst tegen sampling beslist.

Omzetten tussen propagatieformaten

W3C Trace Context heeft gewonnen, maar genoeg systemen spreken nog iets ouders, en gateways vertalen daartussen. Dit is hetzelfde traceparent-voorbeeld in vier formaten:

FormaatHeader(s)Waarde voor ons voorbeeld
W3Ctraceparent00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
B3 singleb34bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-1
B3 multiX-B3-TraceId, X-B3-SpanId, X-B3-Sampled4bf92f3577b34da6a3ce929d0e0e4736, 00f067aa0ba902b7, 1
Datadogx-datadog-trace-id, x-datadog-parent-id, _dd.p.tid-tag11803532876627986230, 67667974448284343, 4bf92f3577b34da6
AWS X-RayX-Amzn-Trace-IdRoot=1-4bf92f35-77b34da6a3ce929d0e0e4736;Parent=00f067aa0ba902b7;Sampled=1

Datadog: de splitsing in hoge en lage 64 bits

De identifiers van Datadog dateren van vóór trace-ID’s van 128 bits, en juist in die compatibiliteitslaag gaan de meeste conversies mis. x-datadog-trace-id draagt de onderste 64 bits als decimale string. De bovenste 64 bits reizen apart mee, hexadecimaal, in de _dd.p.tid-tag, die zelf meelift in de x-datadog-tags-header.

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

De klassieke fout is om alle 128 bits naar één decimaal getal om te zetten:

BigInt('0x' + traceId).toString(10);
// 100985939111033328018442752961257817910 — matches nothing in the UI

Die waarde is rekenkundig niet fout. Het is de juiste decimale weergave van de verkeerde grootheid, en daarom overleeft hij de review om vervolgens geruisloos met nul traces overeen te komen.

De tweede valkuil is numerieke precisie. Een identifier van 64 bits overschrijdt Number.MAX_SAFE_INTEGER, oftewel 9007199254740991, dus elk codepad dat een trace-ID een JavaScript-number laat worden, verminkt de laatste cijfers. Houd trace-ID’s als string en grijp alleen naar BigInt als je echt moet rekenen; een ID dat zonder aanhalingstekens in JSON binnenkomt, is al beschadigd tegen de tijd dat jij hem ziet.

AWS X-Ray: de timestamp die er niet is

Een trace-ID van X-Ray ziet eruit als 1-{8 hex}-{24 hex}, waarbij de eerste 8 hex-tekens het aanmaakmoment in epoch-seconden zijn. Omzetten vanuit W3C is mechanisch werk:

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

Kijk naar die datum. De voorbeeldheader uit de specificatie decodeert naar mei 2010, wat overduidelijk onzin is, en dat is precies het punt. Een W3C-trace-id bevat geen timestamp. Zestien willekeurige bytes leveren moeiteloos een geloofwaardig ogende epoch op zodra je de eerste vier als één getal leest, en dat getal betekent niets tenzij de identifier werkelijk uit X-Ray komt. Een tijd uit een willekeurige trace-id decoderen is een willekeurig getal lezen en het geloven.

Komt de ID wél echt uit X-Ray, dan is de omrekening nuttig: plak die acht hex-tekens in de tool om Unix timestamps om te rekenen voor een leesbare datum, en de epoch-gids behandelt de valkuilen rond seconden versus milliseconden en tijdzones die daarna komen.

B3: de erfenis van Zipkin

B3 komt van Zipkin en is het formaat dat je in oudere service meshes tegenkomt. De vorm met één header is traceId-spanId-sampled, waarbij het sampled-veld 1 of 0 is in plaats van een hex-byte. De random-trace-id-bit uit Level 2 kan daar dus nergens heen en gaat bij de vertaling verloren. De vorm met meerdere headers verdeelt dezelfde waarden over X-B3-TraceId, X-B3-SpanId en X-B3-Sampled.

De historische rimpel zit in de breedte. Trace-ID’s in B3 mogen 64 bits zijn, oftewel 16 hex-tekens in plaats van 32. Een B3-ID van 64 bits naar W3C omzetten betekent links met nullen aanvullen tot 32 tekens; terug omzetten betekent beslissen of je afkapt. Links aanvullen is veilig, afkappen niet, want twee traces die alleen in hun hoge bytes verschillen, vallen dan samen tot één.

Waar traceparent in productie verdwijnt

Alles hierboven gaat ervan uit dat de header aankomt. Vaak is dat niet zo. Dit zijn de vier plekken waar hij verdwijnt.

De browser laat hem vallen bij cross-origin aanroepen

Symptoom: er zijn frontend-traces, er zijn backend-traces, en niets verbindt ze. Of het cross-origin request mislukt meteen met een CORS-fout.

Oorzaak: traceparent is een eigen header, dus het toevoegen ervan maakt het request niet-simpel en lokt een preflight-OPTIONS uit. Noemt het preflight-antwoord van de server de header niet in Access-Control-Allow-Headers, dan blokkeert de browser het echte request. Daarnaast weigert de browserinstrumentatie van OpenTelemetry trace-headers in cross-origin requests te injecteren tenzij je vertelt welke origins zijn toegestaan.

Oplossing: stuur op de server Access-Control-Allow-Headers: traceparent, tracestate terug bij de preflight. Zet in de browser-SDK propagateTraceHeaderCorsUrls op een patroon dat op je API-origins past. Je hebt allebei nodig; elk afzonderlijk laat je met hetzelfde symptoom zitten. Komt een preflight met een onverwachte status terug, leg die dan eerst naast het HTTP-statuscodes spiekbriefje voordat je aanneemt dat de header het probleem is.

Proxy’s, WAF’s en load balancers verwijderen onbekende headers

Symptoom: de header is aanwezig als je de dienst rechtstreeks met curl benadert, en afwezig als hetzelfde request via de gateway gaat.

Oorzaak: doorsturen op basis van een allowlist. Genoeg proxyconfiguraties, WAF-regelsets en managed load balancers geven alleen headers door die ze herkennen, en traceparent staat niet in de standaardlijst. Sommige meshes herschrijven de header bovendien: die genereren hun eigen trace-id en gooien de jouwe weg.

Oplossing: zoek de plek met de echoserver van hierboven: zet hem om beurten achter elke hop en kijk welke laag de header laat vallen. Sta traceparent en tracestate daarna expliciet toe in de doorstuurregels van die laag. Is de proxy nginx, bedenk dan dat het blok dat een route afhandelt bepaalt welke headers doorgaan, en dat dat blok niet altijd het blok is dat jij verwacht; de regels voor nginx location-prioriteit leggen uit waarom een headerconfiguratie volledig genegeerd kan lijken.

Message queues hebben geen HTTP-headers

Symptoom: de trace houdt op zodra een request een achtergrondtaak wordt.

Oorzaak: over die grens gaat geen HTTP-request, dus er is niets om de header op mee te sturen. Kafka heeft record headers, SQS heeft message attributes, en geen van beide wordt door HTTP-instrumentatie voor je ingevuld.

Oplossing: injecteer de context aan de producerkant in het bericht en haal hem er aan de consumerkant weer uit. Elke OpenTelemetry-SDK biedt inject en extract precies hiervoor, en het wire-formaat is dezelfde W3C-string; alleen de drager verandert van een verzameling HTTP-headers naar berichtmetadata. De documentatie over OpenTelemetry-propagators beschrijft de drager-interface per taal.

Hoofdlettergebruik, en wat HTTP/2 echt naar kleine letters omzet

Symptoom: verwarring in de code review over de vraag of Traceparent mag.

Oorzaak: twee losse regels lopen door elkaar. De namen van headers zijn in HTTP/1.1 hoofdletterongevoelig, en HTTP/2 eist dat ze in kleine letters over de lijn gaan. Dat gaat over de naam. Los daarvan moet het hexadecimale deel in de waarde van de header in kleine letters staan, omdat de W3C-grammatica dat voorschrijft, en geen enkele protocolversie lost dat voor je op.

Oplossing: stuur de naam als traceparent en zet de waarde nooit in hoofdletters. Een gateway die headernamen normaliseert, normaliseert je hex-tekens niet, en een trace-id in hoofdletters glijdt door elke transportlaag heen om pas te worden geweigerd door de applicatie die hem uiteindelijk uitleest.

Moet je een binnenkomende traceparent vertrouwen?

Een traceparent die van het publieke internet binnenkomt, is invoer die de gebruiker bepaalt: een string die een anonieme client heeft gekozen en die de meeste diensten zonder nadenken accepteren.

Dat levert drie concrete risico’s op. Bij trace splicing stuurt een aanvaller een elders opgevangen trace-id mee en naait hij zijn request in een bestaande trace, wat de grafiek vervuilt en interne timings kan blootleggen aan iedereen die die trace mag lezen. Wie 01 hard invult, forceert sampling bij elk request; een bescheiden vloed verandert dan in een flinke rekening voor data-ingestie of, erger, verdringt precies de traces die je nodig had. En één trace-id hergebruiken over requests van verschillende tenants koppelt records die je tooling daarna als één logische operatie behandelt.

De pragmatische houding is: accepteren aan de rand, maar niet vertrouwen. Valideer de grammatica en wijs misvormde headers af in plaats van ze naar binnen door te geven. Neem bij niet-geauthenticeerd verkeer je eigen samplingbeslissing opnieuw in plaats van de binnenkomende flag te honoreren, zodat geen enkele externe client je sampler op “altijd vastleggen” kan vastzetten. Bij geauthenticeerd verkeer is de beslissing van de aanroeper volgen meestal prima, want je weet wie hij is.

En behandel de trace-id als openbaar. Het is geen geheim en is dat ook nooit geweest: hij duikt op in logs, in foutpagina’s, in response-headers en in screenshots die in supporttickets worden geplakt. Encodeer er nooit een gebruikers-ID, een tenantnaam of iets anders betekenisvols in, en gebruik hem nooit als autorisatiesleutel. Het is een correlatie-identifier, en meer zou het nooit moeten zijn.

FAQ

Wat is het verschil tussen traceparent en tracestate?

traceparent draagt de gestandaardiseerde identiteit (trace-id, parent-id en de sampling-flags) en elke implementatie moet hem begrijpen. tracestate draagt state van een specifieke leverancier, die implementaties die hem niet kennen ongewijzigd doorgeven. De twee hangen samen: is de traceparent ongeldig, dan verplicht de spec je om ook de tracestate te negeren.

Waarom begint mijn trace halverwege de aanroepketen opnieuw?

Een trace begint bijna altijd opnieuw omdat één hop een header ontving die niet door de grammatica kwam, hem weggooide en een nieuwe trace-id genereerde. Hex in hoofdletters, een trace-id van alleen nullen en een header die niet precies 55 tekens telt, veroorzaken dit allemaal. Is de header wel goed gevormd, dan zijn de volgende verdachten een proxy die hem verwijdert en een cross-origin preflight die mislukt.

Moet ik CORS instellen om traceparent vanuit een browser te versturen?

Ja, CORS-configuratie is nodig. traceparent is een eigen header, dus het request wordt niet-simpel en er komt een preflight bij; de server moet traceparent in Access-Control-Allow-Headers noemen. De browserinstrumentatie van OpenTelemetry heeft daarnaast een ingestelde propagateTraceHeaderCorsUrls nodig, omdat die standaard geen trace-headers cross-origin injecteert.

Hoe geef je trace context door via Kafka of SQS?

Schrijf de traceparent-waarde aan de producerkant in een record header van Kafka of een message attribute van SQS, en lees hem aan de consumerkant terug om de context te herstellen. OpenTelemetry-SDK’s bieden hiervoor in elke taal inject en extract. Het formaat blijft gelijk; alleen de drager verschilt van een verzameling HTTP-headers.

Is een trace-ID veilig om te tonen in logs of responses?

Ja, een trace-ID is veilig om te tonen. Het is een willekeurige identifier zonder ingebakken identiteit en zonder autorisatiekracht. Hij koppelt wel records tussen systemen, dus encodeer er nooit een gebruikers-ID of tenantnaam in en accepteer hem nooit als bewijs van wat dan ook. Behandel hem als een openbare correlatiesleutel, dan kun je hem veilig in logs zetten, teruggeven en delen.

Wie genereert de traceparent-header?

De traceparent-header ontstaat bij de eerste dienst die een request zonder header afhandelt: meestal een edge proxy, een API-gateway of een browser-SDK. Die dienst wordt de root van de trace. Hij genereert de trace-id, maakt de eerste span aan en neemt de samplingbeslissing; elke hop daarna herschrijft alleen nog de parent-id.

Is de traceparent-header verplicht?

Nee, de traceparent-header is niet verplicht. Op protocolniveau is hij optioneel en een request zonder header is volstrekt geldig; de ontvangende dienst wordt dan gewoon de root van een nieuwe trace. Verplicht is hij alleen in praktische zin: zonder header valt werk aan weerszijden van een dienstgrens niet meer tot één trace samen te voegen.

Levert traceparent meetbare overhead op?

Niet noemenswaardig. Een traceparent is 55 bytes en een tracestate voegt er meestal een paar honderd aan toe, verwaarloosbaar naast een TLS-handshake of welke echte payload dan ook. De werkelijke kosten van tracing zitten in het exporteren en opslaan van de spans die je bewaart, niet in het meesturen van headers voor distributed tracing over de lijn.

Tags: distributed-tracing opentelemetry observability http-headers w3c

Gerelateerde artikelen

Alle artikelen bekijken