Skip to content
Zurück zum Blog
Tutorials

traceparent-Header erklärt: Leitfaden zu W3C Trace Context

Der traceparent-Header Feld für Feld: Bedeutung jedes Hex-Segments, Gründe für Ungültigkeit und warum Traces abreißen. Kostenloser Online-Decoder.

13 Min. Lesezeit

traceparent-Header erklärt: Leitfaden zu W3C Trace Context

Der traceparent-Header ist eine einzige ASCII-Zeile, die beim Distributed Tracing die Identität einer Anfrage durch jeden Dienst trägt, den sie berührt. In der aktuellen Version ist er exakt 55 Zeichen lang und besteht aus vier durch Bindestriche getrennten Feldern:

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

Zwei dieser Felder verhalten sich unterschiedlich, während die Anfrage weiterwandert. Die trace-id bleibt bei jedem Hop identisch; sie ist der Name der Anfrage, vom Edge-Proxy bis hinunter zum letzten Datenbankaufruf. Die parent-id ändert sich bei jedem Hop, denn sie benennt den Span, der Sie aufgerufen hat, nicht die Anfrage. Diese beiden zu verwechseln erklärt einen guten Teil aller Tickets nach dem Muster „meine Traces sehen falsch aus“.

So viel zur Anatomie. Schwieriger wird es bei dem, was die Feldtabelle nicht verrät: was einen Header ungültig macht, wie sich ein spezifikationstreuer Empfänger dann verhält und an welcher Stelle der Header zwischen zwei Diensten sang- und klanglos verschwindet, die beide behaupten, Tracing zu unterstützen. Wenn Sie einen echten Header vor sich haben, fügen Sie ihn in den kostenlosen traceparent-Dekodierer ein und lesen Sie parallel mit: Er zerlegt die Felder, klappt das Flags-Byte Bit für Bit auf und benennt die Regel, gegen die ein kaputter Header verstößt.

Der traceparent-Header auf einen Blick

Der traceparent-Header ist ein einzelner HTTP-Header, der einen verteilten Trace von Dienst zu Dienst weiterreicht. Vier durch Bindestriche getrennte Hexadezimalfelder stecken darin: version, trace-id, parent-id und trace-flags, zusammen 55 Zeichen unter der heutigen Version. Die trace-id benennt die gesamte Anfrage, die parent-id den Span, der Sie aufgerufen hat.

FeldHex-ZiffernByteWas es identifiziertÄndert sich pro Hop?
version21Welchem Format der Rest folgt. Heute immer 00Nein
trace-id3216Die gesamte Anfrage, von Anfang bis EndeNein
parent-id168Der aufrufende Span (die Span-ID Ihres Aufrufers)Ja
trace-flags21Ein 8-Bit-Feld; Bit 0 ist sampledSelten

Rechnen Sie zu diesen 52 Hex-Ziffern drei Bindestriche hinzu, kommen Sie auf 55 Zeichen. Merken Sie sich diese Zahl. Ein Header der Version 00 mit jeder anderen Länge ist ungültig, und die Länge sehen Sie mit bloßem Auge schneller als jeden anderen Defekt.

Alles im Header ist Hexadezimal in Kleinbuchstaben. Nicht „hexadezimal, Groß-/Kleinschreibung egal“, sondern Kleinbuchstaben. Die Grammatik in der W3C-Trace-Context-Empfehlung lässt 0-9 und a-f zu und sonst nichts. Deshalb verwirft ein Dienst weiter unten in der Kette eine Trace-ID in Großbuchstaben, auch wenn ihr Wert vollkommen korrekt ist.

Feld für Feld

Jedes Feld hat seine eigene Breite, seine eigenen ungültigen Werte und seine eigene Art, kaputtzugehen.

version – warum es nicht immer „einfach 00“ ist

Heute lautet das Versions-Byte 00, und das wird noch eine Weile so bleiben. ff ist allerdings ausdrücklich verboten: Die Spezifikation reserviert diesen Wert als ungültig, ein Header mit führendem ff ist also von vornherein tot, egal was danach kommt.

Interessant ist die Regel für Versionen, die Sie noch nie gesehen haben. Ein Parser mit if (version !== '00') reject() ist falsch, und zwar auf teure Weise. Die Spezifikation verlangt, dass Empfänger den Parse-Versuch unternehmen, wenn die Version höher und der Header mindestens so lang wie das bekannte Format ist: die Felder lesen, die Sie kennen, überzählige Daten am Ende tolerieren und weitermachen. Wer stattdessen ablehnt, macht seinen Dienst zu genau der Grenze, an der ein Trace endet und ein neuer beginnt, sobald irgendjemand weiter oben in der Kette aktualisiert.

