Blog

Amostragem de trace em produção: guardar o que explica o incidente

O incidente aconteceu às 03h14, durou onze minutos e afetou quarenta clientes. Você abre o painel de traces, filtra pela janela, e encontra oitocentos traces de requisições que funcionaram perfeitamente. Os que falharam foram descartados pelo amostrador, porque ele sorteia um por cento das requisições e não faz ideia do que é interessante. Este artigo mostra por que a amostragem uniforme guarda exatamente o volume errado, por que decidir no início da requisição é a raiz do problema e o que muda quando a decisão vai para o fim, como escrever uma política que garante o raro e limita o comum sem estourar orçamento, por que a decisão precisa viajar no contexto de propagação para o trace não sair pela metade, o que quebra nas métricas quando você agrega em cima de dados amostrados, e como validar a política antes de descobrir na próxima madrugada que ela não guardou nada.

2026-08-20 / Observabilidade / 16 min

01

A amostragem uniforme guarda o volume errado

O amostrador padrão da maioria dos SDKs é probabilístico e fixo: cada trace tem a mesma chance de ser guardado, geralmente algo entre um e dez por cento. A escolha faz sentido do ponto de vista de custo, porque o volume de traces cresce junto com o tráfego e a fatura de armazenamento é linear. O problema é que ela otimiza a estatística e destrói a investigação. Amostrar de forma uniforme significa preservar a distribuição do tráfego, e a distribuição do tráfego é dominada por requisições que deram certo. Você acaba com uma amostra fiel do que já sabia e uma amostra vazia do que precisa entender.

Vale fazer a conta, porque ela é mais brutal do que parece. Um endpoint com dez mil requisições por minuto e taxa de erro de zero vírgula um por cento produz dez erros por minuto. Com amostragem de um por cento, a expectativa é de zero vírgula um erro guardado por minuto: um a cada dez minutos. Um incidente de cinco minutos produz, em média, meio trace de erro. Metade das vezes você não terá nenhum. E note que a taxa de erro não precisa ser baixa para o problema aparecer: qualquer classe de falha que seja rara em relação ao tráfego total sofre o mesmo, inclusive a cauda de latência, que é onde quase toda investigação de performance começa.

EstratégiaComo decideCustoO que perde
Uniforme fixaSorteio no início, taxa única para tudoPrevisível e linear no tráfegoErros e cauda de latência, que são raros por definição
Por taxa de cabeça (head-based)Sorteio no início, taxa diferente por rota ou clientePrevisível, ajustável por segmentoAinda não sabe se a requisição falhou quando decide
Por resultado (tail-based)Decide no fim, com latência, status e erro em mãosExige buffer dos spans até o trace fecharTraces muito longos, se o buffer expirar antes
Sempre ligada em erro, cota no sucessoRegra explícita por classe de traceTeto por classe, não pelo tráfego totalNada relevante, desde que a cota seja monitorada

O objetivo da amostragem não é reduzir volume, é reduzir volume redundante. Cem traces idênticos de um checkout bem-sucedido explicam a mesma coisa que um. Um trace de um checkout que estourou o timeout do gateway explica algo que nenhum outro registro do sistema explica. A política certa parte dessa assimetria em vez de tratar toda requisição como igualmente informativa.

02

A decisão no início é o problema, não a taxa

A amostragem de cabeça decide no primeiro span, quando o trace nasce. Naquele instante você sabe a rota, o método, talvez o identificador do cliente, e não sabe nada do que interessa: se vai falhar, quanto vai demorar, se vai cair no caminho lento, se a tool externa vai estourar. Você está apostando em quais requisições serão interessantes antes de qualquer coisa interessante acontecer. Por isso aumentar a taxa não resolve: subir de um para dez por cento multiplica por dez o custo e continua guardando dez vezes mais do que já funcionava.

A amostragem de cauda inverte a ordem. Todos os spans são emitidos, um coletor os agrupa por identificador de trace, e a decisão de guardar ou descartar só é tomada quando o trace termina ou quando um tempo limite expira. Aí você tem os fatos: houve erro, a duração passou de um limiar, uma tool específica foi chamada. O preço é operacional e precisa ser dito com honestidade: o coletor passa a segurar spans em memória durante uma janela, o que exige que traces do mesmo identificador cheguem ao mesmo coletor, o que por sua vez exige roteamento consistente por identificador quando há mais de uma instância.

