Skip to content
Powrót do bloga
Poradniki

Nagłówek traceparent: kompletny przewodnik po W3C Trace Context

Nagłówek traceparent pole po polu: znaczenie każdego segmentu szesnastkowego, przyczyny nieważności i powody zrywania śladów. Darmowy dekoder online.

13 min czytania

Nagłówek traceparent: kompletny przewodnik po W3C Trace Context

Nagłówek traceparent to standard W3C dla nagłówków śledzenia rozproszonego: jedna linia znaków ASCII, która przenosi tożsamość żądania przez każdą usługę, jakiej ono dotknie. W obecnej wersji ma dokładnie 55 znaków i składa się z czterech pól rozdzielonych łącznikami:

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)

Dwa z tych pól zachowują się inaczej, gdy żądanie pokonuje kolejne przeskoki (hops). trace-id pozostaje identyczny na każdym przeskoku: to nazwa żądania, od proxy brzegowego aż po ostatnie zapytanie do bazy. parent-id zmienia się na każdym przeskoku, bo nazywa span, który wykonał wywołanie, a nie samo żądanie. Mylenie tych dwóch pól odpowiada za sporą część zgłoszeń w stylu „moje ślady wyglądają źle”.

Tyle anatomii. Reszta to sprawy, o których tabela pól milczy: co unieważnia nagłówek, co robi z takim nagłówkiem zgodny ze specyfikacją odbiorca i w którym miejscu nagłówek po cichu znika między dwiema usługami, a obie deklarują obsługę śledzenia. Jeśli pod ręką jest prawdziwy nagłówek, wystarczy wkleić go do darmowego dekodera traceparent i czytać dalej. Narzędzie rozdziela pola, rozwija bajt flag bit po bicie i nazywa regułę, którą złamał uszkodzony nagłówek.

Nagłówek traceparent w skrócie

Nagłówek traceparent to pojedynczy nagłówek HTTP, który przenosi ślad rozproszony między usługami. Zawiera cztery pola szesnastkowe rozdzielone łącznikami (version, trace-id, parent-id i trace-flags), a w obecnej wersji ma dokładnie 55 znaków. trace-id nazywa całe żądanie, a parent-id wskazuje span, od którego przyszło wywołanie.

PoleCyfry szesnastkoweBajtyCo identyfikujeZmienia się na przeskoku?
version21Format, którego trzyma się reszta. Dziś zawsze 00Nie
trace-id3216Całe żądanie, od początku do końcaNie
parent-id168Wywołujący span (span ID po stronie wywołującego)Tak
trace-flags21Pole 8-bitowe; bit 0 to sampledRzadko

Do tych 52 cyfr szesnastkowych dochodzą trzy łączniki i wychodzi 55 znaków. Tę liczbę opłaca się zapamiętać: nagłówek w wersji 00 o jakiejkolwiek innej długości jest nieważny, a długość sprawdza się okiem najszybciej.

Wszystko w nagłówku zapisane jest szesnastkowo małymi literami. Nie „szesnastkowo, bez rozróżniania wielkości liter” — małymi literami. Gramatyka z rekomendacji W3C Trace Context dopuszcza 0-9 i a-f, i nic poza tym. Dlatego trace ID zapisany wielkimi literami, z idealnie poprawną wartością, i tak zostanie odrzucony dalej w łańcuchu.

Pole po polu

Każde pole ma własną szerokość, własny zestaw wartości nieprawidłowych i własny sposób, w jaki potrafi się zepsuć.

version — dlaczego nie zawsze jest to „po prostu 00”

Dziś bajt wersji ma wartość 00 i przez jakiś czas tak zostanie. Ale ff jest jawnie zabronione: specyfikacja rezerwuje tę wartość jako nieprawidłową, więc nagłówek zaczynający się od ff jest martwy już w chwili odbioru, niezależnie od tego, co następuje dalej.

