Pular para o conteúdo

Tracing distribuído

Avançado18 min de leituraobservabilidade

A métrica diz que o p95 do checkout dobrou. O log do serviço de checkout diz que a chamada interna demorou. Qual das seis chamadas? O trace responde: ele reconstrói uma requisição inteira, atravessando processos, com o tempo de cada trecho e a relação de causa entre eles.

Um span é uma operação com início, fim, atributos e status. Um trace é a árvore de spans com o mesmo trace_id; cada span aponta para o pai. O contexto é o par trace_id + span_id que precisa viajar junto com a chamada — se ele se perde, você ganha vários traces órfãos em vez de um.

trace 4bf92f35… ── POST /checkout 412 ms
├── validar-carrinho 18 ms
├── consultar-estoque 102 ms
├── autorizar-pagamento 271 ms ← o tempo está aqui
│ └── HTTP POST gateway.cielo 268 ms
└── gravar-pedido 19 ms

O padrão é o cabeçalho traceparent do W3C, propagado em toda saída HTTP, gRPC ou mensagem de fila.

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
│ └ trace_id └ span_id └ flags (amostrado)

Os três pontos onde a propagação quebra na prática:

  • Fila e job assíncrono. O contexto precisa ir dentro da mensagem; ninguém propaga cabeçalho HTTP para o SQS sozinho.
  • Cliente HTTP construído à mão. Instrumentação automática cobre o cliente padrão da linguagem; um http.Client criado sem o transporte instrumentado some do trace.
  • Proxy que remove cabeçalho desconhecido. Ingress, WAF e CDN precisam permitir traceparent e tracestate.

Guardar 100% dos traces em produção custa mais que guardar as métricas — e a maioria dos traces é de requisições rápidas e bem-sucedidas, que ninguém vai olhar.

  • Head sampling: decide no início, pela probabilidade. Simples e barato, mas descarta o erro raro justamente porque ele é raro.
  • Tail sampling: decide depois que o trace terminou, no Collector, olhando o resultado. Guarda todo erro e toda cauda lenta, e amostra o resto.
processors:
tail_sampling:
decision_wait: 10s # espere o trace fechar antes de decidir
policies:
- name: erros
type: status_code
status_code: { status_codes: [ERROR] }
- name: lentos
type: latency
latency: { threshold_ms: 1000 }
- name: amostra-do-resto
type: probabilistic
probabilistic: { sampling_percentage: 5 }

Tail sampling exige que todos os spans de um trace cheguem ao mesmo Collector — com várias réplicas, use um balanceamento por trace_id na frente, ou a decisão sai errada.

A decisão de amostragem precisa ser consistente ao longo do trace: se o serviço da ponta amostra e o do meio não, você recebe pedaços. O flag no traceparent existe exatamente para propagar essa decisão.

Na ordem, e sem pular etapas:

  1. Olhe a duração total e o span de maior duração própria (excluindo os filhos).
  2. Veja se há espaço vazio entre spans — tempo sem span costuma ser fila, GC, espera por conexão ou pool esgotado, e não trabalho.
  3. Conte spans repetidos: cinquenta consultas idênticas é o clássico N+1 disfarçado.
  4. Compare com um trace rápido da mesma rota. A diferença estrutural aparece rápido.
  5. Só então abra os logs daquela requisição pelo trace_id.
Janela do terminal
# Tempo é dado, não impressão: percentil por rota, no Tempo/Jaeger via métricas de span
histogram_quantile(0.95, sum by (le, span_name) (rate(traces_spanmetrics_latency_bucket[5m])))

Grave atributos que você vai querer filtrar (http.route, db.system, messaging.system, identificador de tenant) e não grave corpo de requisição, token ou dado pessoal em atributo de span — o trace vai para o mesmo lugar que o log, com os mesmos riscos.

Próximo passo: para instrumentar uma vez e trocar de backend sem reescrever, veja OpenTelemetry.