Skip to content
Voltar ao blog
Tutoriais

Cabeçalho traceparent: guia completo do W3C Trace Context

O cabeçalho traceparent campo a campo: o que significa cada segmento hexadecimal, o que o invalida e por que os traces quebram. Decodificador online grátis.

13 min de leitura

Cabeçalho traceparent: guia completo do W3C Trace Context

O cabeçalho traceparent é a base do tracing distribuído: uma linha de ASCII que carrega a identidade de uma requisição por todos os serviços em que ela encosta. Na versão atual ele tem exatamente 55 caracteres e quatro campos separados por hífen:

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)

Dois desses campos se comportam de maneiras diferentes conforme a requisição avança. O trace-id continua idêntico em todos os saltos: ele é o nome da requisição, do proxy de borda até a última chamada de banco. O parent-id muda a cada salto, porque ele nomeia o span que chamou você, não a requisição. Confundir os dois responde por boa parte dos chamados de “meus traces estão errados”.

Essa é a anatomia. O mais difícil é o que a tabela de campos não conta: o que torna um cabeçalho inválido, o que um receptor em conformidade faz quando recebe um desses, e onde o cabeçalho some sem avisar entre dois serviços que dizem suportar tracing. Se você tem um cabeçalho real na sua frente, cole no decodificador traceparent gratuito e acompanhe a leitura: ele separa os campos, expande o byte de flags bit a bit e diz qual regra um cabeçalho quebrado violou.

O cabeçalho traceparent em resumo

O cabeçalho traceparent é um único cabeçalho HTTP que leva um trace distribuído de um serviço para o outro. São quatro campos hexadecimais separados por hífen — version, trace-id, parent-id e trace-flags — e, na versão atual, exatamente 55 caracteres. O trace-id nomeia a requisição inteira; o parent-id nomeia o span que chamou você.

CampoDígitos hexBytesO que identificaMuda a cada salto?
version21Qual formato o restante segue. Hoje, sempre 00Não
trace-id3216A requisição inteira, de ponta a pontaNão
parent-id168O span que chamou (o span ID de quem chamou você)Sim
trace-flags21Um campo de 8 bits; o bit 0 é sampledRaramente

Some três hífens a esses 52 dígitos hexadecimais e você chega a 55 caracteres. Decore esse número: um cabeçalho de versão 00 com qualquer outro comprimento é inválido, e comprimento é a primeira coisa que dá para conferir a olho.

Tudo no cabeçalho é hexadecimal minúsculo. Não é “hexadecimal, tanto faz a caixa”. É minúsculo. A gramática da recomendação W3C Trace Context aceita 0-9 e a-f e mais nada, e é por isso que um trace ID em maiúsculas, com um valor perfeitamente correto, ainda assim é jogado fora lá na frente.

Campo a campo

Cada campo tem sua própria largura, seus próprios valores inválidos e seu próprio jeito de dar errado.

version — por que nem sempre é “só 00”

Hoje o byte de versão é 00, e vai continuar 00 por um bom tempo. Mas ff é explicitamente proibido: a especificação o reserva como valor inválido, então um cabeçalho que começa com ff já nasce morto, não importa o que venha depois.

A regra interessante é a das versões que você nunca viu. Um parser que faz if (version !== '00') reject() está errado, e errado de um jeito caro. A especificação pede que o receptor tente fazer o parse quando a versão for maior e o cabeçalho tiver pelo menos o comprimento do formato conhecido: leia os campos que você reconhece, tolere dados extras no final e siga em frente. Rejeitar significa que o seu serviço vira a fronteira onde o trace acaba e outro começa, no instante em que alguém acima na cadeia atualizar.

// Errado: transforma o seu serviço no lugar onde os traces vão morrer
if (version !== '00') throw new Error('bad traceparent');

// Certo: faça o parse do prefixo que você entende
if (version !== '00' && header.length >= 55) {
  // leia version, trace-id, parent-id, trace-flags; ignore o resto
}

trace-id — 16 bytes, a identidade da requisição inteira

