Skip to content
Torna al blog
Tutorial

Header traceparent: guida completa a W3C Trace Context

L'header traceparent campo per campo: cosa significa ogni segmento esadecimale, cosa lo rende non valido e perché le tracce si rompono. Decoder online gratis.

13 min di lettura

Header traceparent: guida completa a W3C Trace Context

L’header traceparent è lo standard W3C per gli header di distributed tracing: una singola riga di ASCII che porta l’identità di una richiesta attraverso ogni servizio che questa tocca. Nella versione attuale è lungo esattamente 55 caratteri e ha quattro campi separati da trattini:

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)

Due di questi campi si comportano in modo diverso mentre la richiesta viaggia. Il trace-id resta identico a ogni hop, cioè a ogni passaggio da un servizio al successivo: è il nome della richiesta, dal proxy di frontiera fino all’ultima chiamata al database. Il parent-id cambia a ogni hop, perché nomina lo span che ti ha chiamato, non la richiesta. Confondere i due spiega buona parte dei ticket del tipo “le mie tracce sembrano sbagliate”.

Questa è l’anatomia. Il resto è ciò che la tabella dei campi non può dirti: cosa rende un header non valido, cosa fa un ricevitore conforme quando ne riceve uno, e in che punto l’header sparisce in silenzio tra due servizi che dichiarano entrambi di supportare il tracing. Se hai un header vero sotto mano, incollalo nel decodificatore traceparent gratuito: separa i campi, espande il byte dei flag bit a bit e indica la regola che un header rotto viola.

L’header traceparent in sintesi

L’header traceparent è un singolo header HTTP che trasporta una traccia distribuita tra i servizi. Contiene quattro campi esadecimali separati da trattini (version, trace-id, parent-id e trace-flags) e nella versione attuale è lungo esattamente 55 caratteri. Il trace-id è il nome dell’intera richiesta; il parent-id è il nome dello span che ti ha chiamato.

CampoCifre esadecimaliByteCosa identificaCambia a ogni hop?
version21Quale formato segue il resto. Oggi sempre 00No
trace-id3216L’intera richiesta, da un capo all’altroNo
parent-id168Lo span chiamante (lo span ID di chi ti ha chiamato)
trace-flags21Un campo a 8 bit; il bit 0 è sampledRaramente

Aggiungi tre trattini a quelle 52 cifre esadecimali e ottieni 55 caratteri. Impara a memoria quel numero: un header di versione 00 con qualsiasi altra lunghezza non è valido, e la lunghezza è la cosa più veloce da controllare a occhio.

Tutto ciò che sta nell’header è esadecimale minuscolo. Non “esadecimale, indifferente alle maiuscole”: minuscolo. La grammatica della raccomandazione W3C Trace Context ammette 0-9 e a-f e nient’altro, ed è per questo che un trace ID in maiuscolo con un valore perfettamente corretto viene comunque buttato via a valle.

Campo per campo

Ogni campo ha la sua larghezza, i suoi valori non validi e il suo modo tipico di andare storto.

version: perché non è sempre “solo 00”

Oggi il byte di versione è 00, e resterà 00 ancora per un po’. Ma ff è vietato in modo esplicito: la specifica lo riserva come valore non valido, quindi un header che si apre con ff è morto all’arrivo, qualunque cosa lo segua.

La regola interessante riguarda le versioni che non hai mai visto. Un parser che fa if (version !== '00') reject() è sbagliato, e sbagliato in modo costoso. La specifica chiede ai ricevitori di tentare comunque il parsing quando la versione è più alta e l’header è lungo almeno quanto il formato noto: leggi i campi che riconosci, tollera i dati in eccesso alla fine e vai avanti. Rifiutare significa invece che il tuo servizio diventa il confine dove la traccia si interrompe e ne comincia una nuova, nel momento stesso in cui qualcuno a monte aggiorna.

// Sbagliato: rende il tuo servizio il posto dove le tracce vanno a morire
if (version !== '00') throw new Error('bad traceparent');

// Giusto: analizza il prefisso che sai leggere
if (version !== '00' && header.length >= 55) {
  // leggi version, trace-id, parent-id, trace-flags; ignora il resto
}

trace-id: 16 byte, l’identità dell’intera richiesta