AMOSTRAGEM DE CABECA (head-based)
  requisicao --> [sorteio 1%] --> descartada
                      |
                      +-- decidiu sem saber o resultado
  (o erro que aconteceu 300ms depois nunca foi gravado)

AMOSTRAGEM DE CAUDA (tail-based)
  requisicao --> spans --> [buffer do coletor por trace_id]
                                    |
                          trace fecha ou expira janela
                                    |
                      +-------------+-------------+
                      |                           |
                 tem erro?                   duracao > p99?
                 SEMPRE guarda               SEMPRE guarda
                      |                           |
                      +-----------+---------------+
                                  |
                          nenhum dos dois
                                  |
                        guarda 1 em N (cota)

Existe um meio-termo que funciona bem e custa pouco: manter a amostragem de cabeça generosa para o tráfego comum e adicionar uma regra de força bruta para as classes raras. Se a rota é sabidamente crítica, ou se o cliente está numa lista de contas grandes, ou se um cabeçalho de depuração está presente, a decisão é forçada para guardar sem sorteio. Isso não substitui a amostragem de cauda, mas cobre boa parte do valor prático antes de você ter que operar um coletor com estado.

03

Escrevendo a política: garantir o raro, limitar o comum

Uma política útil tem duas metades. A primeira é uma lista de condições que forçam a retenção, sem sorteio nenhum, porque o volume delas já é naturalmente pequeno: erro, latência acima do limiar, presença de um cabeçalho de depuração, rota em observação após um deploy. A segunda é uma cota para o que sobrou, e a cota importa mais do que a porcentagem. Uma taxa percentual em cima de um pico de tráfego produz um pico proporcional de custo, exatamente no momento em que você menos quer surpresa. Um teto por segundo produz um custo com limite superior conhecido.

// tracing/sampling-policy.js
// Politica de amostragem por resultado: decide quando o trace fecha,
// com status, duracao e atributos ja disponiveis.
//
// Duas metades:
//   1) condicoes que SEMPRE guardam (o raro que explica incidente)
//   2) cota por segundo para o resto (o comum, com teto de custo)

const KEEP = { decision: 'keep', rate: 1 };
const DROP = { decision: 'drop', rate: 0 };

// Limiares de latencia por rota, derivados do p99 observado em producao.
// Nao invente numero redondo: um limiar acima do p99 real nunca dispara,
// e um limiar abaixo do p95 guarda um terco do trafego sem querer.
const LATENCY_THRESHOLD_MS = {
  'POST /checkout': 2500,
  'POST /chat': 8000,
  default: 1500,
};

// Cota simples de token bucket: N traces por segundo para o trafego normal.
// O teto e absoluto, entao um pico de trafego nao vira pico de fatura.
function createQuota(perSecond) {
  let tokens = perSecond;
  let lastRefill = 0;

  return function take(nowMs) {
    const elapsed = (nowMs - lastRefill) / 1000;
    if (elapsed > 0) {
      tokens = Math.min(perSecond, tokens + elapsed * perSecond);
      lastRefill = nowMs;
    }
    if (tokens < 1) return false;
    tokens -= 1;
    return true;
  };
}

const normalTrafficQuota = createQuota(5); // 5 traces/s de trafego saudavel

export function decide(trace, nowMs) {
  const { route, status, durationMs, attributes = {} } = trace;

  // 1) Sempre guardar: erro de servidor e a classe mais rara e mais util.
  if (status >= 500) return { ...KEEP, reason: 'server_error' };

  // Erro de cliente tambem interessa, mas so o que indica bug nosso:
  // 429 e 401 em volume sao esperados e poluiriam a amostra.
  if (status === 400 || status === 422) return { ...KEEP, reason: 'client_error' };

  // 2) Sempre guardar: cauda de latencia, que e onde a investigacao comeca.
  const threshold = LATENCY_THRESHOLD_MS[route] ?? LATENCY_THRESHOLD_MS.default;
  if (durationMs > threshold) return { ...KEEP, reason: 'slow' };

  // 3) Sempre guardar: pedido explicito de depuracao vindo do cabecalho.
  if (attributes['debug.force_sample'] === true) {
    return { ...KEEP, reason: 'forced' };
  }

  // 4) O resto disputa a cota. Sem cota, um pico de trafego saudavel
  //    afoga o orcamento e empurra o raro para fora da retencao.
  if (normalTrafficQuota(nowMs)) return { ...KEEP, reason: 'quota' };

  return { ...DROP, reason: 'sampled_out' };
}

