1

Pitch: TokenZero — como retentativas mudam a leitura dos logs

Quatro respostas HTTP 5xx podem pertencer a apenas três operações. Algumas dessas operações podem ter recebido um 200 depois. Qual número deveria aparecer no relatório?

A resposta depende da pergunta. Preparei um exemplo pequeno em JavaScript para separar frequência de erros, operações afetadas e último resultado observado.

Todos os registros abaixo são fictícios, criados exclusivamente para este artigo. O código é um exemplo didático independente da implementação do produto.

Três perguntas, três contagens

Antes de calcular, precisamos definir a unidade de cada registro. Aqui, uma linha representa o resultado de uma requisição HTTP. Cada requisição tem um ID próprio, e uma retentativa mantém o identificador da operação.

  • Quantas requisições receberam 5xx? Conta todas as tentativas presentes no conjunto, inclusive as repetidas.
  • Quantas operações tiveram algum 5xx? Conta cada operação uma única vez, mesmo que tenha falhado várias vezes no conjunto.
  • Quantas operações têm 5xx no último registro observado? Considera o horário mais recente de cada operação dentro do conjunto.

São medidas diferentes. Neste exemplo, o critério de erro é uma resposta entre 500 e 599. Os demais status não são automaticamente uma confirmação de sucesso de negócio.

Todas as métricas consideram exclusivamente os registros presentes no conjunto. O campo de retentativa não permite reconstruir tentativas ausentes nem determinar seus resultados. Por isso, zero respostas 5xx observadas não garante que a operação nunca tenha apresentado erro.

Uma amostra que cabe na tela

As cinco operações são A, B, C, D e E. A, C e E receberam erros; A e E tiveram retentativas com resposta 200. C repetiu o erro.

RequisiçãoOperaçãoHorário UTCStatusRetentativa
r1A10:00:00503Não
r2B10:00:01200Não
r3A10:00:02200Sim
r4C10:00:03503Não
r5C10:00:04503Sim
r6D10:00:05200Não
r7E10:00:06500Não
r8E10:00:07200Sim

Salve o bloco abaixo em exemplo.cjs e execute node exemplo.cjs. Ele usa apenas recursos do JavaScript, sem pacotes ou serviços externos.

const dados = [
  // ID da requisição, operação, horário UTC, status, retentativa
  ["r1", "A", "2026-01-01T10:00:00Z", 503, false],
  ["r2", "B", "2026-01-01T10:00:01Z", 200, false],
  ["r3", "A", "2026-01-01T10:00:02Z", 200, true],
  ["r4", "C", "2026-01-01T10:00:03Z", 503, false],
  ["r5", "C", "2026-01-01T10:00:04Z", 503, true],
  ["r6", "D", "2026-01-01T10:00:05Z", 200, false],
  ["r7", "E", "2026-01-01T10:00:06Z", 500, false],
  ["r8", "E", "2026-01-01T10:00:07Z", 200, true]
].map(([id, operacao, horario, status, retry]) => ({
  id, operacao, horario, status, retry
}));

function resumir(linhas) {
  if (!Array.isArray(linhas)) throw new Error("Esperada uma lista");
  const ids = new Set();
  const instantes = new Set();
  const afetadas = new Set();
  const ultimas = new Map();
  const texto = v => typeof v === "string" && v.trim().length > 0;
  let falhas = 0, retentativas = 0, retentativasComFalha = 0;

  for (const r of linhas) {
    const tempo = r && typeof r.horario === "string"
      ? Date.parse(r.horario) : NaN;
    if (!r || !texto(r.id) || !texto(r.operacao) ||
        !Number.isFinite(tempo) || !Number.isInteger(r.status) ||
        r.status < 100 || r.status > 599 || typeof r.retry !== "boolean") {
      throw new Error("Registro inválido");
    }
    if (ids.has(r.id)) throw new Error("Requisição repetida: " + r.id);
    ids.add(r.id);

    const instante = JSON.stringify([r.operacao, tempo]);
    if (instantes.has(instante)) {
      throw new Error("Ordem ambígua para a operação: " + r.operacao);
    }
    instantes.add(instante);

    const falhou = r.status >= 500;
    if (falhou) {
      falhas++;
      afetadas.add(r.operacao);
    }
    if (r.retry) {
      retentativas++;
      if (falhou) retentativasComFalha++;
    }
    const anterior = ultimas.get(r.operacao);
    if (!anterior || tempo > anterior.tempo) {
      ultimas.set(r.operacao, { tempo, status: r.status });
    }
  }

  return {
    requisicoes: linhas.length,
    respostas5xx: falhas,
    taxa5xxPorRequisicao: linhas.length ? 100 * falhas / linhas.length : null,
    operacoesObservadas: ultimas.size,
    operacoesComAlgum5xx: afetadas.size,
    operacoesComUltimo5xx: [...ultimas.values()]
      .filter(r => r.status >= 500).length,
    retentativas,
    retentativasCom5xx: retentativasComFalha
  };
}