Ciekawsza jest reguła dotycząca wersji, których nikt jeszcze nie widział. Parser, który robi if (version !== '00') reject(), jest błędny — i to w kosztowny sposób. Specyfikacja wymaga, żeby odbiorca spróbował sparsować nagłówek, gdy wersja jest wyższa, a sam nagłówek co najmniej tak długi jak znany format: odczytać rozpoznane pola, tolerować nadmiarowe dane na końcu i działać dalej. Odrzucenie oznacza, że usługa staje się granicą, na której ślad się urywa i zaczyna nowy, dokładnie w chwili, gdy ktokolwiek wyżej w łańcuchu zaktualizuje swoją implementację.

// 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 bajtów, tożsamość całego żądania

Trzydzieści dwie cyfry szesnastkowe małymi literami, stałe przez całe życie śladu. Bez względu na to, która usługa wygenerowała go na starcie, każdy przeskok przekazuje go dalej bez zmian. Szukając śladu w backendzie obserwowalności, wkleja się właśnie ten ciąg.

Wartość rządzi się dwiema regułami. Musi mieć 32 cyfry szesnastkowe i nie może składać się z samych zer. 00000000000000000000000000000000 to nie „ślad, który nie ma jeszcze danych” — specyfikacja wskazuje tę wartość jako nieprawidłową i wymaga, aby odbiorca zignorował cały nagłówek. W praktyce trace ID z samych zer oznacza SDK, które nigdy się nie zainicjalizowało, albo middleware wstawiające wartość zastępczą, bo nie miało realnego kontekstu do przekazania.

trace-id ma 128 bitów — tyle samo co UUID — i nie jest UUID-em. Nie ma tu bitów wersji, bitów wariantu, łączników ani żadnej struktury: szesnaście nieprzezroczystych bajtów. Nie da się wyciągnąć z niego v4, a UUID pozbawiony łączników też nie staje się automatycznie poprawnym trace-id, bo nibble wersji i wariantu psują równomierność losowości. Tekst o tym, co tak naprawdę koduje UUID, rozkłada ten układ bitów na czynniki pierwsze, a generator UUID pokazuje bity wersji i wariantu na swoim miejscu.

parent-id — 8 bajtów, span, który wykonał wywołanie

Szesnaście cyfr szesnastkowych, nadpisywanych na każdym przeskoku. Nazwa tego pola powoduje więcej zamieszania, niż samo pole na to zasługuje: specyfikacja W3C mówi o parent-id, OpenTelemetry nazywa te same 8 bajtów span ID, a to jedno i to samo widziane z dwóch stron. Z punktu widzenia usługi odbierającej jest to rodzic; z punktu widzenia wywołującego — ID spanu, który właśnie utworzył na potrzeby żądania wychodzącego.

Kiedy więc usługa A wywołuje usługę B, A wstawia w miejsce parent-id własne span ID. B tworzy wtedy span potomny, a wywołując C, wstawia tam już span ID usługi B. trace-id przez cały czas pozostaje nietknięty. I to jest cały algorytm propagacji.

parent-id z samych zer też jest nieprawidłowy, z tego samego powodu co trace-id: 0000000000000000 oznacza, że wywołujący nie podał prawdziwego spanu, więc nagłówek należy odrzucić, a nie honorować połowicznie.

trace-flags — wygląda jak wartość logiczna, a to osiem bitów

Prawie każdy spotykany nagłówek kończy się na 01, więc naturalnie czyta się to pole jak tak/nie. To jednak bajt, a jego bity mają przypisane znaczenia:

  • bit 0, maska 0x01sampled
  • bit 1, maska 0x02random-trace-id, dodany w Trace Context Level 2
  • bity 2–7 — zarezerwowane; przy odbiorze należy je ignorować, a w żądaniach wychodzących zerować

Poszczególne kombinacje dekodują się tak:

HexBinarniesampledrandom-trace-idCzy flags === 0x01 zachodzi?
0000000000fałszfałszfałsz
0100000001prawdafałszprawda
0200000010fałszprawdafałsz
0300000011prawdaprawdafałsz ← tu jest błąd

Ostatni wiersz zasługuje na drugie czytanie. Ślad z flagami 03 jest próbkowany, a każdy kod porównujący cały bajt z 01 uzna go za niepróbkowany — po cichu i tylko dla tej części ruchu, w której akurat ustawiona jest flaga z Level 2. Podstępne jest w tym to, że objaw wygląda jak problem ze współczynnikiem próbkowania, a nie jak błąd parsowania.

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;