Trinta e dois dígitos hexadecimais minúsculos, constantes por toda a vida do trace. Seja qual for o serviço que o gerou no início, cada salto o copia adiante sem mexer. Quando você procura um trace no seu backend de observabilidade, é essa a string que você cola.

Duas regras governam o valor. Ele precisa ter 32 dígitos hex e não pode ser todo zeros. 00000000000000000000000000000000 não é “um trace ainda sem dados”: a especificação nomeia esse valor como inválido e exige que o receptor ignore o cabeçalho inteiro. Na prática, um trace ID todo zerado significa um SDK que nunca inicializou, ou um middleware inserindo um placeholder porque não tinha contexto real para repassar.

Um trace-id tem 128 bits, a mesma largura de um UUID, e não é um UUID. Não há bits de versão, nem bits de variante, nem hífens, nem estrutura de espécie alguma: dezesseis bytes opacos. Você não consegue extrair um v4 dali, e um UUID com os hífens removidos também não vira automaticamente um trace-id válido, porque os nibbles de versão e variante deixam a aleatoriedade irregular. Se quiser ver o que um UUID de fato reserva dentro desses 128 bits, o que um UUID realmente codifica percorre o layout, e o gerador de UUID mostra os bits de versão e variante no lugar.

parent-id — 8 bytes, o span que chamou você

Dezesseis dígitos hexadecimais, reescritos a cada salto. O nome causa mais confusão do que o campo merece: a spec do W3C chama de parent-id, o OpenTelemetry chama esses mesmos 8 bytes de span ID, e são a mesma coisa vista de dois ângulos. Do ponto de vista do seu serviço é o pai; do ponto de vista de quem chamou, é o ID do span que ele acabou de criar para a requisição de saída.

Então, quando o serviço A chama o serviço B, A coloca o próprio span ID no lugar do parent-id. B cria um span filho e, quando B chama C, coloca ali o span ID do B. O trace-id fica intocado o tempo todo. É esse o algoritmo de propagação inteiro.

parent-ids todos zerados também são inválidos, pelo mesmo motivo dos trace-ids: 0000000000000000 significa que quem chamou não forneceu um span de verdade, e o cabeçalho deve ser descartado em vez de aproveitado pela metade.

trace-flags — parece um booleano, mas são oito bits

Quase todo cabeçalho que você vai encontrar termina em 01, então é natural ler o campo como um sim/não. Ele é um byte, e os bits têm significados atribuídos:

  • bit 0, máscara 0x01sampled
  • bit 1, máscara 0x02random-trace-id, adicionado no Trace Context Level 2
  • bits 2–7 — reservados; ignore-os na recepção, zere-os nas requisições de saída

Veja no que as combinações decodificam:

HexBináriosampledrandom-trace-idflags === 0x01 vale?
0000000000falsefalsefalse
0100000001truefalsetrue
0200000010falsetruefalse
0300000011truetruefalse ← o bug

Leia a última linha de novo. Um trace com flags 03 está amostrado. Qualquer código que compare o byte inteiro com 01 o reporta como não amostrado, em silêncio, e só para a fatia de tráfego em que o flag do Level 2 por acaso está ligado. É o pior formato de falha possível, porque parece problema de taxa de amostragem, e não bug de parsing.

const flags = parseInt(traceFlags, 16);

// Errado: trata um campo de bits como uma enumeração
const sampled = traceFlags === '01';

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

O que o random-trace-id afirma, na prática? Que pelo menos os 7 bytes mais à direita do trace-id foram gerados com aleatoriedade uniforme. Isso soa acadêmico até você pensar em amostragem consistente: se um sistema lá na frente quer guardar 1% dos traces e precisa que todo serviço concorde de forma independente sobre quais 1%, ele pode pegar esses bytes módulo alguma coisa em vez de fazer hash do ID antes. O flag é a promessa de quem está acima na cadeia de que isso é seguro.

O que torna um traceparent inválido

As bases de conhecimento dos fornecedores costumam pular essa parte, que é justamente a dúvida com que as pessoas chegam. Para um cabeçalho de versão 00, a lista de rejeições é esta:

SintomaRegraResultado
00-4BF92F35...-01A gramática só aceita hex minúsculoInválido — o valor está certo, o cabeçalho é rejeitado
ff-...A versão ff é proibida pela specInválido
trace-id é 00000000000000000000000000000000trace-id todo zerado é um valor inválido nomeadoInválido
parent-id é 0000000000000000parent-id todo zerado é um valor inválido nomeadoInválido
trace-id não tem 32 dígitos hexLargura fixaInválido
parent-id não tem 16 dígitos hexLargura fixaInválido
trace-flags não tem 2 dígitos hexLargura fixaInválido
O cabeçalho não tem exatamente 55 caracteres, versão 00Dados extras no fim só são legais sob uma versão futuraInválido
Qualquer caractere fora de 0-9a-f e dos hífensNão é hexadecimalInválido

A consequência disso:

Um receptor em conformidade não conserta um cabeçalho traceparent inválido nem o repassa adiante. Ele descarta o cabeçalho e inicia um trace novinho em folha, com um trace-id recém-gerado.

Ou seja, o sintoma na sua tela não é um trace quebrado. São dois traces curtos e desconectados: um que termina de repente no serviço que emitiu o cabeçalho ruim, e outro que parece começar do nada no serviço que o recebeu. Nada é marcado como erro em lugar nenhum. Os dois traces parecem saudáveis quando olhados isoladamente. As pessoas passam tardes atrás do elo perdido entre eles quando a resposta é que um middleware colocou uma string hex em maiúsculas, ou que um cabeçalho montado à mão saiu com 54 caracteres.

Comprimento e caixa são os dois modos de falha que você não enxerga só olhando. Cole o cabeçalho no decodificador e ele aponta exatamente a regra violada, em vez de fazer você contar dígitos.

tracestate: o cabeçalho companheiro que todo mundo entende errado

O traceparent carrega a identidade padronizada. O cabeçalho tracestate carrega o que cada fornecedor quiser acrescentar ao lado, como membros key=value separados por vírgula:

tracestate: rojo=00f067aa0ba902b7,congo=t61rcWkgMzE

Uma implementação que não reconhece uma chave precisa repassá-la intocada. Esse é o objetivo do design: fornecedores podem pegar carona com estado proprietário em cima de um trace padrão sem que cada salto precise entendê-lo.

Mas a gramática tem dentes, e três das suas regras explicam sintomas reais de produção.

32 list-members é um teto rígido. Isso não é conselho, é a gramática: list = list-member 0*31( OWS "," OWS list-member ). Um tracestate com 33 membros é um cabeçalho inválido, e os receptores têm o direito de descartar a coisa toda. Essa é a resposta para um sintoma que de outro modo parece mágica: dados do fornecedor presentes na borda, presentes dois saltos adiante e completamente sumidos no quinto salto. Cada salto vinha anexando o próprio membro, a lista passou de 32 e, dali em diante, o cabeçalho inteiro era descartado em vez de aparado.

Valores têm de 1 a 256 caracteres e nunca podem ser vazios. A produção do valor termina com um caractere obrigatoriamente não-branco, então vendor= não é “uma chave sem valor”, e sim um erro de sintaxe. Só ASCII imprimível, e nunca uma vírgula ou um sinal de igual dentro do valor.

A gramática das chaves mudou entre o Level 1 e o Level 2. O Level 1 definia chaves por uma produção tenant@vendor, em que @ era um separador estrutural. O Level 2 trocou isso por uma classe de caracteres plana: a chave começa com uma letra minúscula ou um dígito e continua com a-z, 0-9, _, -, *, / e @. Sob o Level 2, @ é um caractere comum, chaves podem começar com dígito, e a@b@c é uma chave perfeitamente legal que a produção do Level 1 rejeitaria. Se você tem um proxy validando contra o Level 1 e um serviço emitindo chaves do Level 2, um lado aceita o que o outro rejeita, e o cabeçalho some em exatamente um salto.

Faltam duas regras. Chaves duplicadas são inválidas, ponto. E quando você modifica o parent-id do traceparent, precisa mover a sua própria entrada de tracestate para o começo da lista: a lista é ordenada do mais recente para o mais antigo. Pular esse passo deixa estado velho de fornecedor num lugar onde quem lê vai tratá-lo como atual.