// Falsch: Ihr Dienst wird zum Ort, an dem Traces sterben
if (version !== '00') throw new Error('bad traceparent');

// Richtig: das Präfix parsen, das Sie verstehen
if (version !== '00' && header.length >= 55) {
  // version, trace-id, parent-id, trace-flags lesen; den Rest ignorieren
}

trace-id – 16 Byte, die Identität der gesamten Anfrage

Zweiunddreißig Hex-Ziffern in Kleinbuchstaben, konstant über die gesamte Lebensdauer des Trace. Welcher Dienst sie am Anfang erzeugt hat, spielt keine Rolle: Jeder Hop reicht sie unverändert weiter. Wenn Sie im Observability-Backend nach einem Trace suchen, ist das die Zeichenkette, die Sie einfügen.

Zwei Regeln bestimmen den Wert. Er muss aus 32 Hex-Ziffern bestehen, und er darf nicht ausschließlich aus Nullen bestehen. 00000000000000000000000000000000 bedeutet nicht „ein Trace, der noch keine Daten hat“. Die Spezifikation führt diesen Wert als ungültig auf und verlangt, dass Empfänger den kompletten Header ignorieren. In der Praxis steht eine Trace-ID aus lauter Nullen für ein SDK, das nie initialisiert wurde, oder für eine Middleware, die einen Platzhalter einsetzt, weil sie keinen echten Kontext weiterzureichen hatte.

Eine trace-id ist 128 Bit breit, genau wie eine UUID, und trotzdem ist sie keine UUID. Es gibt keine Versions-Bits, keine Varianten-Bits, keine Bindestriche und überhaupt keine Struktur: sechzehn undurchsichtige Byte. Sie können daraus keine v4 herauslesen, und umgekehrt ist eine UUID ohne ihre Bindestriche nicht automatisch eine gültige trace-id, weil die Versions- und Varianten-Nibbles ihre Zufälligkeit ungleichmäßig machen. Wenn Sie sehen möchten, was eine UUID innerhalb dieser 128 Bit tatsächlich reserviert: Was eine UUID wirklich kodiert geht das Layout durch, und der UUID-Generator zeigt die Versions- und Varianten-Bits an Ort und Stelle.

parent-id – 8 Byte, der Span, der Sie aufgerufen hat

Sechzehn Hex-Ziffern, bei jedem Hop neu geschrieben. Der Name stiftet mehr Verwirrung, als das Feld verdient: Die W3C-Spezifikation nennt es parent-id, OpenTelemetry nennt dieselben 8 Byte eine Span-ID, und beides ist dieselbe Sache aus zwei Blickrichtungen. Aus Sicht Ihres Dienstes ist es der Elternteil; aus Sicht des Aufrufers ist es die ID des Spans, den er gerade für die ausgehende Anfrage erzeugt hat.

Wenn Dienst A also Dienst B aufruft, schreibt A seine eigene Span-ID in das parent-id-Feld. B erzeugt daraufhin einen Kind-Span, und wenn B den Dienst C aufruft, steht dort stattdessen die Span-ID von B. Die trace-id bleibt durchgehend unangetastet. Das ist der komplette Weitergabe-Algorithmus.

parent-ids aus lauter Nullen sind ebenfalls ungültig, aus demselben Grund wie bei trace-ids: 0000000000000000 heißt, dass der Aufrufer keinen echten Span mitgegeben hat, und der Header sollte verworfen statt halb beachtet werden.

trace-flags – sehen aus wie ein Boolean, sind in Wahrheit acht Bit

Fast jeder Header, der Ihnen begegnet, endet auf 01, deshalb liest man das Feld ganz selbstverständlich als Ja/Nein. Es ist ein Byte, und die Bits sind fest vergeben:

  • Bit 0, Maske 0x01: sampled
  • Bit 1, Maske 0x02: random-trace-id, ergänzt in Trace Context Level 2
  • Bit 2–7: reserviert; beim Empfang ignorieren, bei ausgehenden Anfragen auf null setzen

So dekodieren sich die Kombinationen:

HexBinärsampledrandom-trace-idGilt flags === 0x01?
0000000000falsefalsefalse
0100000001truefalsetrue
0200000010falsetruefalse
0300000011truetruefalse ← hier steckt der Bug