Co właściwie stwierdza random-trace-id? Że co najmniej 7 skrajnie prawych bajtów trace-id powstało z równomiernie rozłożonej losowości. Brzmi to akademicko, dopóki nie wejdzie w grę spójne próbkowanie: jeśli system niżej w łańcuchu chce zachować 1% śladów i potrzebuje, by każda usługa niezależnie zgodziła się co do tego samego 1%, może wziąć te bajty modulo pewną liczbę, zamiast najpierw obliczać hash z identyfikatora. Flaga jest obietnicą nadawcy, że takie podejście jest bezpieczne.

Co unieważnia nagłówek traceparent

Bazy wiedzy dostawców zwykle pomijają tę część, a pyta się o nią najczęściej. Pełny zestaw powodów odrzucenia nagłówka w wersji 00:

ObjawRegułaWynik
00-4BF92F35...-01Gramatyka dopuszcza wyłącznie małe litery szesnastkoweNieważny — wartość jest dobra, nagłówek zostaje odrzucony
ff-...Wersja ff jest zabroniona przez specyfikacjęNieważny
trace-id to 00000000000000000000000000000000trace-id z samych zer to nazwana wartość nieprawidłowaNieważny
parent-id to 0000000000000000parent-id z samych zer to nazwana wartość nieprawidłowaNieważny
trace-id nie ma 32 cyfr szesnastkowychStała szerokośćNieważny
parent-id nie ma 16 cyfr szesnastkowychStała szerokośćNieważny
trace-flags nie ma 2 cyfr szesnastkowychStała szerokośćNieważny
Nagłówek nie ma dokładnie 55 znaków, wersja 00Dane doklejone na końcu są legalne dopiero w przyszłej wersjiNieważny
Dowolny znak spoza 0-9a-f i łącznikówTo nie jest zapis szesnastkowyNieważny

Wynika z tego jedna reguła:

Zgodny ze specyfikacją odbiorca nie naprawia nieprawidłowego nagłówka traceparent i nie przekazuje go dalej. Odrzuca nagłówek i rozpoczyna zupełnie nowy ślad ze świeżo wygenerowanym trace-id.

Objaw widoczny na ekranie nie jest więc zepsutym śladem. To dwa rozłączne, krótkie ślady — jeden urywa się nagle na usłudze, która wyemitowała wadliwy nagłówek, drugi zaczyna się znikąd na usłudze, która go odebrała. Nigdzie nic nie jest oznaczone jako błąd. Oba ślady w oderwaniu od siebie wyglądają zdrowo. Ludzie tracą całe popołudnia na szukanie brakującego ogniwa, podczas gdy odpowiedź brzmi: middleware zamieniło ciąg szesnastkowy na wielkie litery albo ręcznie sklejony nagłówek wyszedł 54-znakowy.

Długość i wielkość liter to dwa tryby awarii, których nie widać gołym okiem. Wystarczy wkleić nagłówek do dekodera, a wskaże on dokładnie złamaną regułę, zamiast zmuszać do liczenia cyfr.

tracestate: nagłówek towarzyszący, który wszyscy rozumieją źle

traceparent niesie standardową tożsamość. Nagłówek tracestate niesie to, co obok niej chce dołożyć każdy dostawca — jako rozdzielone przecinkami elementy key=value:

tracestate: rojo=00f067aa0ba902b7,congo=t61rcWkgMzE

Implementacja, która nie rozpoznaje klucza, musi przekazać go dalej nietkniętego. Na tym polega cały zamysł: dostawcy mogą przenosić własny, zamknięty stan na standardowym śladzie, bez wymogu, by każdy przeskok go rozumiał.

Gramatyka ma jednak zęby, a trzy jej reguły tłumaczą objawy widywane na produkcji.