Trentadue cifre esadecimali minuscole, costanti per tutta la vita della traccia. Qualunque servizio l’abbia generato all’inizio, ogni hop lo copia in avanti senza modificarlo. Quando cerchi una traccia nel tuo backend di observability, è questa la stringa che incolli.

Due regole governano il suo valore: deve essere di 32 cifre esadecimali e non può essere tutto zeri. 00000000000000000000000000000000 non significa “una traccia che non ha ancora dati”: la specifica lo indica come valore non valido e impone ai ricevitori di ignorare l’intero header. In pratica un trace ID tutto a zeri significa un SDK che non si è mai inizializzato, oppure un middleware che infila un segnaposto perché non aveva nessun contesto reale da inoltrare.

Un trace-id è lungo 128 bit, la stessa larghezza di un UUID, e non è un UUID. Non ci sono bit di versione, bit di variante, trattini né struttura di alcun tipo: sedici byte opachi. Non puoi estrarne un v4 con il parsing, e nemmeno un UUID a cui hai tolto i trattini è automaticamente un trace-id valido, perché i nibble di versione e variante rendono la sua casualità non uniforme. Se vuoi vedere cosa riserva davvero un UUID dentro quei 128 bit, cosa codifica realmente un UUID ne ripercorre il layout, e il generatore UUID mostra i bit di versione e variante al loro posto.

parent-id: 8 byte, lo span che ti ha chiamato

Sedici cifre esadecimali, riscritte a ogni hop. Il nome crea più confusione di quanta ne meriti il campo: la specifica W3C lo chiama parent-id, OpenTelemetry chiama gli stessi 8 byte span ID, e sono la stessa cosa vista da due direzioni. Dal punto di vista del tuo servizio è il padre; dal punto di vista del chiamante è l’ID dello span che ha appena creato per la richiesta in uscita.

Quindi, quando il servizio A chiama il servizio B, A mette il proprio span ID nella casella parent-id. B crea poi uno span figlio e, quando chiama C, ci mette lo span ID di B. Il trace-id resta intatto per tutto il percorso. Questo è l’intero algoritmo di propagazione.

Anche i parent-id tutti a zeri non sono validi, per lo stesso motivo dei trace-id: 0000000000000000 significa che il chiamante non ha indicato nessuno span reale, e l’header va scartato invece che onorato a metà.

trace-flags: sembra un booleano, in realtà sono otto bit

Quasi tutti gli header che ti capiterà di vedere finiscono con 01, quindi viene naturale leggere il campo come un sì/no. È un byte, e i bit hanno assegnazioni precise:

  • bit 0, maschera 0x01sampled
  • bit 1, maschera 0x02random-trace-id, aggiunto in Trace Context Level 2
  • bit 2–7 — riservati; ignorali in ricezione, azzerali nelle richieste in uscita

Le combinazioni si decodificano così:

EsadecimaleBinariosampledrandom-trace-idVale flags === 0x01?
0000000000falsefalsefalse
0100000001truefalsetrue
0200000010falsetruefalse
0300000011truetruefalse ← il bug

Rileggi l’ultima riga. Una traccia con flag 03 è campionata. Qualunque codice confronti l’intero byte con 01 la segnala come non campionata, in silenzio, e solo per la porzione di traffico in cui il flag di Level 2 risulta impostato: la forma di guasto peggiore possibile, perché sembra un problema di sampling rate invece che un bug di parsing.

const flags = parseInt(traceFlags, 16);

// Sbagliato: tratta un campo di bit come un'enumerazione
const sampled = traceFlags === '01';

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

Cosa afferma davvero random-trace-id? Che almeno i 7 byte più a destra del trace-id sono stati generati con casualità uniforme. Sembra accademico finché non pensi al campionamento consistente: se un sistema a valle vuole tenere l’1% delle tracce e ha bisogno che ogni servizio decida in modo indipendente ma concorde quale 1%, può prendere quei byte modulo qualcosa invece di calcolare prima l’hash dell’ID. Il flag è la promessa, da parte di chi sta a monte, che farlo è sicuro.

Cosa rende un traceparent non valido

Le knowledge base dei vendor saltano quasi sempre questa parte, che è poi la domanda con cui la gente arriva qui. Ecco tutti i motivi di rifiuto per un header di versione 00:

SintomoRegolaRisultato
00-4BF92F35...-01La grammatica ammette solo esadecimale minuscoloNon valido: il valore è giusto, l’header viene rifiutato
ff-...La versione ff è vietata dalla specificaNon valido
il trace-id è 00000000000000000000000000000000Il trace-id tutto a zeri è un valore dichiarato non validoNon valido
il parent-id è 0000000000000000Il parent-id tutto a zeri è un valore dichiarato non validoNon valido
il trace-id non ha 32 cifre esadecimaliLarghezza fissaNon valido
il parent-id non ha 16 cifre esadecimaliLarghezza fissaNon valido
trace-flags non ha 2 cifre esadecimaliLarghezza fissaNon valido
L’header non è lungo esattamente 55 caratteri, versione 00I dati in coda sono leciti solo con una versione futuraNon valido
Un carattere qualsiasi fuori da 0-9a-f e dai trattiniNon è esadecimaleNon valido

La conseguenza pratica è una sola:

Un ricevitore conforme non ripara un header traceparent non valido e non lo inoltra. Scarta l’header e avvia una traccia completamente nuova, con un trace-id appena generato.

Il che significa che il sintomo sul tuo schermo non è una traccia rotta. Sono due tracce corte e scollegate: una che finisce di colpo sul servizio che ha emesso l’header sbagliato, e una che sembra iniziare dal nulla sul servizio che l’ha ricevuto. Da nessuna parte compare un errore. Prese singolarmente, entrambe le tracce sembrano sane. Puoi passare un pomeriggio intero a cercare l’anello mancante tra le due, quando la risposta è che un middleware ha messo in maiuscolo una stringa esadecimale, oppure che un header costruito a mano è venuto lungo 54 caratteri.

Lunghezza e maiuscole sono i due modi di fallire che non vedi a forza di fissare lo schermo. Incolla l’header nel decodificatore: ti indica la regola esatta che è stata violata, invece di costringerti a contare le cifre.

tracestate: l’header compagno che sbagliano tutti

traceparent porta l’identità standard. L’header tracestate porta tutto ciò che ogni vendor vuole aggiungere accanto, come membri key=value separati da virgole:

tracestate: rojo=00f067aa0ba902b7,congo=t61rcWkgMzE

Un’implementazione che non riconosce una chiave deve inoltrarla intatta. È tutto qui l’obiettivo di progettazione: i vendor possono far viaggiare stato proprietario su una traccia standard senza che ogni hop debba capirlo.

La grammatica però morde, e tre delle sue regole spiegano sintomi reali in produzione.

32 list-member sono un tetto rigido, scritto nella grammatica: list = list-member 0*31( OWS "," OWS list-member ). Un tracestate con 33 membri non è un tracestate con una voce in più: è un header non valido, e i ricevitori hanno il diritto di scartarlo per intero. Questa è la risposta a un sintomo che altrimenti sembra magia: dati del vendor presenti sul bordo della rete, presenti dopo due hop e spariti del tutto al quinto. Ogni hop aggiungeva il proprio membro, la lista ha superato 32 e da lì in poi l’header intero è stato buttato via invece che accorciato.

I valori vanno da 1 a 256 caratteri e non possono mai essere vuoti. La produzione del valore termina con un carattere non vuoto obbligatorio, quindi vendor= è un errore di sintassi, non una chiave rimasta senza valore. Solo ASCII stampabile, e mai una virgola o un segno di uguale dentro il valore.

La grammatica delle chiavi è cambiata tra Level 1 e Level 2. Level 1 definiva le chiavi con una produzione tenant@vendor, in cui @ era un separatore strutturale. Level 2 l’ha sostituita con una classe di caratteri piatta: una chiave inizia con una lettera minuscola o una cifra e prosegue con a-z, 0-9, _, -, *, / e @. Con Level 2 la @ è un carattere qualunque, le chiavi possono iniziare con una cifra e a@b@c è una chiave perfettamente legale che la produzione di Level 1 rifiuterebbe. Se hai un proxy che valida secondo Level 1 e un servizio che emette chiavi Level 2, un lato accetta ciò che l’altro rifiuta, e l’header sparisce esattamente su un hop.