Por fim, a regra simpática: membros vazios são legais. Quando um middlebox remove uma entrada, costuma deixar a vírgula para trás, produzindo rojo=1,,congo=2. A spec permite isso explicitamente, então um parser deve descartar o membro vazio e continuar em vez de declarar o cabeçalho malformado. A visão de tracestate no decodificador lista cada membro com validação individual e uma contagem corrente contra o limite de 32 membros, o que costuma ser mais rápido do que contar vírgulas.

Como os cabeçalhos de tracing distribuído viajam: uma requisição, quatro saltos

Acompanhe uma requisição por um proxy de borda, um serviço de API e dois serviços adiante:

Cliente
  │  (sem traceparent — a borda é a raiz)

Proxy de borda      gera trace-id 4bf9…4736, span 00f0…02b7
  │  traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

Serviço de API      lê o cabeçalho, cria o span a1b2c3d4e5f60718
  │  traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-a1b2c3d4e5f60718-01

Serviço de pedidos  lê o cabeçalho, cria o span 9f8e7d6c5b4a3928
  │  traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-9f8e7d6c5b4a3928-01

Serviço de estoque

Todo salto faz as mesmas três coisas: lê o cabeçalho que chegou, troca o parent-id pelo próprio span ID em cada chamada de saída e repassa o trace-id e as flags sem alteração. Quando não chega cabeçalho nenhum, como acontece no cliente lá em cima, o serviço que recebe é a raiz: ele gera um trace-id e toma a decisão de amostragem para tudo que vem depois.

Dá para injetar um cabeçalho na mão e testar uma cadeia de ponta a ponta:

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

Reproduza contra o ambiente de homologação um cabeçalho capturado em produção e você verá o mesmo trace-id aparecer no seu backend. O gerador de comando cURL monta as flags para você quando for preciso adicionar autenticação ou um corpo, e a folha de consulta do curl cobre as opções de cabeçalho e de verbosidade que você vai querer durante a depuração.

Para ver o que um serviço realmente recebeu, e não o que você acha que enviou, suba um servidor de eco descartável e aponte um salto para ele:

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

Depois rode curl -H 'traceparent: …' http://127.0.0.1:8080/ e leia o que saiu do outro lado. Metade das investigações de “o proxy está comendo o meu cabeçalho” termina aqui.

trace-flags sampled: uma decisão de quem está acima na cadeia, não um recibo

Um bit sampled igual a 1 em trace-flags significa que o serviço acima na cadeia decidiu gravar esse trace. Não é promessa de que os dados chegaram ao seu backend.

A amostragem head-based toma essa decisão na raiz, antes de qualquer coisa acontecer, e a propaga para baixo: barata, consistente entre serviços e cega, porque não tem como saber que a requisição estava prestes a falhar. A amostragem tail-based guarda os spans em buffer até o trace terminar e só então decide, então consegue manter todo trace que contenha um erro, ao custo de segurar spans em memória e de exigir que os spans de todos os serviços caiam no mesmo coletor.

Sob amostragem tail-based, um trace pode chegar marcado como 01 em todos os saltos e mesmo assim ser descartado no fim. Limites de taxa e cotas de exportação também podem derrubá-lo. Então 01 na borda e nenhum trace na interface não é necessariamente um bug de propagação; confira as métricas de descarte do próprio coletor antes de sair olhando cabeçalho.

O caso inverso importa mais no dia a dia. Se as flags que chegaram são 00, quem chamou rodou o sampler dele e escolheu não gravar. Nada no seu serviço está mal configurado, e auditar o seu próprio sampler é tempo perdido: a pergunta é qual serviço acima está decidindo contra a amostragem.

Convertendo entre formatos de propagação

O W3C Trace Context venceu, mas muita gente ainda fala algo mais antigo, e os gateways traduzem entre eles. O mesmo exemplo de traceparent escrito em quatro formatos:

FormatoCabeçalho(s)Valor no nosso exemplo
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: a divisão em 64 bits altos e baixos

