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ção | Operação | Horário UTC | Status | Retentativa |
|---|---|---|---|---|
| r1 | A | 10:00:00 | 503 | Não |
| r2 | B | 10:00:01 | 200 | Não |
| r3 | A | 10:00:02 | 200 | Sim |
| r4 | C | 10:00:03 | 503 | Não |
| r5 | C | 10:00:04 | 503 | Sim |
| r6 | D | 10:00:05 | 200 | Não |
| r7 | E | 10:00:06 | 500 | Não |
| r8 | E | 10:00:07 | 200 | Sim |
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
| Pergunta | Cálculo | Resultado |
|---|---|---|
| Requisições com 5xx | 4 de 8 requisições | 50% |
| Operações com pelo menos um 5xx | 3 de 5 operações | 60% |
| Operações cujo último registro tem 5xx | 1 de 5 operações | 20% |
| Retentativas com 5xx | 1 de 3 retentativas | aproximadamente 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));
| Medida | Antes | Com a nova tentativa |
|---|---|---|
| Requisições | 8 | 9 |
| Respostas 5xx | 4 | 4 |
| Taxa de 5xx por requisição | 50% | aproximadamente 44,4% |
| Operações que tiveram algum 5xx | 3 | 3 |
| Operações cujo último registro tem 5xx | 1 | 0 |
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.
Fonte: https://tokenzero.pro