Repare no campo reason. Ele não é decoração: é o que permite responder mais tarde por que um trace específico está ou não no armazenamento, e é o que você agrega para saber se a política está fazendo o que promete. Uma política sem motivo registrado é uma caixa preta dentro do sistema que existe justamente para eliminar caixas pretas.

O limiar de latência merece atenção porque é onde a política costuma degradar sozinha. Se você fixa dois segundos e o sistema fica mais rápido ao longo do tempo, o limiar deixa de disparar e a cauda some da amostra. Se o sistema fica mais lento, o limiar passa a disparar em metade do tráfego e o custo explode. Derivar o limiar do percentil observado, recalculado periodicamente e com um piso e um teto absolutos, mantém a política estável enquanto o sistema muda.

04

A decisão precisa viajar junto com o trace

Um trace atravessa serviços, e cada serviço tem o seu próprio amostrador. Se cada um decide por conta própria, o resultado é um trace pela metade: o serviço de borda guardou o span de entrada, o serviço de pagamento sorteou descartar o dele, e o que sobra no painel é uma árvore com buracos que sugere que a requisição terminou onde ela na verdade continuou. Esse é o modo de falha mais insidioso da amostragem, porque não parece dado faltando, parece dado errado.

A solução é padronizada e vale usá-la em vez de inventar cabeçalho próprio. O contexto de propagação do W3C carrega, no cabeçalho traceparent, um byte de flags cujo bit menos significativo indica se o trace foi amostrado. Todo serviço a jusante lê esse bit e o respeita em vez de sortear de novo. Um serviço só decide quando é a raiz do trace, isto é, quando não recebeu contexto de ninguém.

// tracing/propagation.js
// Le a decisao de amostragem do contexto recebido e so decide localmente
// quando este servico e a raiz do trace.
//
// Formato do traceparent (W3C Trace Context):
//   00-<trace-id 32 hex>-<parent-id 16 hex>-<flags 2 hex>
//   flags bit 0 (0x01) = sampled

const SAMPLED_FLAG = 0x01;

export function parseTraceparent(header) {
  if (typeof header !== 'string') return null;

  const parts = header.split('-');
  if (parts.length !== 4) return null;

  const [version, traceId, parentId, flags] = parts;
  if (version !== '00') return null;
  if (!/^[0-9a-f]{32}$/.test(traceId) || /^0+$/.test(traceId)) return null;
  if (!/^[0-9a-f]{16}$/.test(parentId) || /^0+$/.test(parentId)) return null;
  if (!/^[0-9a-f]{2}$/.test(flags)) return null;

  return {
    traceId,
    parentId,
    sampled: (parseInt(flags, 16) & SAMPLED_FLAG) === SAMPLED_FLAG,
  };
}

export function shouldSample(incomingHeader, decideLocally) {
  const parent = parseTraceparent(incomingHeader);

  // Contexto valido recebido: respeitar a decisao de quem comecou o trace.
  // Sortear de novo aqui e o que produz arvore com buracos no painel.
  if (parent) return { sampled: parent.sampled, traceId: parent.traceId };

  // Somos a raiz: agora sim a decisao e nossa.
  return { sampled: decideLocally(), traceId: null };
}

export function buildTraceparent({ traceId, spanId, sampled }) {
  const flags = sampled ? '01' : '00';
  return `00-${traceId}-${spanId}-${flags}`;
}

Há uma tensão real aqui com a amostragem de cauda, e ela precisa ser reconhecida: se a decisão só acontece no fim, o bit de propagação não pode carregá-la, porque no momento da chamada a jusante ninguém sabe ainda. Nesse desenho, o bit é mantido ligado durante toda a requisição para que todos os spans sejam emitidos, e quem descarta é o coletor, depois, olhando o trace inteiro. O custo se desloca da retenção para o transporte, o que costuma ser um bom negócio, mas não é de graça e precisa ser dimensionado.

05

O que quebra nas métricas quando você amostra