32 elementy listy to twardy sufit. Limit siedzi wprost w gramatyce: list = list-member 0*31( OWS "," OWS list-member ). tracestate z 33 elementami nie jest nagłówkiem z jednym wpisem więcej — jest nagłówkiem nieprawidłowym, a odbiorca ma prawo odrzucić całość. To odpowiedź na objaw, który inaczej wygląda jak magia: dane dostawcy są obecne na brzegu sieci, są dwa przeskoki dalej, a przy piątym znikają bez śladu. Każdy przeskok dopisywał własny element, lista przekroczyła 32 i od tego momentu cały nagłówek był odrzucany, a nie przycinany.

Wartości mają od 1 do 256 znaków i nigdy nie mogą być puste. Produkcja wartości kończy się obowiązkowym znakiem niebędącym odstępem, więc vendor= to nie „klucz bez wartości” — to błąd składni. Wyłącznie drukowalne ASCII, nigdy przecinek ani znak równości wewnątrz wartości.

Gramatyka kluczy zmieniła się między Level 1 a Level 2. Level 1 definiował klucze produkcją tenant@vendor, w której @ był separatorem strukturalnym. Level 2 zastąpił to płaską klasą znaków: klucz zaczyna się małą literą albo cyfrą i biegnie dalej znakami a-z, 0-9, _, -, *, / oraz @. W Level 2 @ jest zwykłym znakiem, klucz może zaczynać się cyfrą, a a@b@c to całkowicie legalny klucz, który produkcja z Level 1 by odrzuciła. Jeśli proxy waliduje według Level 1, a usługa emituje klucze w stylu Level 2, jedna strona przyjmuje to, co druga odrzuca, i nagłówek ginie dokładnie na jednym przeskoku.

Zostają jeszcze dwie reguły. Zduplikowane klucze są nieprawidłowe wprost. Druga: przy modyfikacji parent-id w traceparent trzeba przenieść własny wpis tracestate na początek listy, bo lista jest uporządkowana od najnowszego. Pominięcie tego kroku zostawia nieaktualny stan dostawcy w miejscu, w którym czytający uzna go za bieżący.

Na koniec reguła przyjazna: puste elementy listy są legalne. Gdy element usuwa pośrednik sieciowy, często zostawia po nim przecinek, dając rojo=1,,congo=2. Specyfikacja jawnie na to pozwala, więc parser powinien pominąć pusty element i działać dalej, zamiast uznawać nagłówek za zniekształcony. Widok tracestate w dekoderze wypisuje każdy element z osobną walidacją i bieżącym licznikiem względem limitu 32 elementów, co zwykle idzie szybciej niż liczenie przecinków.

Jak wędrują nagłówki śledzenia rozproszonego: jedno żądanie, cztery przeskoki

Jedno żądanie przechodzące przez proxy brzegowe, usługę API i dwie usługi niżej w łańcuchu:

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

Każdy przeskok robi te same trzy rzeczy: odczytuje nagłówek przychodzący, podmienia parent-id na własne span ID dla każdego wywołania wychodzącego i przekazuje trace-id oraz flagi bez zmian. Gdy nagłówka przychodzącego w ogóle nie ma (jak u klienta powyżej), usługa odbierająca jest korzeniem: generuje trace-id i podejmuje decyzję o próbkowaniu dla wszystkiego, co znajdzie się dalej.

Nagłówek można wstrzyknąć ręcznie, żeby przetestować cały łańcuch od początku do końca:

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

Powtórzenie nagłówka przechwyconego z produkcji na środowisku staging pozwala zobaczyć ten sam trace-id w backendzie. Generator poleceń cURL składa flagi samodzielnie, gdy dochodzi uwierzytelnianie albo ciało żądania, a ściągawka curl omawia opcje nagłówków i trybu verbose, przydatne przy debugowaniu.

Żeby zobaczyć, co usługa faktycznie odebrała, a nie co rzekomo zostało wysłane, wystarczy uruchomić jednorazowy serwer echo i skierować na niego jeden przeskok:

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

Potem curl -H 'traceparent: …' http://127.0.0.1:8080/ i wystarczy odczytać, co wyszło po drugiej stronie. Połowa śledztw w stylu „proxy zjada mi nagłówek” kończy się właśnie tutaj.

trace-flags sampled: decyzja usługi wyżej w łańcuchu, a nie potwierdzenie

Bit sampled o wartości 1 w polu trace-flags oznacza, że usługa wyżej w łańcuchu zdecydowała się zapisać ten ślad. Nie obiecuje, że dane dotarły do backendu.