Altre due regole da conoscere. Le chiavi duplicate non sono valide, senza appello. E quando modifichi il parent-id del traceparent devi spostare la tua voce di tracestate in testa alla lista: la lista è ordinata dal più recente al meno recente. Saltare lo spostamento in testa lascia stato del vendor ormai vecchio in una posizione dove chi legge lo tratterà come attuale.

Infine la regola gentile: i list-member vuoti sono leciti. Quando un middlebox rimuove una voce, spesso lascia la virgola al suo posto, producendo rojo=1,,congo=2. La specifica lo consente esplicitamente, quindi un parser dovrebbe scartare il membro vuoto e proseguire invece di dichiarare l’header malformato. La vista tracestate del decodificatore elenca ogni membro con la validazione voce per voce e un conteggio progressivo rispetto al limite di 32 membri, il che di solito è più veloce che contare le virgole.

Come viaggiano gli header di distributed tracing: una richiesta, quattro hop

Segui una richiesta che attraversa un proxy di frontiera, un servizio API e due servizi a valle:

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

Ogni hop fa le stesse tre cose: legge l’header in ingresso, sostituisce il parent-id con il proprio span ID per ogni chiamata in uscita, e inoltra trace-id e flag senza modificarli. Quando non c’è nessun header in ingresso, come per il client qui sopra, il servizio che riceve è la radice: genera un trace-id e prende la decisione di campionamento per tutto ciò che sta a valle.

Puoi iniettare un header a mano per testare una catena da un capo all’altro:

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

Rigioca su staging un header catturato in produzione e vedrai comparire lo stesso trace-id nel tuo backend. Il generatore di comandi cURL monta le opzioni al posto tuo se devi aggiungere autenticazione o un body, e il cheat sheet curl copre le opzioni per header e output verboso che ti serviranno mentre fai debug.

Per vedere cosa ha ricevuto davvero un servizio, invece di quello che pensi di aver inviato, avvia un echo server usa e getta e puntaci contro un hop:

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

Poi lancia curl -H 'traceparent: …' http://127.0.0.1:8080/ e leggi cosa è uscito dall’altro lato. Metà delle indagini del tipo “il proxy si mangia il mio header” finisce qui.

trace-flags sampled: una decisione a monte, non una ricevuta

Un bit sampled di trace-flags a 1 significa che il servizio a monte ha deciso di registrare questa traccia. Non promette che i dati siano arrivati al tuo backend.

Il campionamento head-based prende quella decisione alla radice, prima che succeda qualsiasi cosa, e la propaga verso il basso: costa poco, è coerente tra i servizi ed è cieco, perché non può sapere che la richiesta stava per fallire. Il campionamento tail-based tiene gli span in buffer finché la traccia non è completa e solo allora decide, così può conservare ogni traccia che contiene un errore, al costo di tenere gli span in memoria e di far arrivare gli span di tutti i servizi nello stesso collector.

Con il campionamento tail-based una traccia può arrivare con flag 01 a ogni hop ed essere comunque scartata alla fine. Anche i rate limit e le quote di esportazione possono scartarla. Quindi 01 sul bordo della rete e nessuna traccia nella UI non è per forza un bug di propagazione: controlla le metriche di scarto del collector prima di andare a guardare gli header.

Il caso opposto conta di più nel quotidiano. Se i flag in ingresso sono 00, il chiamante ha eseguito il suo sampler e ha scelto di non registrare. Nel tuo servizio non c’è niente di mal configurato, e passare al setaccio il tuo sampler è tempo sprecato: la domanda è quale servizio a monte stia decidendo di non campionare.

Conversione tra formati di propagazione

W3C Trace Context ha vinto, ma parecchi sistemi parlano ancora qualcosa di più vecchio, e i gateway traducono tra gli uni e gli altri. Ecco lo stesso esempio di traceparent scritto in quattro formati:

FormatoHeaderValore per il nostro esempio
W3Ctraceparent00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
B3 singolob34bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-1
B3 multiploX-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: la divisione tra i 64 bit alti e bassi

Gli identificatori di Datadog sono più vecchi dei trace ID a 128 bit, e il livello di compatibilità è il punto in cui la maggior parte delle conversioni sbaglia. x-datadog-trace-id porta i 64 bit bassi come stringa decimale. I 64 bit alti viaggiano a parte, in esadecimale, nel tag _dd.p.tid, che a sua volta sta dentro l’header x-datadog-tags.