O erro mais caro depois de acertar a política é continuar calculando métricas em cima dos traces guardados. Se você retém cem por cento dos erros e uma fração do sucesso, a taxa de erro calculada sobre a amostra é absurdamente maior que a real, e o painel passa a mentir de forma sistemática. O mesmo vale para latência: se a política guarda toda a cauda e uma amostra do corpo da distribuição, o percentil calculado sobre os traces retidos é muito pior que o percentil verdadeiro.

A separação correta é conceitual antes de ser técnica. Métricas devem sair de contadores e histogramas emitidos em cem por cento das requisições, porque são baratos e agregados na origem. Traces devem responder a pergunta de investigação, ou seja, dado que a métrica acusou um problema, mostrar exemplos concretos que o expliquem. Métrica diz que existe e quanto; trace diz por quê. Tentar tirar as duas respostas da mesma fonte amostrada arruína as duas.

PerguntaFonte corretaPor que não a outra
A taxa de erro subiu?Contador de requisições por status, sem amostragemTrace amostrado por política superestima erro por construção
Qual é o p95 da rota?Histograma de latência agregado na origemAmostra enviesada para a cauda infla o percentil
Por que esta requisição demorou?Trace com spans por etapaMétrica agregada não guarda o caminho individual
Qual dependência causou o incidente?Traces retidos pela regra de erroContador mostra o sintoma, não a cadeia de chamadas

Se por algum motivo a métrica precisa mesmo sair da amostra, então cada trace retido precisa carregar o peso da sua decisão, isto é, o inverso da probabilidade com que foi guardado, e toda contagem tem que somar pesos em vez de somar linhas. É o que instrumentos maduros fazem internamente. É também mais uma razão para registrar o motivo e a taxa efetiva junto do trace, em vez de descobrir depois que não dá para reconstruir o peso.

06

Validar a política antes do próximo incidente

Uma política de amostragem é código que decide o que você vai poder ver no futuro, e a única forma de descobrir que ela está errada por acidente é durante um incidente, no pior momento possível. Por isso ela precisa de teste como qualquer outra regra de negócio, e de um painel que a observe em produção. As duas coisas são simples e quase nunca são feitas.

  1. Teste de retenção do raro: monte traces sintéticos com status 500, com latência acima do limiar e com o cabeçalho de depuração, e afirme que a decisão é guardar em todos, independentemente da cota.
  2. Teste de teto da cota: dispare mil traces saudáveis dentro do mesmo segundo e afirme que o número de retidos respeita o teto configurado, e que os descartados trazem o motivo correto.
  3. Teste de precedência: um trace que é lento e que também estourou a cota tem que ser guardado pela regra de latência, provando que a cota nunca vence uma condição de retenção obrigatória.
  4. Teste de propagação: dado um traceparent recebido com o bit ligado e outro com o bit desligado, afirme que o serviço respeita a decisão recebida e não sorteia de novo.
  5. Painel de decisões: agregue a contagem de traces por motivo de retenção ao longo do tempo, para enxergar o momento em que uma classe deixa de aparecer.
  6. Alerta de silêncio: se a contagem de retidos por erro cair a zero enquanto o contador de erros continua positivo, a política ou a instrumentação quebrou e ninguém vai notar sem esse alerta.
// tracing/sampling-policy.test.js
// O que estes testes protegem: a garantia de que o raro sempre passa
// e de que o comum nunca estoura o teto.

import { decide } from './sampling-policy.js';

const trace = (over = {}) => ({
  route: 'POST /checkout',
  status: 200,
  durationMs: 120,
  attributes: {},
  ...over,
});

test('erro de servidor e sempre retido, mesmo com a cota esgotada', () => {
  // Esgota a cota com trafego saudavel antes de testar o erro.
  for (let i = 0; i < 50; i += 1) decide(trace(), 1000);

  const result = decide(trace({ status: 503 }), 1000);
  expect(result.decision).toBe('keep');
  expect(result.reason).toBe('server_error');
});

test('latencia acima do limiar vence a cota esgotada', () => {
  for (let i = 0; i < 50; i += 1) decide(trace(), 2000);

  const result = decide(trace({ durationMs: 9000 }), 2000);
  expect(result.decision).toBe('keep');
  expect(result.reason).toBe('slow');
});