Próbkowanie head-based podejmuje tę decyzję w korzeniu, zanim cokolwiek się wydarzy, i propaguje ją w dół: tanio, spójnie między usługami i na ślepo — nie ma jak wiedzieć, że żądanie za chwilę się wywali. Próbkowanie tail-based buforuje spany aż do zakończenia śladu i dopiero wtedy decyduje, dzięki czemu może zachować każdy ślad zawierający błąd, kosztem trzymania spanów w pamięci i wymogu, by spany wszystkich usług trafiały do tego samego kolektora.

Przy próbkowaniu tail-based ślad może dotrzeć z flagą 01 na każdym przeskoku, a mimo to zostać odrzucony na końcu. Odrzucić go mogą również ograniczenia przepustowości i limity eksportu. 01 na brzegu sieci i brak śladu w interfejsie nie musi więc oznaczać błędu propagacji. Pierwszym przystankiem są wtedy metryki odrzuceń samego kolektora, a dopiero potem nagłówki.

Na co dzień częstszy jest przypadek odwrotny. Jeśli przychodzące flagi to 00, wywołujący uruchomił swój sampler i postanowił nie zapisywać. Nic w odbierającej usłudze nie jest źle skonfigurowane, a audyt własnego samplera to stracony czas; szukać trzeba usługi wyżej w łańcuchu, która decyduje przeciwko próbkowaniu.

Konwersja między formatami propagacji

W3C Trace Context wygrał, ale mnóstwo systemów wciąż mówi czymś starszym, a bramy tłumaczą między nimi. Ten sam przykładowy traceparent zapisany w czterech formatach:

FormatNagłówkiWartość dla naszego przykładu
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, tag _dd.p.tid11803532876627986230, 67667974448284343, 4bf92f3577b34da6
AWS X-RayX-Amzn-Trace-IdRoot=1-4bf92f35-77b34da6a3ce929d0e0e4736;Parent=00f067aa0ba902b7;Sampled=1

Datadog: podział na wyższe i niższe 64 bity

Identyfikatory Datadoga są starsze niż 128-bitowe trace ID, a warstwa zgodności to miejsce, w którym większość konwersji się wykłada. x-datadog-trace-id niesie niższe 64 bity jako ciąg dziesiętny. Wyższe 64 bity podróżują osobno, szesnastkowo, w tagu _dd.p.tid, przenoszonym z kolei w nagłówku 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

Klasyczny błąd to przekonwertowanie wszystkich 128 bitów na jedną liczbę dziesiętną:

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

Ta wartość nie jest pomyłką arytmetyczną. To poprawny dziesiętny zapis niewłaściwej wielkości — dlatego przechodzi code review, a potem po cichu nie pasuje do żadnego śladu.

Druga pułapka to precyzja liczbowa. 64-bitowy identyfikator przekracza Number.MAX_SAFE_INTEGER, czyli 9007199254740991, więc każda ścieżka kodu, która pozwala trace ID stać się liczbą JavaScriptu, psuje jego najmłodsze cyfry. Trace ID trzeba trzymać jako ciągi znaków, a po BigInt sięgać dopiero przy działaniach arytmetycznych; identyfikator, który przyszedł w JSON bez cudzysłowów, jest uszkodzony, zanim ktokolwiek go zobaczy.

AWS X-Ray: timestamp, którego tam nie ma

Trace ID z X-Ray wygląda jak 1-{8 hex}-{24 hex}, a wiodące 8 cyfr szesnastkowych to czas utworzenia w sekundach epoch. Konwersja z W3C jest mechaniczna:

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

Na tę datę trzeba spojrzeć uważnie. Przykładowy nagłówek ze specyfikacji dekoduje się na maj 2010 roku, co jest oczywistą bzdurą — i o to właśnie chodzi. trace-id W3C nie zawiera żadnego timestampu. Szesnaście losowych bajtów bez trudu wyprodukuje wiarygodnie wyglądający epoch, gdy pierwsze cztery z nich odczytać jako jedną liczbę, a liczba ta nic nie znaczy, o ile identyfikator nie pochodzi naprawdę z X-Ray. Odczytywanie czasu z dowolnego trace-id to czytanie liczby losowej i wiara w nią.

