Pular para o conteúdo
9 min de leitura

Logs Estruturados na Prática: De Texto Solto a Dados Consultáveis

Por Equipe Nebular ·

Log em texto livre não escala. Entenda por que emitir logs em JSON com campos padronizados, correlação por trace_id e níveis coerentes transforma depuração em consulta.

Neste artigo

Às três da manhã, com um alerta disparando e um cliente reclamando, a diferença entre resolver um incidente em cinco minutos ou em duas horas costuma estar na qualidade dos seus logs. E qualidade, aqui, não é quantidade — é estrutura. Um sistema que despeja milhões de linhas de texto solto é tão inútil quanto um que não loga nada: em ambos os casos, você não consegue responder à pergunta que importa naquele momento.

Log estruturado é a prática de emitir cada evento como um dado consultável — tipicamente um objeto JSON com campos nomeados — em vez de uma frase livre pensada para um humano ler linha a linha. É uma mudança pequena no código e enorme na operação. Vamos entender por quê, e como fazer certo.

O problema do texto livre#

Considere uma linha de log tradicional:

``text 2026-08-13 03:14:22 ERROR pagamento falhou para usuario 8842 no pedido 5591 apos 3 tentativas ``

Para um humano lendo um arquivo, tudo bem. Mas quando você tem mil réplicas gerando isso e precisa responder "quantos pagamentos falharam para o usuário 8842 na última hora?", começa o inferno das expressões regulares. Você vai escrever um grep frágil que quebra assim que alguém muda a ordem das palavras, adiciona um campo ou traduz a mensagem. A informação está lá, mas não está acessível.

Agora a mesma informação, estruturada:

``json { "timestamp": "2026-08-13T03:14:22.481Z", "level": "error", "event": "pagamento_falhou", "user_id": "8842", "pedido_id": "5591", "tentativas": 3, "gateway": "stripe", "trace_id": "a1b2c3d4e5f6" } ``

A pergunta vira uma consulta: filtre por event = "pagamento_falhou" e user_id = "8842" na janela de tempo. Sem regex, sem fragilidade. E você ganha de brinde a capacidade de agregar: contar falhas por gateway, por faixa de tentativas, por qualquer campo. O log deixou de ser prosa e virou uma tabela.

O que faz um bom log estruturado#

Estruturar não é só trocar texto por JSON. Há um conjunto de decisões que separam logs úteis de logs que só ocupam disco.

Campos padronizados em toda a stack#

Se um serviço chama o usuário de user_id e outro chama de userId, e um terceiro de uid, você não consegue correlacionar nada. Defina um esquema de campos comuns que todos os serviços respeitam: timestamp, level, service, event, trace_id, e os identificadores de domínio com nomes fixos. Trate esse esquema como um contrato de plataforma, não uma preferência de cada time.

Níveis com significado real#

Os níveis de log existem para você filtrar por severidade, mas só funcionam se forem usados com disciplina:

  • ERROR: algo falhou e exige atenção ou já está degradando o usuário. Todo

ERROR deveria ser, em tese, acionável.

  • WARN: algo inesperado que o sistema conseguiu contornar, mas que pode

virar problema (um retry que funcionou, um recurso próximo do limite).

  • INFO: eventos de negócio relevantes — um pedido criado, um deploy iniciado.

É o nível padrão de produção.

  • DEBUG: detalhe fino para investigação, geralmente desligado em produção por

volume.

O antipadrão clássico é logar tudo como INFO (perdendo a capacidade de filtrar) ou espalhar ERROR em coisas que não são erro (gerando fadiga de alerta, onde ninguém mais olha os ERROR porque a maioria é ruído).

Contexto, não só a mensagem#

Um log que diz "falhou" e nada mais te obriga a adivinhar. Cada evento deve carregar o contexto suficiente para você entender o que aconteceu sem precisar reproduzir: quais IDs estavam envolvidos, qual foi a duração da operação, qual recurso externo estava sendo chamado, qual o resultado.

``go // Log com contexto rico, usando um logger estruturado. logger.Error("chamada ao gateway falhou", "event", "gateway_timeout", "gateway", "stripe", "pedido_id", pedidoID, "duracao_ms", elapsed.Milliseconds(), "trace_id", traceID, ) ``

Correlação: o campo que muda tudo#

Em uma arquitetura distribuída, uma única requisição do usuário atravessa vários serviços. Sem um identificador que os costure, cada serviço loga sua parte de forma isolada e você nunca reconstrói a história completa.

A solução é o trace_id (ou correlation ID): um identificador único gerado na borda do sistema — no gateway, no primeiro serviço que recebe a requisição — e propagado por todas as chamadas subsequentes, geralmente via cabeçalho HTTP. Cada serviço inclui esse trace_id em todos os seus logs.

``text Gateway trace_id=abc123 event=requisicao_recebida Auth trace_id=abc123 event=token_validado Pedidos trace_id=abc123 event=pedido_criado Pagamento trace_id=abc123 event=pagamento_falhou ``

Com o trace_id, uma consulta filtrando por abc123 te devolve a jornada inteira daquela requisição, na ordem, atravessando todos os serviços. É a ponte entre logs e tracing distribuído: o mesmo identificador que aparece nos seus logs é o que amarra os spans no seu sistema de traces.

Vale reforçar o padrão de propagação: o serviço lê o trace_id do cabeçalho de entrada; se não houver (é a borda), gera um novo; e o repassa em todo cabeçalho de saída. Um único elo que esquece de propagar quebra a corrente inteira.

O que nunca deve entrar no log#