test('trafego saudavel respeita o teto por segundo', () => {
  const kept = Array.from({ length: 200 }, () => decide(trace(), 3000)).filter(
    (r) => r.decision === 'keep',
  );

  // Teto configurado em 5 traces/s: o excedente sai com motivo explicito.
  expect(kept.length).toBeLessThanOrEqual(5);
});

O alerta de silêncio da lista acima é o item que mais paga por si. A falha típica de amostragem não é ruidosa: ninguém recebe erro, nenhum serviço cai, apenas os traces de uma classe param de chegar. Sem um alerta que compare a contagem de retidos por motivo com o contador correspondente de métrica, essa quebra fica invisível por semanas e só aparece na madrugada em que você precisava dela.

FAQ

Perguntas frequentes

Amostragem de cauda sempre vale mais a pena que a de cabeça?

Não, e a diferença de custo operacional é maior do que a discussão costuma admitir. A amostragem de cauda exige que todos os spans sejam transportados até o coletor, mesmo os que serão descartados, então você troca custo de armazenamento por custo de rede e memória. Além disso, o coletor precisa agrupar spans por identificador de trace numa janela de tempo, o que significa manter estado, e com mais de uma instância significa garantir que todos os spans de um mesmo trace cheguem na mesma instância, geralmente por um balanceamento consistente pelo identificador. Se a sua janela expira antes de o trace fechar, requisições longas são decididas com informação incompleta. Em muitos sistemas o melhor primeiro passo é bem mais barato: manter amostragem de cabeça no tráfego comum e adicionar regras que forçam a retenção para classes conhecidamente raras, como rotas críticas, contas grandes e requisições com cabeçalho de depuração. Isso captura boa parte do valor sem operar um coletor com estado, e deixa a amostragem de cauda para quando as regras explícitas não derem mais conta.

Como escolher o limiar de latência sem chutar um número redondo?

Derivando do comportamento observado, por rota, e nunca fixando um valor global. O procedimento que funciona é medir a distribuição de latência de cada rota durante uma janela representativa, que inclua pico e vale, e posicionar o limiar num percentil alto o suficiente para ser raro e baixo o suficiente para capturar a degradação antes de ela virar timeout, tipicamente entre o p95 e o p99. Dois cuidados evitam que a política se degrade sozinha. O primeiro é recalcular periodicamente, porque um limiar fixo perde o sentido conforme o sistema fica mais rápido ou mais lento: no primeiro caso ele nunca dispara e a cauda desaparece da amostra, no segundo ele dispara em metade do tráfego e o custo explode. O segundo é prender o valor recalculado entre um piso e um teto absolutos, senão um período degradado ensina o limiar a considerar normal uma latência que não é, e a política deixa de enxergar exatamente o problema que você quer investigar.

Dá para calcular taxa de erro e percentis a partir dos traces amostrados?

Não com uma política que retém classes de forma desigual, que é justamente a política que vale a pena ter. Se você guarda cem por cento dos erros e uma fração do sucesso, contar linhas na amostra produz uma taxa de erro várias ordens de grandeza acima da real, e o mesmo viés infla qualquer percentil de latência, porque a cauda está inteira na amostra e o corpo da distribuição não. O caminho correto é separar as fontes: métricas saem de contadores e histogramas emitidos em cem por cento das requisições e agregados na origem, que são baratos justamente por não guardarem o evento individual, e traces respondem a pergunta seguinte, mostrando exemplos concretos que expliquem o que a métrica acusou. Se ainda assim for necessário estimar algo a partir da amostra, cada trace precisa carregar a taxa efetiva com que foi retido e toda contagem tem que somar o inverso dessa taxa em vez de somar registros, o que também exige que o motivo e a taxa sejam gravados junto do trace desde o início.

Amostrar é decidir hoje o que você vai poder investigar amanhã

A política de amostragem não é uma configuração de custo, é uma decisão sobre quais perguntas o sistema ainda vai conseguir responder daqui a três meses, tomada meses antes de a pergunta existir. Uma taxa uniforme guarda com fidelidade o tráfego que já funcionava e joga fora a minoria que explica o incidente. Posso revisar ou desenhar a política de amostragem da sua stack de observabilidade, definindo as condições que forçam retenção, a cota que limita o custo do tráfego comum, a propagação que impede trace pela metade entre serviços, a separação entre métrica e trace que evita painel enviesado, e os testes e alertas que provam que o raro continua sendo guardado.