Gdy identyfikator faktycznie pochodzi z X-Ray, konwersja się przydaje: wystarczy wrzucić te osiem cyfr szesnastkowych do konwertera Unix timestamp, żeby dostać czytelną datę, a przewodnik po czasie epoch omawia pułapki sekund kontra milisekund oraz stref czasowych, które przychodzą zaraz potem.

B3: dziedzictwo Zipkina

Format wywodzi się z Zipkina i spotyka się go w starszych siatkach usług (service mesh). Postać jednonagłówkowa to traceId-spanId-sampled, gdzie pole sampled przyjmuje 1 albo 0 zamiast bajtu szesnastkowego — bit random-trace-id z Level 2 nie ma się więc gdzie podziać i po prostu ginie w tłumaczeniu. Postać wielonagłówkowa rozdziela te same wartości na X-B3-TraceId, X-B3-SpanId i X-B3-Sampled.

Historyczny zgrzyt dotyczy szerokości. Trace ID w B3 bywają 64-bitowe, czyli mają 16 cyfr szesnastkowych zamiast 32. Konwersja 64-bitowego ID z B3 na W3C oznacza dopełnienie zerami z lewej strony do 32 cyfr, a konwersja w drugą stronę — decyzję, czy obcinać. Dopełnianie z lewej jest bezpieczne, obcinanie nie, bo dwa ślady różniące się wyłącznie starszymi bajtami zlewają się w jeden.

Gdzie traceparent ginie na produkcji

Wszystko powyżej zakłada, że nagłówek dociera. Często nie dociera, a przepada zwykle w jednym z czterech miejsc.

Przeglądarka gubi go przy wywołaniach cross-origin

Objaw: ślady z frontendu są, ślady z backendu są, i nic ich nie łączy. Albo żądanie cross-origin od razu kończy się błędem CORS.

Przyczyna: traceparent jest nagłówkiem niestandardowym, więc jego dodanie sprawia, że żądanie przestaje być „proste”, i wyzwala zapytanie preflight OPTIONS. Jeśli odpowiedź serwera na preflight nie wymienia tego nagłówka w Access-Control-Allow-Headers, przeglądarka blokuje właściwe żądanie. Osobno: instrumentacja przeglądarkowa OpenTelemetry odmawia wstrzykiwania nagłówków śledzenia do żądań cross-origin, dopóki nie wskaże się jej dozwolonych origin.

Rozwiązanie: po stronie serwera zwracać dla preflightu Access-Control-Allow-Headers: traceparent, tracestate. W SDK przeglądarkowym ustawić propagateTraceHeaderCorsUrls na wzorzec pasujący do origin API. Potrzebne są oba; każde z osobna zostawia dokładnie ten sam objaw. Jeśli preflight wraca z nieoczekiwanym statusem, warto zajrzeć do ściągi kodów statusu HTTP, zanim uzna się nagłówek za winowajcę.

Proxy, WAF-y i load balancery usuwają nieznane nagłówki

Objaw: nagłówek jest obecny przy bezpośrednim wywołaniu usługi przez curl i znika, gdy to samo żądanie idzie przez bramę.

Przyczyna: przekazywanie oparte na allowlist. Mnóstwo konfiguracji proxy, zestawów reguł WAF i zarządzanych load balancerów przekazuje wyłącznie rozpoznane nagłówki, a traceparent nie figuruje na domyślnej liście. Niektóre siatki usług dodatkowo nadpisują nagłówek, generując własny trace-id i odrzucając ten przychodzący.

Rozwiązanie: przeszukiwanie połówkowe z serwerem echo z wcześniejszego fragmentu — wystawić go kolejno za każdym przeskokiem i sprawdzić, która warstwa gubi nagłówek. Potem jawnie dopuścić traceparent i tracestate w regułach przekazywania tej warstwy. Jeśli proxy to nginx, o przekazywanych nagłówkach decyduje blok obsługujący daną trasę, a ten nie zawsze jest blokiem, którego można się spodziewać; reguły priorytetu bloków location w nginx tłumaczą, dlaczego konfiguracja nagłówków potrafi sprawiać wrażenie całkowicie ignorowanej.