Lesen Sie die letzte Zeile noch einmal. Ein Trace mit den Flags 03 ist gesampelt. Jeder Code, der das ganze Byte mit 01 vergleicht, meldet ihn als nicht gesampelt – stillschweigend und nur für den Teil des Verkehrs, bei dem das Level-2-Flag zufällig gesetzt ist. Schlimmer kann ein Fehler kaum sein, denn er wirkt wie ein Problem mit der Sampling-Rate und nicht wie ein Parser-Bug.

const flags = parseInt(traceFlags, 16);

// Falsch: behandelt ein Bitfeld wie eine Aufzählung
const sampled = traceFlags === '01';

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

Was genau sichert random-trace-id zu? Dass mindestens die rechten 7 Byte der trace-id mit gleichverteiltem Zufall erzeugt wurden. Das klingt akademisch, bis konsistentes Sampling ins Spiel kommt: Wenn ein nachgelagertes System 1 % der Traces behalten will und jeder Dienst unabhängig zum selben 1 % kommen muss, kann es diese Byte modulo irgendetwas rechnen, statt die ID vorher zu hashen. Das Flag ist das Versprechen des vorgelagerten Dienstes, dass genau das gefahrlos möglich ist.

Was einen traceparent ungültig macht

Die Wissensdatenbanken der Anbieter lassen diesen Teil meist aus, obwohl die Leute genau damit ankommen. Für einen Header der Version 00 sieht die vollständige Ablehnungsliste so aus:

SymptomRegelErgebnis
00-4BF92F35...-01Die Grammatik lässt nur Hex in Kleinbuchstaben zuUngültig – der Wert stimmt, der Header wird verworfen
ff-...Version ff ist laut Spezifikation verbotenUngültig
trace-id ist 00000000000000000000000000000000Eine trace-id aus lauter Nullen ist ein benannter ungültiger WertUngültig
parent-id ist 0000000000000000Eine parent-id aus lauter Nullen ist ein benannter ungültiger WertUngültig
trace-id hat nicht 32 Hex-ZiffernFeste BreiteUngültig
parent-id hat nicht 16 Hex-ZiffernFeste BreiteUngültig
trace-flags hat nicht 2 Hex-ZiffernFeste BreiteUngültig
Header ist bei Version 00 nicht exakt 55 Zeichen langZusätzliche Daten am Ende sind nur unter einer künftigen Version erlaubtUngültig
Irgendein Zeichen außerhalb von 0-9a-f und den BindestrichenNicht hexadezimalUngültig

Die Konsequenz daraus:

Ein spezifikationstreuer Empfänger repariert einen ungültigen traceparent-Header nicht und reicht ihn auch nicht weiter. Er verwirft den Header und startet einen völlig neuen Trace mit frisch erzeugter trace-id.

Das Symptom auf Ihrem Bildschirm ist demnach kein kaputter Trace. Es sind zwei voneinander getrennte kurze Traces: einer, der abrupt bei dem Dienst endet, der den fehlerhaften Header verschickt hat, und einer, der beim empfangenden Dienst scheinbar aus dem Nichts beginnt. Nirgendwo wird etwas als Fehler markiert. Für sich betrachtet sehen beide Traces gesund aus. Ganze Nachmittage gehen für die Suche nach dem fehlenden Bindeglied drauf, während die Antwort lautet: Eine Middleware hat eine Hex-Zeichenkette in Großbuchstaben umgewandelt, oder ein von Hand gebauter Header ist 54 Zeichen lang geworden.

Länge und Groß-/Kleinschreibung sind die beiden Fehlerbilder, die man durch bloßes Hinsehen nicht erkennt. Fügen Sie den Header in den Dekodierer ein: Er nennt die genau verletzte Regel, statt Sie Ziffern zählen zu lassen.

tracestate: der Begleit-Header, den alle falsch machen

traceparent trägt die standardisierte Identität. Der Header tracestate trägt alles, was ein Anbieter zusätzlich mitgeben möchte, als kommagetrennte key=value-Mitglieder:

tracestate: rojo=00f067aa0ba902b7,congo=t61rcWkgMzE

Eine Implementierung, die einen Schlüssel nicht kennt, muss ihn unverändert weiterreichen. Genau das ist das Entwurfsziel: Anbieter können proprietären Zustand auf einem standardisierten Trace mitreisen lassen, ohne dass jeder Hop ihn verstehen muss.

Die Grammatik hat allerdings Zähne, und drei ihrer Regeln erklären echte Symptome im Produktivbetrieb.