const traceparent = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01';
const [, traceId, parentId] = traceparent.split('-');

const datadogTraceId  = BigInt('0x' + traceId.slice(16)).toString(10);
const higher64Hex     = traceId.slice(0, 16);
const datadogParentId = BigInt('0x' + parentId).toString(10);

console.log('x-datadog-trace-id:',  datadogTraceId);  // 11803532876627986230
console.log('x-datadog-tags:',      higher64Hex);     // _dd.p.tid=4bf92f3577b34da6
console.log('x-datadog-parent-id:', datadogParentId); // 67667974448284343

L’errore classico è convertire tutti i 128 bit in un unico numero decimale:

BigInt('0x' + traceId).toString(10);
// 100985939111033328018442752961257817910 — non corrisponde a niente nella UI

Quel valore non è un errore di aritmetica: è la resa decimale corretta della quantità sbagliata, ed è per questo che supera la code review e poi, in silenzio, non corrisponde a nessuna traccia.

La seconda trappola è la precisione numerica. Un identificatore a 64 bit supera Number.MAX_SAFE_INTEGER, che vale 9007199254740991, quindi qualunque percorso di codice lasci diventare un trace ID un number JavaScript ne corrompe le cifre basse. Tieni i trace ID come stringhe e ricorri a BigInt solo quando devi fare aritmetica: un ID che arriva senza virgolette dentro un JSON è già danneggiato nel momento in cui lo vedi.

AWS X-Ray: il timestamp che non c’è

Un trace ID X-Ray ha la forma 1-{8 hex}-{24 hex}, e le prime 8 cifre esadecimali sono l’istante di creazione in secondi dall’epoca Unix. La conversione da W3C è meccanica:

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

Guarda quella data. L’header di esempio della specifica si decodifica in maggio 2010, il che è palesemente assurdo, ed è esattamente questo il punto. Un trace-id W3C non contiene nessun timestamp. Sedici byte casuali producono senza problemi un epoch dall’aria plausibile quando leggi i primi quattro come se fossero un numero, e quel numero non significa niente a meno che l’identificatore non sia nato davvero in X-Ray. Decodificare un orario da un trace-id qualsiasi vuol dire leggere un numero casuale e credergli.

Quando l’ID viene davvero da X-Ray, la conversione è utile: butta quelle otto cifre esadecimali nel convertitore di timestamp Unix per ottenere una data leggibile, e la guida all’epoca Unix copre le trappole di secondi contro millisecondi e di fuso orario che ne derivano.

B3: l’eredità di Zipkin

B3 nasce da Zipkin ed è il formato che incontri nelle service mesh più vecchie. La forma a header singolo è traceId-spanId-sampled, dove il campo sampled è 1 o 0 invece di un byte esadecimale: così il bit random-trace-id di Level 2 non ha dove andare e si perde semplicemente nella traduzione. La forma a più header divide gli stessi valori tra X-B3-TraceId, X-B3-SpanId e X-B3-Sampled.

La complicazione storica è la larghezza. I trace ID B3 possono essere a 64 bit, cioè 16 cifre esadecimali invece di 32. Convertire un ID B3 a 64 bit verso W3C significa riempire a sinistra con zeri fino a 32 cifre, e convertire all’indietro significa decidere se troncare. Riempire a sinistra è sicuro; troncare no, perché due tracce che differiscono solo nei byte alti collassano in una sola.

Dove traceparent si perde in produzione

Tutto quanto sopra dà per scontato che l’header arrivi. Spesso non arriva, e quasi sempre sparisce in uno di questi quattro punti.

Il browser lo scarta nelle chiamate cross-origin

Sintomo: le tracce del frontend ci sono, quelle del backend anche, e niente le collega. Oppure la richiesta cross-origin fallisce del tutto con un errore CORS.

Causa: traceparent è un header personalizzato, quindi aggiungerlo rende la richiesta non semplice e fa scattare un preflight OPTIONS. Se la risposta di preflight del server non elenca l’header in Access-Control-Allow-Headers, il browser blocca la richiesta vera. A parte questo, la strumentazione browser di OpenTelemetry si rifiuta di iniettare header di tracing nelle richieste cross-origin se non le dici quali origin sono ammesse.