Kolejki wiadomości nie mają nagłówków HTTP

Objaw: ślad kończy się w chwili, gdy żądanie zamienia się w zadanie w tle.

Przyczyna: przez tę granicę nie przechodzi żadne żądanie HTTP, więc nie ma na czym propagować nagłówka. Kafka ma nagłówki rekordów, SQS ma atrybuty wiadomości, a instrumentacja HTTP nie wypełnia ani jednych, ani drugich.

Rozwiązanie: wstrzyknąć kontekst do wiadomości po stronie producenta i wydobyć go po stronie konsumenta. Każde SDK OpenTelemetry udostępnia do tego inject i extract, a format przesyłany po sieci to ten sam ciąg W3C; zmienia się wyłącznie nośnik — z mapy nagłówków HTTP na metadane wiadomości. Dokumentacja propagatorów OpenTelemetry opisuje interfejs nośnika dla każdego języka.

Wielkość liter i to, co naprawdę zmienia HTTP/2

Objaw: zamieszanie na code review co do tego, czy Traceparent jest dopuszczalne.

Przyczyna: dwie osobne reguły zlewają się w jedną. Nazwy nagłówków w HTTP/1.1 są nieczułe na wielkość liter, a HTTP/2 wymaga zakodowania ich w sieci małymi literami. To dotyczy nazwy. Niezależnie od tego zapis szesnastkowy w wartości nagłówka musi być małymi literami, bo tak mówi gramatyka W3C — i żadna wersja protokołu tego nie naprawi.

Rozwiązanie: wysyłać nazwę jako traceparent i nigdy nie zapisywać wartości wielkimi literami. Brama, która normalizuje nazwy nagłówków, nie znormalizuje cyfr szesnastkowych, a trace-id wielkimi literami przepłynie przez każdą warstwę transportową, żeby dopiero na końcu zostać odrzuconym przez aplikację, która wreszcie go parsuje.

Czy można ufać przychodzącemu nagłówkowi traceparent?

traceparent przychodzący z publicznego internetu to dane kontrolowane przez użytkownika — ciąg wybrany przez anonimowego klienta, który większość usług przyjmuje bez zastanowienia.

Wynikają z tego trzy ryzyka. Po pierwsze, wszywanie się w ślad (trace splicing): atakujący, który wyśle trace-id podpatrzony gdzie indziej, wszywa własne żądanie w istniejący ślad, co zanieczyszcza graf i może ujawnić wewnętrzne czasy każdemu, kto ten ślad przeczyta. Po drugie, wypalanie limitów: zaszycie na sztywno 01 wymusza próbkowanie każdego żądania, a nawet umiarkowany zalew ruchu zamienia się w bardzo wysoki rachunek za ingest albo, co gorsza, wypycha ślady faktycznie potrzebne. Po trzecie, korelacja między tenantami: wielokrotne użycie jednego trace-id w żądaniach od różnych tenantów łączy rekordy, które narzędzia potraktują potem jak jedną operację logiczną.

Pragmatyczna postawa to przyjmować na brzegu, ale nie ufać. Sprawdzać zgodność z gramatyką i odrzucać zniekształcone nagłówki, zamiast przepuszczać je do środka. Dla ruchu nieuwierzytelnionego podejmować decyzję o próbkowaniu od nowa, zamiast honorować flagę przychodzącą. Dzięki temu żaden zewnętrzny klient nie ustawi samplera na stałe w tryb „zawsze zapisuj”. Dla ruchu uwierzytelnionego honorowanie decyzji wywołującego zwykle nie jest problemem, bo wiadomo, kto stoi po drugiej stronie.

I trzeba traktować trace-id jak wartość publiczną. Nie jest tajemnicą i nigdy nią nie był: pojawia się w logach, na stronach błędów, w nagłówkach odpowiedzi i na zrzutach ekranu wklejanych do zgłoszeń serwisowych. Nigdy nie należy kodować w nim ID użytkownika, nazwy tenanta ani niczego innego znaczącego i nigdy nie wolno używać go jako klucza autoryzacyjnego. To identyfikator korelacyjny i niczym więcej być nie powinien.