32 Listenmitglieder sind eine harte Obergrenze. Das ist kein Ratschlag, das ist die Grammatik: list = list-member 0*31( OWS "," OWS list-member ). Ein tracestate mit 33 Mitgliedern ist kein tracestate mit einem Eintrag zu viel, sondern ein ungültiger Header, und Empfänger dürfen das Ganze verwerfen. Das erklärt ein Symptom, das sonst nach Zauberei aussieht: Anbieterdaten, die am Edge vorhanden sind, zwei Hops später immer noch vorhanden sind und ab Hop fünf vollständig fehlen. Jeder Hop hat sein eigenes Mitglied angehängt, die Liste hat 32 überschritten, und von da an wurde der komplette Header verworfen statt gekürzt.

Werte sind 1 bis 256 Zeichen lang und dürfen nie leer sein. Die Produktionsregel für den Wert endet mit einem verpflichtenden Zeichen, das kein Leerzeichen ist. vendor= ist deshalb nicht „ein Schlüssel ohne Wert“, sondern ein Syntaxfehler. Nur druckbares ASCII, und niemals ein Komma oder ein Gleichheitszeichen innerhalb des Werts.

Auch die Grammatik der Schlüssel hat sich zwischen Level 1 und Level 2 geändert. Level 1 definierte Schlüssel über eine tenant@vendor-Produktionsregel, in der @ ein strukturelles Trennzeichen war. Level 2 ersetzt das durch eine flache Zeichenklasse: Ein Schlüssel beginnt mit einem Kleinbuchstaben oder einer Ziffer und setzt sich fort mit a-z, 0-9, _, -, *, / und @. Unter Level 2 ist @ ein gewöhnliches Zeichen, Schlüssel dürfen mit einer Ziffer anfangen, und a@b@c ist ein vollkommen legaler Schlüssel, den die Regel aus Level 1 ablehnen würde. Wenn ein Proxy gegen Level 1 validiert und ein Dienst Level-2-Schlüssel erzeugt, akzeptiert die eine Seite, was die andere ablehnt – und der Header verschwindet an genau einem Hop.

Zwei weitere Regeln fallen in der Praxis auf. Doppelte Schlüssel sind schlicht ungültig. Und wenn Sie die parent-id im traceparent ändern, müssen Sie Ihren eigenen tracestate-Eintrag an den Anfang der Liste verschieben, denn die Liste ist nach Aktualität sortiert, das Jüngste zuerst. Wer diesen Schritt auslässt, hinterlässt veralteten Anbieterzustand an einer Stelle, an der ein Leser ihn für aktuell hält.

Eine Regel arbeitet ausnahmsweise für Sie: Leere Listenmitglieder sind erlaubt. Wenn eine Middlebox einen Eintrag entfernt, bleibt oft das Komma stehen, und heraus kommt rojo=1,,congo=2. Die Spezifikation lässt das ausdrücklich zu, ein Parser sollte das leere Mitglied also überspringen und weitermachen, statt den Header für fehlerhaft zu erklären. Die tracestate-Ansicht im Dekodierer listet jedes Mitglied mit eigener Validierung und einem laufenden Zähler gegen das Limit von 32 Mitgliedern auf – meist schneller, als Kommas zu zählen.

Wie Distributed-Tracing-Header reisen: eine Anfrage, vier Hops

Verfolgen Sie eine Anfrage durch einen Edge-Proxy, einen API-Dienst und zwei nachgelagerte Dienste:

Client
  │  (kein traceparent – der Edge ist die Wurzel)

Edge-Proxy      erzeugt trace-id 4bf9…4736, Span 00f0…02b7
  │  traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

API-Dienst      liest ihn, erzeugt Span a1b2c3d4e5f60718
  │  traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-a1b2c3d4e5f60718-01

Bestelldienst   liest ihn, erzeugt Span 9f8e7d6c5b4a3928
  │  traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-9f8e7d6c5b4a3928-01

Lagerdienst

Jeder Hop tut dieselben drei Dinge: den eingehenden Header lesen, die parent-id für jeden ausgehenden Aufruf durch die eigene Span-ID ersetzen und trace-id sowie Flags unverändert weiterreichen. Fehlt ein eingehender Header vollständig – wie beim Client oben –, ist der empfangende Dienst die Wurzel: Er erzeugt eine trace-id und trifft die Sampling-Entscheidung für alles, was danach kommt.

Sie können einen Header von Hand einschleusen, um eine Kette durchgängig zu 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

Spielen Sie einen in der Produktion aufgezeichneten Header gegen Staging ab, und Sie sehen dieselbe trace-id in Ihrem Backend auftauchen. Der cURL-Befehlsgenerator stellt die Optionen für Sie zusammen, wenn Authentifizierung oder ein Body dazukommen, und der curl-Spickzettel behandelt die Header- und Verbose-Optionen, die Sie beim Debuggen brauchen.