Logs são frequentemente o ponto de vazamento de dados sensíveis mais negligenciado. Eles são copiados para agregadores, indexados, retidos por meses e acessados por muita gente. Alguns absolutos:

  • Senhas, tokens, chaves de API: nunca, em nenhum nível. Nem mesmo em DEBUG.
  • Dados pessoais sensíveis sem necessidade: CPF, cartão de crédito, dados de

saúde. Se precisar correlacionar por usuário, use um ID interno, não o dado real.

  • Corpo completo de requisições que possam conter os itens acima. Logue

metadados (tamanho, tipo, status), não o payload cru.

  • Mensagens de erro internas ecoadas ao cliente: o detalhe do driver de banco

vai para o log interno, nunca para a resposta HTTP do usuário.

Uma boa prática é ter uma função de redação que mascara campos sensíveis por nome antes de qualquer coisa ser serializada, e negar por padrão: só loga o que foi explicitamente marcado como seguro.

Eventos, não frases: pense no consumidor#

Uma mudança de mentalidade ajuda a escrever bons logs estruturados: pare de pensar "que frase eu escreveria para descrever isso?" e comece a pensar "que evento acabou de acontecer e quais são seus atributos?". A frase é para humanos lerem uma por uma; o evento é para máquinas agregarem aos milhões.

Isso se reflete no campo event, que merece atenção especial. Ele deve conter um identificador estável e de baixa cardinalidade do tipo de acontecimento — pedido_criado, pagamento_falhou, cache_miss — e não a mensagem completa com os valores interpolados. Os valores vão em campos próprios. Assim, você consegue contar quantos pagamento_falhou houve por minuto sem que cada variação de mensagem conte como um evento diferente.

``json { "event": "pagamento_falhou", "gateway": "stripe", "valor_centavos": 4990 } ``

Com o event estável, montar um alerta ("me avise se pagamento_falhou passar de 10 por minuto") ou um painel ("taxa de falha por gateway ao longo do dia") vira trivial. Se a informação estivesse presa numa frase — "pagamento de R$ 49,90 no Stripe falhou" — cada valor diferente geraria uma string única e a agregação seria impossível. É a mesma disciplina que separa uma métrica de um texto: nomes estáveis para agrupar, valores em dimensões para filtrar.

Logs no mundo dos containers#

Em ambientes containerizados, há uma regra de ouro que vem direto da metodologia 12-Factor: a aplicação não gerencia arquivos de log. Ela escreve no stdout e no stderr, sem buffer, e pronto. Quem captura, agrega e roteia é a plataforma.

```python # A aplicação só emite no stdout; o runtime coleta. import sys, json, datetime def log(level, event, campos): registro = { "timestamp": datetime.datetime.utcnow().isoformat() + "Z", "level": level, "event": event, campos, } print(json.dumps(registro), file=sys.stdout, flush=True)

log("info", "pedido_criado", pedido_id="5591", user_id="8842") ```

Por quê? Porque o container é efêmero. Se a aplicação escreve em /var/log/app.log, esse arquivo some quando o container é descartado — e em Kubernetes containers são descartados o tempo todo. Escrevendo no stdout, um agente de coleta (rodando como DaemonSet, por exemplo) pega o fluxo, enriquece com metadados do pod (nome, namespace, labels) e envia ao agregador central, como Loki ou Elasticsearch. A aplicação fica desacoplada de para onde os logs vão.

Cuidado com o volume e o custo#

Log estruturado é poderoso, mas cada campo tem custo de armazenamento e indexação. Dois cuidados evitam que a conta exploda:

Cardinalidade. Campos com um número enorme de valores distintos (como um ID único por requisição) são ótimos para filtrar, mas caros para indexar como dimensão de agregação. Saiba a diferença entre um campo que você quer filtrar (alta cardinalidade, ok) e um que você quer agrupar em métricas (mantenha baixa cardinalidade).

Amostragem e níveis. Nem todo evento precisa de log em produção. Eventos de altíssima frequência e baixo valor podem ser amostrados (logar 1 a cada N) ou ficar em DEBUG desligado. Reserve o log garantido para o que é acionável ou de negócio.

Um erro comum é achar que "logar mais é sempre melhor". Não é. Volume excessivo aumenta custo, atrasa a ingestão e — pior — enterra o sinal no ruído. Bom logging é sobre logar o evento certo com o contexto certo, não sobre logar tudo.

Colocando em prática#

Se você está migrando de logs em texto para estruturados, não precisa reescrever o mundo de uma vez. Um caminho incremental que costuma funcionar:

  1. Adote uma biblioteca de log estruturado na linguagem do serviço, que emita

JSON por padrão e permita anexar campos de contexto.

  1. Defina o esquema mínimo de campos comuns (timestamp, level, service,

event, trace_id) e documente como contrato.

  1. Propague o trace_id da borda para dentro, em todo cabeçalho de chamada

entre serviços.

  1. Mande tudo para o stdout e deixe a plataforma coletar; nada de arquivos.
  2. Configure a redação de campos sensíveis antes da serialização.
  3. Meça o volume e ajuste níveis e amostragem para o custo caber.

O retorno aparece no primeiro incidente. Quando a pergunta "o que aconteceu com essa requisição?" vira uma consulta de dez segundos em vez de uma arqueologia de grep, você entende por que times maduros tratam log como dado de primeira classe — e não como um print esquecido no meio do código.

Leituras relacionadas

Nenhum comentário ainda

Seja o primeiro a comentar.

Deixe seu comentário

Entre com sua conta Canverly para comentar. Você pode usar a mesma conta em qualquer site da rede.

Entrar com Canverly