FAQ

Czym różni się traceparent od tracestate?

traceparent niesie ustandaryzowaną tożsamość — trace-id, parent-id i flagi próbkowania — i każda implementacja musi go rozumieć. tracestate niesie stan specyficzny dla dostawcy, który nieznające go implementacje przekazują dalej nietknięty. Oba są powiązane: gdy traceparent jest nieprawidłowy, specyfikacja wymaga zignorowania również tracestate.

Dlaczego ślad zaczyna się od nowa w połowie łańcucha wywołań?

Ślad zaczyna się od nowa w połowie łańcucha niemal zawsze dlatego, że jeden przeskok odebrał nagłówek niezgodny z gramatyką, odrzucił go i wygenerował świeży trace-id. Powodują to: zapis szesnastkowy wielkimi literami, trace-id z samych zer oraz nagłówek, który nie ma dokładnie 55 znaków. Jeśli nagłówek jest poprawny, kolejni podejrzani to proxy usuwające go po drodze i nieudany preflight cross-origin.

Czy trzeba skonfigurować CORS, aby wysyłać traceparent z przeglądarki?

Tak, konfiguracja CORS jest wymagana. traceparent to nagłówek niestandardowy, więc żądanie przestaje być „proste” i wyzwala preflight; serwer musi wymienić traceparent w Access-Control-Allow-Headers. Instrumentacja przeglądarkowa OpenTelemetry wymaga dodatkowo skonfigurowania propagateTraceHeaderCorsUrls, bo domyślnie nie wstrzykuje nagłówków śledzenia w wywołaniach cross-origin.

Jak propagować kontekst śladu przez Kafkę lub SQS?

Po stronie producenta zapisać wartość traceparent w nagłówku rekordu Kafki albo w atrybucie wiadomości SQS, a po stronie konsumenta odczytać ją z powrotem, żeby odtworzyć kontekst. SDK OpenTelemetry udostępniają do tego inject i extract w każdym języku. Format pozostaje bez zmian; różni się wyłącznie nośnik: metadane wiadomości zamiast mapy nagłówków HTTP.

Czy trace ID można bezpiecznie ujawniać w logach i odpowiedziach?

Tak, trace ID można bezpiecznie ujawniać. To losowy identyfikator, który nie zawiera wbudowanej tożsamości i niczego nie autoryzuje. Koreluje jednak rekordy między systemami, więc nigdy nie należy kodować w nim ID użytkownika ani nazwy tenanta i nigdy nie wolno przyjmować go jako dowodu na cokolwiek. Traktowany jak publiczny klucz korelacyjny jest bezpieczny do logowania, zwracania i udostępniania.

Kto generuje nagłówek traceparent?

Pierwsza usługa, która obsłuży żądanie przychodzące bez tego nagłówka. Zwykle jest to proxy brzegowe, brama API albo SDK przeglądarkowy, i to ona staje się korzeniem śladu: generuje trace-id, tworzy pierwszy span i podejmuje decyzję o próbkowaniu. Każdy kolejny przeskok podmienia już tylko parent-id.

Czy nagłówek traceparent jest wymagany?

Nie. Na poziomie protokołu jest opcjonalny, a żądanie bez niego jest całkowicie poprawne; usługa odbierająca po prostu staje się korzeniem nowego śladu. Wymagany jest wyłącznie w tym praktycznym sensie, że bez niego pracy wykonanej po obu stronach granicy usług nie da się powiązać w jeden ślad.

Czy traceparent powoduje mierzalny narzut?

Nie w istotnym stopniu. Nagłówek traceparent ma 55 bajtów, a tracestate dokłada zwykle kilkaset kolejnych — pomijalnie mało obok handshake’u TLS czy jakiegokolwiek realnego payloadu. Rzeczywistym kosztem śledzenia jest eksport i przechowywanie próbkowanych spanów, a nie przenoszenie nagłówków śledzenia rozproszonego po sieci.

Tagi: distributed-tracing opentelemetry observability http-headers w3c

Powiązane artykuły

Zobacz wszystkie artykuły