Um zu sehen, was ein Dienst tatsächlich empfangen hat statt dessen, was Sie zu senden glauben, starten Sie einen Wegwerf-Echo-Server und richten einen Hop darauf aus:

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

Danach curl -H 'traceparent: …' http://127.0.0.1:8080/ aufrufen und lesen, was auf der anderen Seite herauskommt. Viele Untersuchungen nach dem Motto „der Proxy frisst meinen Header“ enden genau hier.

trace-flags sampled: eine Entscheidung von weiter oben, keine Quittung

Ein sampled-Bit von 1 in den trace-flags bedeutet, dass der vorgelagerte Dienst entschieden hat, diesen Trace aufzuzeichnen. Es sagt nicht zu, dass die Daten Ihr Backend erreicht haben.

Head-based Sampling trifft diese Entscheidung an der Wurzel, bevor irgendetwas passiert ist, und gibt sie nach unten weiter: günstig, über alle Dienste hinweg konsistent und blind, denn es kann nicht wissen, dass die Anfrage gleich scheitern wird. Tail-based Sampling puffert die Spans, bis der Trace abgeschlossen ist, und entscheidet dann. Es kann jeden Trace behalten, der einen Fehler enthält – zum Preis von Spans im Arbeitsspeicher und der Anforderung, dass die Spans aller Dienste im selben Collector landen.

Unter Tail-based Sampling kann ein Trace an jedem Hop mit 01 markiert ankommen und am Ende trotzdem verworfen werden. Auch Rate Limits und Export-Kontingente können ihn verwerfen. 01 am Edge und kein Trace in der Oberfläche ist also nicht zwangsläufig ein Weitergabe-Fehler; prüfen Sie die Drop-Metriken des Collectors, bevor Sie sich die Header vornehmen.

Der umgekehrte Fall ist im Alltag wichtiger. Stehen die eingehenden Flags auf 00, hat der Aufrufer seinen Sampler laufen lassen und sich gegen die Aufzeichnung entschieden. In Ihrem Dienst ist nichts falsch konfiguriert, und den eigenen Sampler zu prüfen ist verlorene Zeit. Die Frage lautet, welcher vorgelagerte Dienst sich gegen das Sampling entscheidet.

Umrechnen zwischen Propagationsformaten

W3C Trace Context hat sich durchgesetzt, aber viele Systeme sprechen noch etwas Älteres, und Gateways übersetzen dazwischen. Dasselbe traceparent-Beispiel, geschrieben in vier Formaten:

FormatHeaderWert für unser Beispiel
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: die Aufteilung in obere und untere 64 Bit

Die Bezeichner von Datadog sind älter als 128-Bit-Trace-IDs, und die Kompatibilitätsschicht ist die Stelle, an der die meisten Umrechnungen schiefgehen. x-datadog-trace-id trägt die unteren 64 Bit als Dezimalzeichenkette. Die oberen 64 Bit reisen getrennt, hexadezimal, im Tag _dd.p.tid, das seinerseits im Header x-datadog-tags mitreist.

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

Der klassische Fehler besteht darin, alle 128 Bit in eine einzige Dezimalzahl umzuwandeln:

BigInt('0x' + traceId).toString(10);
// 100985939111033328018442752961257817910 – passt zu nichts in der Oberfläche

Dieser Wert ist keine falsche Rechnung. Er ist die korrekte Dezimaldarstellung der falschen Größe – deshalb übersteht er das Review und trifft danach klammheimlich null Traces.

Die zweite Falle ist die Zahlengenauigkeit. Ein 64-Bit-Bezeichner überschreitet Number.MAX_SAFE_INTEGER, also 9007199254740991. Jeder Codepfad, der eine Trace-ID zu einer JavaScript-Zahl werden lässt, beschädigt daher ihre niedrigen Stellen. Halten Sie Trace-IDs als Zeichenketten und greifen Sie nur dann zu BigInt, wenn Sie wirklich rechnen müssen; eine ID, die ohne Anführungszeichen im JSON ankommt, ist bereits beschädigt, bevor Sie sie zu Gesicht bekommen.

AWS X-Ray: der Zeitstempel, der keiner ist

Eine X-Ray-Trace-ID sieht aus wie 1-{8 hex}-{24 hex}, und die führenden 8 Hex-Ziffern sind der Erstellungszeitpunkt in Epoch-Sekunden. Die Umrechnung aus W3C ist rein mechanisch:

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