Os identificadores do Datadog são anteriores aos trace IDs de 128 bits, e a camada de compatibilidade é onde a maioria das conversões dá errado. O x-datadog-trace-id carrega os 64 bits inferiores como string decimal. Os 64 bits superiores viajam separados, em hexadecimal, na tag _dd.p.tid, que por sua vez viaja dentro do cabeçalho 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

O erro clássico é converter os 128 bits inteiros em um único número decimal:

BigInt('0x' + traceId).toString(10);
// 100985939111033328018442752961257817910 — não casa com nada na interface

Esse valor não é aritmética errada. É a representação decimal correta da quantidade errada, e é por isso que ele sobrevive à revisão e depois, caladinho, não casa com trace nenhum.

A segunda armadilha é precisão numérica. Um identificador de 64 bits ultrapassa Number.MAX_SAFE_INTEGER, que é 9007199254740991, então qualquer caminho de código que deixe um trace ID virar um number do JavaScript corrompe os dígitos menos significativos. Mantenha trace IDs como strings e recorra a BigInt só quando precisar fazer aritmética; um ID que chega sem aspas no JSON já vem danificado antes de você olhar para ele.

AWS X-Ray: o timestamp que não existe

Um trace ID do X-Ray tem o formato 1-{8 hex}-{24 hex}, e os 8 primeiros dígitos hexadecimais são o horário de criação em segundos de época. Converter a partir do W3C é mecânico:

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

Olhe essa data. O cabeçalho de exemplo da especificação decodifica para maio de 2010, o que é um absurdo evidente, e é exatamente esse o ponto. Um trace-id do W3C não contém timestamp nenhum. Dezesseis bytes aleatórios produzem sem esforço uma época de aparência plausível quando você lê os quatro primeiros como um número só, e esse número não significa nada, a menos que o identificador tenha nascido mesmo no X-Ray. Decodificar um horário de um trace-id qualquer é ler um número aleatório e acreditar nele.

Quando o ID veio de fato do X-Ray, a conversão é útil: jogue esses oito dígitos hexadecimais no conversor de timestamp Unix para obter uma data legível, e o guia de época cobre as armadilhas de segundos versus milissegundos e de fuso horário que vêm logo depois.

B3: a linhagem do Zipkin

O B3 veio do Zipkin e é o formato que você encontra em service meshes mais antigos. A forma de cabeçalho único é traceId-spanId-sampled, em que o campo sampled é 1 ou 0 em vez de um byte hexadecimal, então o bit random-trace-id do Level 2 não tem para onde ir e simplesmente se perde na tradução. A forma multi-cabeçalho divide os mesmos valores entre X-B3-TraceId, X-B3-SpanId e X-B3-Sampled.

A ruga histórica é a largura. Trace IDs do B3 podem ter 64 bits, ou seja, 16 dígitos hexadecimais em vez de 32. Converter um ID B3 de 64 bits para W3C significa preencher à esquerda com zeros até chegar a 32 dígitos, e converter de volta significa decidir se vai truncar. Preencher à esquerda é seguro; truncar não é, porque dois traces que diferem apenas nos bytes altos colapsam em um só.

Onde o traceparent se perde em produção

Tudo acima pressupõe que o cabeçalho chega. Muitas vezes não chega, e são quatro os lugares em que ele some.

O navegador o descarta em chamadas cross-origin

Sintoma: existem traces do frontend, existem traces do backend, e nada liga os dois. Ou a requisição cross-origin falha de vez com um erro de CORS.

Causa: traceparent é um cabeçalho personalizado, então incluí-lo torna a requisição não-simples e dispara um preflight OPTIONS. Se a resposta de preflight do servidor não listar o cabeçalho em Access-Control-Allow-Headers, o navegador bloqueia a requisição de verdade. E a instrumentação de navegador do OpenTelemetry ainda se recusa a injetar cabeçalhos de trace em requisições cross-origin, a não ser que você diga a ela quais origens são permitidas.