console.log(resumir(dados));

O Set de operações afetadas evita contar C duas vezes. O Map dos últimos registros permite responder outra pergunta: qual resposta aparece por último para cada operação?

A comparação usa o horário, portanto a ordem das linhas no array não define qual registro é o último. O exemplo rejeita IDs de requisição repetidos e horários empatados para a mesma operação. Na presença dessas ambiguidades, ele interrompe o cálculo.

Os resultados e seus denominadores

PerguntaCálculoResultado
Requisições com 5xx4 de 8 requisições50%
Operações com pelo menos um 5xx3 de 5 operações60%
Operações cujo último registro tem 5xx1 de 5 operações20%
Retentativas com 5xx1 de 3 retentativasaproximadamente 33,3%

Os quatro erros são r1, r4, r5 e r7. As três operações afetadas são A, C e E. Somente C termina este recorte com um 5xx no registro mais recente.

50%, 60% e 20% estão corretos, mas respondem a perguntas diferentes. Usar apenas “taxa de falha” como rótulo deixa o denominador e a interpretação escondidos.

Também seria incorreto apagar as falhas anteriores de A e E porque houve um 200 depois. As respostas posteriores mudam o último resultado observado; o histórico de erros presente no conjunto permanece.

E se a tentativa anterior estiver ausente?

Atualização: acrescentei este esclarecimento após a observação do Florian nos comentários.

Imagine que o conjunto recebido contenha somente r3: operação A, status 200 e retry: true.

Nesse caso, o resumo retornará:

  • Uma requisição.
  • Uma retentativa.
  • Zero respostas 5xx observadas.
  • Uma operação observada.
  • Zero operações com 5xx observado.
  • Zero operações com 5xx no último registro disponível.

Esses resultados descrevem corretamente a única linha recebida. Porém, não permitem determinar o resultado da tentativa anterior.

Na amostra completa deste artigo, sabemos que r1 recebeu 503 porque esse registro está disponível. Se r1 não vier no conjunto, o código não reconstrói essa informação a partir de retry: true.

O booleano também não informa quantas tentativas anteriores existiram. Ele apenas classifica aquela linha conforme o dado recebido.

Portanto, o resumo não verifica se o histórico está completo. Para avaliar o histórico inteiro de uma operação, seriam necessários registros adicionais ou outra fonte confiável que estabelecesse essa completude.

O que muda quando uma nova tentativa recebe 200?

Voltando à amostra com as oito requisições, acrescente este bloco ao mesmo arquivo. Ele simula uma nova requisição de C após as oito anteriores:

const comNovaTentativa = [
  ...dados,
  {
    id: "r9", operacao: "C", horario: "2026-01-01T10:00:08Z",
    status: 200, retry: true
  }
];
console.log(resumir(comNovaTentativa));
MedidaAntesCom a nova tentativa
Requisições89
Respostas 5xx44
Taxa de 5xx por requisição50%aproximadamente 44,4%
Operações que tiveram algum 5xx33
Operações cujo último registro tem 5xx10

A taxa por requisição caiu porque entrou uma resposta 200 no denominador. Os quatro erros anteriores continuam nos dados. Essa queda, isoladamente, não demonstra que a causa dos erros foi corrigida.