Schauen Sie sich dieses Datum an. Der Beispiel-Header aus der Spezifikation dekodiert zu Mai 2010, was offensichtlich Unsinn ist – und genau das ist der Punkt. Eine W3C-trace-id enthält keinen Zeitstempel. Sechzehn zufällige Byte liefern bereitwillig einen plausibel aussehenden Epoch-Wert, wenn Sie die ersten vier davon als eine Zahl lesen, und dieser Wert bedeutet nichts, solange der Bezeichner nicht wirklich aus X-Ray stammt. Eine Zeit aus einer beliebigen trace-id zu dekodieren heißt, eine Zufallszahl abzulesen und ihr zu glauben.

Stammt die ID tatsächlich aus X-Ray, ist die Umrechnung nützlich: Werfen Sie diese acht Hex-Ziffern in den Unix-Zeitstempel-Konverter, um ein lesbares Datum zu erhalten, und der Epoch-Leitfaden behandelt die anschließenden Fallstricke rund um Sekunden gegen Millisekunden und Zeitzonen.

B3: das Erbe von Zipkin

B3 stammt von Zipkin und ist das Format, das Ihnen in älteren Service-Meshes begegnet. Die Variante mit einem einzigen Header lautet traceId-spanId-sampled, wobei das sampled-Feld 1 oder 0 ist statt eines Hex-Bytes. Das Level-2-Bit random-trace-id hat dort also keinen Platz und geht bei der Übersetzung schlicht verloren. Die Mehr-Header-Variante verteilt dieselben Werte auf X-B3-TraceId, X-B3-SpanId und X-B3-Sampled.

Die historische Eigenheit ist die Breite. B3-Trace-IDs können 64 Bit haben, also 16 statt 32 Hex-Ziffern. Eine 64-Bit-B3-ID nach W3C umzurechnen heißt, links mit Nullen auf 32 Ziffern aufzufüllen; der Weg zurück heißt, über das Abschneiden zu entscheiden. Das Auffüllen ist ungefährlich, das Abschneiden nicht: Zwei Traces, die sich nur in ihren oberen Byte unterscheiden, fallen dabei zu einem zusammen.

Wo traceparent in der Produktion verloren geht

Alles bisher Gesagte setzt voraus, dass der Header ankommt. Oft tut er das nicht, und meistens liegt es an einer dieser vier Stellen.

Der Browser verwirft ihn bei Cross-Origin-Aufrufen

Symptom: Frontend-Traces existieren, Backend-Traces existieren, und nichts verbindet sie. Oder die Cross-Origin-Anfrage scheitert gleich ganz mit einem CORS-Fehler.

Ursache: traceparent ist ein eigener Header. Ihn hinzuzufügen macht die Anfrage im Sinne von CORS „nicht einfach“ und löst einen Preflight per OPTIONS aus. Führt die Preflight-Antwort des Servers den Header nicht in Access-Control-Allow-Headers auf, blockiert der Browser die eigentliche Anfrage. Unabhängig davon weigert sich die Browser-Instrumentierung von OpenTelemetry, Trace-Header in Cross-Origin-Anfragen einzufügen, solange Sie ihr nicht sagen, welche Origins erlaubt sind.

Lösung: Geben Sie serverseitig für den Preflight Access-Control-Allow-Headers: traceparent, tracestate zurück. Setzen Sie im Browser-SDK propagateTraceHeaderCorsUrls auf ein Muster, das zu den Origins Ihrer API passt. Beides ist nötig; eines allein lässt dasselbe Symptom bestehen. Kommt ein Preflight mit einem unerwarteten Status zurück, lohnt ein Blick in den Spickzettel zu HTTP-Statuscodes, bevor Sie den Header verdächtigen.

Proxys, WAFs und Load Balancer entfernen unbekannte Header

Symptom: Der Header ist da, wenn Sie den Dienst direkt per curl ansprechen, und fehlt, wenn dieselbe Anfrage über das Gateway läuft.

Ursache: Weiterleitung nach Positivliste. Viele Proxy-Konfigurationen, WAF-Regelwerke und verwaltete Load Balancer reichen nur Header weiter, die sie kennen, und traceparent steht nicht auf der Standardliste. Manche Meshes überschreiben den Header außerdem, erzeugen eine eigene trace-id und verwerfen Ihre.