Correção: no servidor, devolva Access-Control-Allow-Headers: traceparent, tracestate no preflight. No SDK do navegador, configure propagateTraceHeaderCorsUrls com um padrão que case com as origens da sua API. Os dois são necessários; qualquer um deles sozinho deixa você com o mesmo sintoma. Um preflight que volta com um status inesperado merece uma conferida no guia de códigos de status HTTP antes de você concluir que o problema é o cabeçalho.

Proxies, WAFs e balanceadores de carga removem cabeçalhos desconhecidos

Sintoma: o cabeçalho aparece quando você faz curl direto no serviço e some quando a mesma requisição passa pelo gateway.

Causa: encaminhamento baseado em allowlist. Muita configuração de proxy, muito conjunto de regras de WAF e muito balanceador de carga gerenciado só encaminham os cabeçalhos que reconhecem, e traceparent não está na lista padrão. Alguns meshes também reescrevem o cabeçalho, gerando o próprio trace-id e jogando o seu fora.

Correção: faça uma bissecção com o servidor de eco de agora há pouco: coloque-o atrás de cada salto, um por vez, e veja qual camada derruba o cabeçalho. Depois libere explicitamente traceparent e tracestate nas regras de encaminhamento daquela camada. Se o proxy for o nginx, lembre que o bloco que atende uma rota decide quais cabeçalhos ele repassa, e nem sempre o bloco que atende uma rota é o que você imagina; as regras de prioridade de location no nginx explicam por que uma configuração de cabeçalho pode parecer simplesmente ignorada.

Filas de mensagens não têm cabeçalhos HTTP

Sintoma: o trace termina no instante em que uma requisição vira um job em segundo plano.

Causa: não existe requisição HTTP naquela fronteira, então não há onde propagar o cabeçalho. O Kafka tem record headers, o SQS tem atributos de mensagem, e nenhum dos dois é preenchido para você pela instrumentação de HTTP.

Correção: injete o contexto na mensagem do lado do produtor e extraia do lado do consumidor. Todo SDK do OpenTelemetry expõe inject e extract exatamente para isso, e o formato na rede é a mesma string do W3C; só o carrier muda de um mapa de cabeçalhos HTTP para metadados de mensagem. A documentação de propagadores do OpenTelemetry cobre a interface de carrier por linguagem.

Caixa alta e baixa, e o que o HTTP/2 realmente deixa em minúsculas

Sintoma: confusão em code review sobre se Traceparent é aceitável.

Causa: duas regras separadas viram uma só na cabeça das pessoas. Nomes de cabeçalho no HTTP/1.1 não diferenciam maiúsculas de minúsculas, e o HTTP/2 exige que sejam codificados em minúsculas na rede. Isso vale para o nome. Independentemente disso, o hexadecimal no valor do cabeçalho precisa estar em minúsculas, porque a gramática do W3C manda, e nenhuma versão de protocolo vai resolver isso por você.

Correção: envie o nome como traceparent e nunca coloque o valor em maiúsculas. Um gateway que normaliza nomes de cabeçalho não vai normalizar os seus dígitos hexadecimais, e um trace-id em maiúsculas atravessa numa boa toda a camada de transporte antes de ser rejeitado pela aplicação que finalmente faz o parse.

Dá para confiar em um traceparent que chega de fora?

Um traceparent que vem da internet pública é entrada controlada pelo usuário: uma string que um cliente anônimo escolheu, e que a maioria dos serviços aceita sem pensar duas vezes.

Isso abre três riscos concretos. Primeiro, emenda de traces: quem envia um trace-id observado em outro lugar consegue costurar a própria requisição dentro de um trace existente, o que polui o grafo e pode expor tempos internos para qualquer um que consiga ler aquele trace. Segundo, queima de cota: fixar 01 no código força a amostragem em toda requisição, e uma enxurrada modesta vira uma conta de ingestão enorme ou, pior, expulsa os traces de que você realmente precisava. Terceiro, correlação entre tenants: reutilizar um mesmo trace-id em requisições de tenants diferentes liga registros que a sua ferramenta passa a tratar como uma operação lógica só.