Soluzione: sul server, rispondi al preflight con Access-Control-Allow-Headers: traceparent, tracestate. Nell’SDK browser, imposta propagateTraceHeaderCorsUrls su un pattern che copra le origin delle tue API. Servono entrambe le cose: una sola lascia lo stesso identico sintomo. Se un preflight torna con uno stato inatteso, confrontalo con il cheat sheet dei codici di stato HTTP prima di dare la colpa all’header.

Proxy, WAF e load balancer eliminano gli header sconosciuti

Sintomo: l’header c’è quando fai curl direttamente sul servizio e non c’è quando la stessa richiesta passa dal gateway.

Causa: inoltro basato su allowlist. Parecchie configurazioni di proxy, molti ruleset WAF e i load balancer gestiti inoltrano solo gli header che riconoscono, e traceparent non è nella lista predefinita. Alcune mesh inoltre riscrivono l’header, generando un proprio trace-id e scartando il tuo.

Soluzione: procedi per bisezione con l’echo server di prima: mettilo dietro un hop alla volta e guarda quale livello elimina l’header. Poi consenti in modo esplicito traceparent e tracestate nelle regole di inoltro di quel livello. Se il proxy è nginx, tieni presente che è il blocco che gestisce una rotta a decidere quali header passa, e il blocco che gestisce una rotta non è sempre quello che ti aspetti: le regole di priorità delle location nginx spiegano perché una configurazione di header può sembrare ignorata del tutto.

Le code di messaggi non hanno header HTTP

Sintomo: la traccia finisce nell’istante in cui una richiesta diventa un job in background.

Causa: attraverso quel confine non passa nessuna richiesta HTTP, quindi non c’è niente su cui propagare l’header. Kafka ha gli header di record, SQS ha gli attributi di messaggio, e nessuno dei due viene popolato al posto tuo dalla strumentazione HTTP.

Soluzione: inietta il contesto nel messaggio lato producer ed estrailo lato consumer. Ogni SDK OpenTelemetry espone inject ed extract esattamente per questo, e il formato sul filo è la stessa stringa W3C: cambia solo il carrier, da una mappa di header HTTP ai metadati del messaggio. La documentazione sui propagator di OpenTelemetry descrive l’interfaccia del carrier linguaggio per linguaggio.

Maiuscole e minuscole: cosa mette davvero in minuscolo HTTP/2

Sintomo: confusione in code review sul fatto che Traceparent sia accettabile o meno.

Causa: due regole distinte vengono fuse in una sola. In HTTP/1.1 i nomi degli header sono indifferenti alle maiuscole, e HTTP/2 impone di codificarli in minuscolo sul filo. Questo riguarda il nome. Indipendentemente da ciò, l’esadecimale nel valore dell’header deve essere minuscolo, perché lo dice la grammatica W3C, e nessuna versione del protocollo lo sistemerà al posto tuo.

Soluzione: invia il nome come traceparent e non mettere mai in maiuscolo il valore. Un gateway che normalizza i nomi degli header non normalizzerà le tue cifre esadecimali, e un trace-id in maiuscolo attraversa indisturbato ogni livello di trasporto per poi essere rifiutato dall’applicazione che alla fine ne fa il parsing.

Ti puoi fidare di un traceparent in ingresso?

Un traceparent che arriva dalla rete pubblica è input controllato dall’utente: una stringa scelta da un client anonimo, che la maggior parte dei servizi accetta senza pensarci due volte.

Ne derivano tre rischi concreti. Primo, il trace splicing: un attaccante che invia un trace-id osservato altrove si fa cucire la propria richiesta dentro una traccia esistente, il che inquina il grafo e può esporre i tempi interni a chiunque riesca a leggere quella traccia. Secondo, il consumo di quota: scrivere 01 fisso nel codice forza il campionamento su ogni richiesta, e un’ondata anche modesta si trasforma in una bolletta di ingestion molto salata o, peggio, sfratta le tracce che ti servivano davvero. Terzo, la correlazione tra tenant: riusare uno stesso trace-id su richieste di tenant diversi collega record che il tuo tooling tratta poi come una sola operazione logica.