Lösung: Grenzen Sie das Problem mit dem Echo-Server von vorhin ein: Hängen Sie ihn nacheinander hinter jeden Hop und sehen Sie, welche Schicht den Header verliert. Erlauben Sie danach traceparent und tracestate ausdrücklich in den Weiterleitungsregeln dieser Schicht. Läuft als Proxy nginx, denken Sie daran: Der Block, der eine Route bearbeitet, entscheidet, welche Header er durchreicht – und welcher Block eine Route bearbeitet, ist nicht immer der, den Sie erwarten. Die Regeln zur nginx-location-Priorität erklären, warum eine Header-Konfiguration vollständig ignoriert wirken kann.

Message Queues haben keine HTTP-Header

Symptom: Der Trace endet in dem Moment, in dem aus einer Anfrage ein Hintergrund-Job wird.

Ursache: Über diese Grenze hinweg gibt es keine HTTP-Anfrage, also auch nichts, worauf der Header mitreisen könnte. Kafka hat Record-Header, SQS hat Message Attributes, und die HTTP-Instrumentierung befüllt keines von beiden für Sie.

Lösung: Schleusen Sie den Kontext auf der Producer-Seite in die Nachricht ein und lesen Sie ihn auf der Consumer-Seite wieder aus. Jedes OpenTelemetry-SDK bietet genau dafür inject und extract, und das Format auf der Leitung ist dieselbe W3C-Zeichenkette; nur der Träger wechselt von einer HTTP-Header-Map zu Nachrichten-Metadaten. Die OpenTelemetry-Dokumentation zu Propagators beschreibt die Carrier-Schnittstelle je Sprache.

Groß-/Kleinschreibung – und was HTTP/2 wirklich kleinschreibt

Symptom: Verwirrung im Code-Review darüber, ob Traceparent zulässig ist.

Ursache: Zwei getrennte Regeln verschmelzen zu einer. Header-Namen sind in HTTP/1.1 unabhängig von der Groß-/Kleinschreibung, und HTTP/2 verlangt, dass sie auf der Leitung klein kodiert werden. Das betrifft den Namen. Davon unabhängig muss das Hexadezimal im Wert des Headers kleingeschrieben sein, weil die W3C-Grammatik es so vorschreibt – und keine Protokollversion nimmt Ihnen das ab.

Lösung: Senden Sie den Namen als traceparent und schreiben Sie den Wert nie groß. Ein Gateway, das Header-Namen normalisiert, normalisiert Ihre Hex-Ziffern nicht: Eine trace-id in Großbuchstaben segelt durch jede Transportschicht und scheitert erst an der Anwendung, die sie am Ende parst.

Sollten Sie einem eingehenden traceparent vertrauen?

Ein traceparent, der aus dem öffentlichen Internet eintrifft, ist benutzergesteuerte Eingabe – eine Zeichenkette, die ein anonymer Client gewählt hat und die die meisten Dienste ohne weiteres Nachdenken übernehmen.

Daraus folgen drei konkrete Risiken. Erstens Trace-Splicing: Wer eine anderswo beobachtete trace-id einsendet, bekommt seine Anfrage in einen bestehenden Trace eingenäht. Das verschmutzt den Graphen und kann interne Zeitmessungen an jeden preisgeben, der diesen Trace lesen darf. Zweitens das verbrannte Kontingent: Ein fest verdrahtetes 01 erzwingt Sampling bei jeder Anfrage, und aus einer überschaubaren Flut wird eine sehr große Rechnung für die Datenaufnahme. Schlimmstenfalls verdrängt sie genau die Traces, die Sie wirklich gebraucht hätten. Drittens Korrelation über Mandantengrenzen hinweg: Wird eine trace-id über Anfragen verschiedener Mandanten hinweg wiederverwendet, verknüpft das Datensätze, die Ihre Werkzeuge anschließend als einen logischen Vorgang behandeln.

Die pragmatische Haltung lautet: am Edge annehmen, aber nicht vertrauen. Validieren Sie die Grammatik und weisen Sie fehlerhafte Header ab, statt sie nach innen durchzureichen. Führen Sie bei nicht authentifiziertem Verkehr Ihre eigene Sampling-Entscheidung erneut aus, statt das eingehende Flag zu übernehmen, damit kein externer Client Ihren Sampler auf „immer aufzeichnen“ festnageln kann. Bei authentifiziertem Verkehr ist es meist unproblematisch, der Entscheidung des Aufrufers zu folgen, weil Sie wissen, wer er ist.

Und behandeln Sie die trace-id als öffentlich. Sie ist kein Geheimnis und war nie eines: Sie taucht in Logs auf, auf Fehlerseiten, in Antwort-Headern und in Screenshots, die in Support-Tickets landen. Kodieren Sie niemals eine Benutzer-ID, einen Mandantennamen oder sonst etwas Aussagekräftiges hinein, und verwenden Sie sie niemals als Autorisierungsschlüssel. Sie ist ein Korrelationsbezeichner, und mehr sollte sie nie sein.