Aceite na borda, mas não confie. Valide a gramática e rejeite cabeçalhos malformados em vez de repassá-los para dentro. Para tráfego não autenticado, refaça a sua própria decisão de amostragem em vez de honrar o flag que chegou, para que nenhum cliente externo consiga travar o seu sampler em “sempre gravar”. Para tráfego autenticado, honrar a decisão de quem chamou costuma ser tranquilo, porque você sabe quem é.

E trate o trace-id como público. Ele não é segredo e nunca foi: aparece em logs, em páginas de erro, em cabeçalhos de resposta e em capturas de tela coladas em chamados de suporte. Nunca codifique um ID de usuário, um nome de tenant ou qualquer outra coisa significativa dentro dele, e nunca o use como chave de autorização. Ele é um identificador de correlação, e é só isso que ele deveria ser.

FAQ

Qual é a diferença entre traceparent e tracestate?

O traceparent carrega a identidade padronizada (trace-id, parent-id e as flags de amostragem), e toda implementação precisa entendê-lo. O tracestate carrega estado específico de cada fornecedor, que implementações desconhecidas repassam intocado. Os dois são ligados: quando o traceparent é inválido, a spec exige que o tracestate seja ignorado também.

Por que meu trace recomeça no meio da cadeia de chamadas?

Um trace recomeça no meio da cadeia quase sempre porque um salto recebeu um cabeçalho reprovado na gramática, descartou-o e gerou um trace-id novo. Hex em maiúsculas, um trace-id todo zerado e um cabeçalho que não tem exatamente 55 caracteres causam isso. Se o cabeçalho está bem formado, os próximos suspeitos são um proxy removendo-o e um preflight cross-origin falhando.

Preciso configurar CORS para enviar traceparent de um navegador?

Sim, configurar CORS é obrigatório. O traceparent é um cabeçalho personalizado, então torna a requisição não-simples e dispara um preflight; o servidor precisa listar traceparent em Access-Control-Allow-Headers. A instrumentação de navegador do OpenTelemetry ainda exige o propagateTraceHeaderCorsUrls configurado, porque por padrão ela não injeta cabeçalhos de trace cross-origin.

Como propago o contexto de trace por Kafka ou SQS?

Escreva o valor do traceparent em um record header do Kafka ou em um atributo de mensagem do SQS, do lado do produtor, e leia de volta do lado do consumidor para restaurar o contexto. Os SDKs do OpenTelemetry expõem inject e extract para isso em todas as linguagens. O formato não muda; só o carrier difere de um mapa de cabeçalhos HTTP.

É seguro expor um trace ID em logs ou respostas?

Sim, expor um trace ID é seguro: um identificador aleatório, sem identidade embutida e sem poder de autorização. Ele correlaciona registros entre sistemas, isso sim, então nunca codifique um ID de usuário ou nome de tenant dentro dele, e nunca o aceite como prova de coisa alguma. Trate-o como chave pública de correlação e é seguro registrar, devolver e compartilhar.

Quem gera o cabeçalho traceparent?

Quem gera o traceparent é o primeiro serviço a receber uma requisição que chega sem ele, normalmente um proxy de borda, um API gateway ou um SDK de navegador. Esse serviço vira a raiz do trace: gera o trace-id, cria o primeiro span e toma a decisão de amostragem. Todo salto depois dele só reescreve o parent-id.

O cabeçalho traceparent é obrigatório?

Não, o traceparent não é obrigatório: no nível do protocolo ele é opcional, e uma requisição sem ele é perfeitamente válida; quem recebe apenas vira a raiz de um trace novo. A obrigatoriedade é prática. Sem ele, o trabalho feito dos dois lados de uma fronteira de serviço não tem como ser correlacionado em um único trace.

O traceparent adiciona overhead mensurável?

Nada relevante. Um traceparent tem 55 bytes, e um tracestate normalmente acrescenta mais algumas centenas, o que é desprezível diante de um handshake TLS ou de qualquer payload real. O custo real do tracing é exportar e armazenar os spans amostrados, não carregar cabeçalhos de tracing distribuído pela rede.

Tags: distributed-tracing opentelemetry observability http-headers w3c

Artigos relacionados

Ver todos os artigos