Limites para levar essa ideia a logs reais

  • Uma linha precisa representar uma requisição concluída. Se o log registra início, etapas e fim, contar linhas diretamente mistura eventos com requisições.
  • A correlação precisa ser confiável. O exemplo exige um ID textual de operação em todas as linhas. Em dados incompletos, é preciso contabilizar o que ficou sem identificação; não inventar uma operação comum para todos os valores ausentes.
  • Os horários precisam ser comparáveis. Aqui são datas UTC válidas e distintas por operação. Em sistemas distribuídos, relógios divergentes ou empates podem exigir uma sequência ou outro critério de ordenação. O código não corrige esses problemas.
  • O recorte pode estar incompleto. Podem existir tentativas anteriores ausentes ou tentativas posteriores fora da janela. “Último registro observado” não garante que toda a operação de negócio terminou corretamente.
  • Status HTTP não demonstra causa raiz. Um 503 seguido de 200 registra uma sequência de respostas. Explicar por que ocorreu exige outras evidências.

Executei o exemplo em Node.js e conferi as contagens, a nova tentativa, o recorte contendo somente r3, a entrada em ordem invertida, a lista vazia e a rejeição de registros inválidos ou ambíguos. Na lista vazia, a taxa por requisição é null, pois não existe denominador.

Para um relatório útil, eu deixaria explícitos a unidade contada, a janela observada, o critério de erro e as limitações de completude dos dados. Assim, quem lê consegue distinguir o volume de requisições com erro do impacto observado por operação.

Sobre o projeto

Sou o criador do TokenZero, um projeto comercial em validação voltado à preparação de contexto para IA. Compartilho este exemplo genérico como contribuição técnica; ele não descreve a implementação interna do produto.

Carregando publicação patrocinada...
1

Outro caso que bagunça essa contagem é retentativa que nem passa pelo cliente. No nginx o proxy_next_upstream já vem ligado pra error e timeout (em método idempotente), então num timeout o primeiro upstream pode ter recebido e logado a requisição, o proxy manda pro segundo, e a aplicação mostra duas linhas sem nenhuma marcada como retentativa. Se o ID da operação vier do cliente num header e o proxy repassar, pelo menos a contagem por operação continua certa.

1

Boa observação! Esse caso mostra outra limitação da marcação: a aplicação pode receber uma tentativa reenviada pelo proxy sem saber que houve uma anterior. Nesse cenário, retry: false não comprova que aquela foi a primeira tentativa.

No exemplo, o contador soma apenas as linhas marcadas com retry: true. Para investigar os reenvios do proxy, seria necessário correlacionar também os registros dessa camada.

Um ID estável por operação, preservado entre as tentativas e diferente do identificador de cada tentativa, ajuda a agrupar os registros disponíveis. Ainda assim, não recupera tentativas que ficaram fora dos logs.

Obrigado por trazer esse cenário — ele complementa bem a discussão sobre o que conseguimos concluir a partir dos registros.

1

se for só pra contar, dá pra tirar isso do log do próprio nginx. quando o proxy tenta mais de um upstream, a variável $upstream_addr sai com os endereços separados por vírgula, e o $upstream_status vem com um status pra cada tentativa. colocando as duas no log_format já aparece qual requisição foi reenviada, sem mexer na aplicação.

1

Boa! Para contar as tentativas feitas pelo próprio Nginx, esse caminho é mais direto. Minha resposta anterior ficou ampla demais: com $upstream_addr e $upstream_status registrados no access log, dá para observar essas tentativas sem alterar a aplicação.

A correlação por ID fica útil quando precisamos relacionar essas informações com os registros da aplicação ou com outras chamadas do cliente. Valeu por indicar as variáveis — deixou o exemplo bem mais concreto!

1

faz sentido, pra juntar com o log da aplicação o ID continua sendo o caminho. dá até pra colocar o mesmo header no log_format do nginx ($http_x_request_id, se o header tiver esse nome), aí a correlação sai das duas pontas com a mesma chave.

1

A coluna Retentativa é um booleano da linha. Se o conjunto só traz a segunda tentativa, ela entra em retentativas e ainda vira o último status da operação. A tentativa que faltou não aparece em nenhuma das três contagens.

1

Boa observação, Florian. O campo retry só classifica aquela requisição como retentativa; ele não reconstrói as tentativas anteriores.

Se o conjunto trouxer apenas uma retentativa com HTTP 200, o exemplo contará uma requisição, uma retentativa e nenhum 5xx observado. Isso não permite concluir que a operação nunca teve erro. “Último registro” também significa apenas o mais recente disponível naquele conjunto.

Vale explicitar essa limitação: as métricas descrevem os registros recebidos, e o histórico anterior pode estar incompleto. Também não dá para assumir qual foi o resultado da tentativa ausente só porque existe uma retentativa. Obrigado por apontar isso.