FAQ

Was ist der Unterschied zwischen traceparent und tracestate?

traceparent trägt die standardisierte Identität – trace-id, parent-id und die Sampling-Flags – und jede Implementierung muss ihn verstehen. tracestate trägt anbieterspezifischen Zustand, den fremde Implementierungen unverändert weiterreichen. Beide hängen zusammen: Ist der traceparent ungültig, verlangt die Spezifikation, dass auch der tracestate ignoriert wird.

Warum beginnt mein Trace mitten in der Aufrufkette von vorn?

Ein Trace beginnt mitten in der Aufrufkette fast immer deshalb von vorn, weil ein Hop einen Header erhalten hat, der an der Grammatik gescheitert ist, ihn verworfen und eine frische trace-id erzeugt hat. Hex in Großbuchstaben, eine trace-id aus lauter Nullen und ein Header, der nicht exakt 55 Zeichen lang ist, lösen das alle aus. Ist der Header wohlgeformt, sind die nächsten Verdächtigen ein Proxy, der ihn entfernt, und ein fehlgeschlagener Cross-Origin-Preflight.

Muss ich CORS konfigurieren, um traceparent aus dem Browser zu senden?

Ja, CORS muss konfiguriert werden. traceparent ist ein eigener Header, damit ist die Anfrage nicht mehr „einfach“ und löst einen Preflight aus; der Server muss traceparent in Access-Control-Allow-Headers aufführen. Die Browser-Instrumentierung von OpenTelemetry braucht zusätzlich ein konfiguriertes propagateTraceHeaderCorsUrls, weil sie Trace-Header standardmäßig nicht über Origin-Grenzen hinweg einfügt.

Wie gebe ich Trace-Kontext über Kafka oder SQS weiter?

Schreiben Sie den traceparent-Wert auf der Producer-Seite in einen Kafka-Record-Header oder ein SQS-Message-Attribute und lesen Sie ihn auf der Consumer-Seite wieder aus, um den Kontext wiederherzustellen. OpenTelemetry-SDKs bieten dafür in jeder Sprache inject und extract. Das Format bleibt unverändert; nur der Träger unterscheidet sich von einer HTTP-Header-Map.

Ist es sicher, eine Trace-ID in Logs oder Antworten preiszugeben?

Ja, eine Trace-ID können Sie bedenkenlos preisgeben. Sie ist ein zufälliger Bezeichner ohne eingebettete Identität und ohne Autorisierungswirkung. Sie korreliert allerdings Datensätze über Systemgrenzen hinweg, kodieren Sie also nie eine Benutzer-ID oder einen Mandantennamen hinein und akzeptieren Sie sie nie als Nachweis für irgendetwas. Behandeln Sie sie als öffentlichen Korrelationsschlüssel, dann können Sie sie gefahrlos loggen, zurückgeben und weitergeben.

Wer erzeugt den traceparent-Header?

Den traceparent-Header erzeugt der erste Dienst, der eine Anfrage ohne ihn bearbeitet: meist ein Edge-Proxy, ein API-Gateway oder ein Browser-SDK. Dieser Dienst wird zur Wurzel des Trace. Er erzeugt die trace-id, legt den ersten Span an und trifft die Sampling-Entscheidung; jeder Hop danach schreibt nur noch die parent-id neu.

Ist der traceparent-Header Pflicht?

Nein, der traceparent-Header ist nicht vorgeschrieben. Auf Protokollebene ist er optional, und eine Anfrage ohne ihn ist völlig gültig: Der empfangende Dienst wird dann eben die Wurzel eines neuen Trace. Pflicht ist er nur in dem praktischen Sinn, dass sich Arbeit über Dienstgrenzen hinweg ohne ihn nicht zu einem Trace zusammenführen lässt.

Verursacht traceparent messbaren Mehraufwand?

Nicht nennenswert. Ein traceparent ist 55 Byte groß, ein tracestate kommt typischerweise auf ein paar hundert weitere – vernachlässigbar neben einem TLS-Handshake oder irgendeiner echten Nutzlast. Die eigentlichen Kosten des Tracings entstehen beim Exportieren und Speichern gesampelter Spans, nicht beim Transport von Headern für Distributed Tracing über die Leitung.

Tags: distributed-tracing opentelemetry observability http-headers w3c

Verwandte Artikel

Alle Artikel anzeigen