La posizione pragmatica è accettare sul bordo della rete senza fidarsi. Valida la grammatica e rifiuta gli header malformati invece di passarli verso l’interno. Per il traffico non autenticato, ricalcola la tua decisione di campionamento invece di onorare il flag in ingresso, così nessun client esterno può inchiodare il tuo sampler su “registra sempre”. Per il traffico autenticato, onorare la decisione del chiamante di solito va bene, perché sai chi hai davanti.

E tratta il trace-id come pubblico. Non è un segreto e non lo è mai stato: compare nei log, nelle pagine di errore, negli header di risposta e negli screenshot incollati nei ticket di assistenza. Non codificarci mai dentro un ID utente, il nome di un tenant o qualsiasi altra cosa dotata di significato, e non usarlo mai come chiave di autorizzazione. È un identificatore di correlazione, e non dovrebbe mai essere nient’altro.

FAQ

Qual è la differenza tra traceparent e tracestate?

traceparent porta l’identità standardizzata (trace-id, parent-id e i flag di campionamento) e ogni implementazione deve saperla leggere. tracestate porta stato specifico del vendor, che le implementazioni inoltrano intatto anche quando non lo riconoscono. I due sono legati: quando il traceparent non è valido, la specifica impone di ignorare anche il tracestate.

Perché la mia traccia ricomincia da capo a metà della catena di chiamate?

Una traccia ricomincia da capo a metà catena quasi sempre perché un hop ha ricevuto un header che non rispetta la grammatica, l’ha scartato e ha generato un trace-id nuovo. Esadecimale in maiuscolo, un trace-id tutto a zeri e un header che non è lungo esattamente 55 caratteri causano tutti questo effetto. Se l’header è ben formato, i sospettati successivi sono un proxy che lo elimina e un preflight cross-origin che fallisce.

Devo configurare il CORS per inviare traceparent da un browser?

Sì, la configurazione CORS è obbligatoria. traceparent è un header personalizzato, quindi rende la richiesta non semplice e fa scattare un preflight; il server deve elencare traceparent in Access-Control-Allow-Headers. La strumentazione browser di OpenTelemetry richiede in più la configurazione di propagateTraceHeaderCorsUrls, perché per impostazione predefinita non inietta header di tracing cross-origin.

Come propago il trace context attraverso Kafka o SQS?

Scrivi il valore di traceparent in un header di record Kafka o in un attributo di messaggio SQS lato producer, e rileggilo lato consumer per ripristinare il contesto. Gli SDK OpenTelemetry espongono inject ed extract per questo in ogni linguaggio. Il formato non cambia: rispetto a una mappa di header HTTP cambia solo il carrier.

È sicuro esporre un trace ID nei log o nelle risposte?

Sì, esporre un trace ID è sicuro. È un identificatore casuale, senza nessuna identità incorporata e senza alcun potere di autorizzazione. Correla però record tra sistemi diversi, quindi non codificarci mai dentro un ID utente o il nome di un tenant, e non accettarlo mai come prova di qualcosa. Trattalo come una chiave di correlazione pubblica e puoi tranquillamente scriverlo nei log, restituirlo e condividerlo.

Chi genera l’header traceparent?

L’header traceparent lo genera il primo servizio che riceve una richiesta che ne è priva. Di solito è un proxy di frontiera, un API gateway o un SDK browser: quel servizio diventa la radice della traccia, genera il trace-id, crea il primo span e prende la decisione di campionamento. Ogni hop successivo si limita a riscrivere il parent-id.

L’header traceparent è obbligatorio?

No, l’header traceparent è facoltativo a livello di protocollo. Una richiesta che non lo porta resta perfettamente valida: il servizio che la riceve diventa semplicemente la radice di una traccia nuova. È obbligatorio solo in senso pratico, perché senza di esso il lavoro svolto oltre il confine di un servizio non si può correlare in un’unica traccia.

traceparent aggiunge un overhead misurabile?

Non in modo significativo. Un traceparent pesa 55 byte, e un tracestate ne aggiunge di solito qualche centinaio: trascurabile accanto a un handshake TLS o a un payload reale. Il costo vero del tracing è esportare e conservare gli span campionati, non trasportare gli header di distributed tracing sul filo.

Tag: distributed-tracing opentelemetry observability http-headers w3c

Articoli correlati

Vedi tutti gli articoli