Logging estruturado: por que console.log não basta em produção
Entenda a diferença entre log de texto solto e log estruturado em JSON, por que isso importa na hora de buscar e filtrar em produção, e como usar o Pino pra registrar contexto junto de cada evento.
Já falei aqui sobre tratamento de erro e sobre debugging avançado com Chrome DevTools, mas os dois assumem que você ainda está com o código na mão, rodando localmente. Em produção, o cenário muda completamente: não tem breakpoint, não tem terminal aberto olhando em tempo real. O que sobra pra entender o que aconteceu é o log, e a forma como ele é escrito faz toda diferença entre achar a resposta em segundos ou passar horas procurando.
O problema do log de texto solto
console.log("Usuário fez login: " + email + " às " + new Date());Isso funciona bem enquanto você está olhando o terminal na hora que acontece. O problema aparece quando esse log já aconteceu há dias, misturado com milhares de outras linhas parecidas, e você precisa achar só os logins de um usuário específico, ou só os que aconteceram num horário específico. Com texto solto, a única ferramenta disponível é busca de texto simples, tipo grep, que funciona até certo ponto, mas não permite filtrar por campo específico, nem combinar condições (esse usuário, nesse intervalo de tempo, com esse nível de severidade) de um jeito confiável.
O que muda com log estruturado
Log estruturado é escrever cada evento como um objeto, geralmente serializado em JSON, em vez de uma frase corrida:
logger.info({
evento: "login",
usuarioId: "123",
email: "ana@exemplo.com",
ip: "200.10.5.3",
});Isso gera uma linha assim:
{"level":"info","evento":"login","usuarioId":"123","email":"ana@exemplo.com","ip":"200.10.5.3","time":1728000000000}Parece mais verboso de escrever, mas a diferença aparece na hora de consultar. Com cada campo separado, uma ferramenta de observabilidade (Datadog, Grafana Loki, CloudWatch Insights) consegue filtrar exatamente por evento: "login" e usuarioId: "123" ao mesmo tempo, sem depender de interpretar texto livre, nem correr o risco de um e-mail com formato estranho confundir a busca.
Usando o Pino em Node
O Pino é uma biblioteca de log bastante usada em Node justamente por gerar JSON estruturado por padrão, com overhead baixo mesmo em alto volume de requisição:
import pino from "pino";
const logger = pino();
logger.info({ usuarioId: "123" }, "usuário fez login");
logger.warn({ tentativas: 3 }, "muitas tentativas de login falhas");
logger.error({ erro: err.message, pedidoId: "456" }, "falha ao processar pagamento");O primeiro argumento é o objeto com contexto estruturado, o segundo é uma mensagem legível pra humano. Isso dá o melhor dos dois mundos: alguém lendo o log corrido ainda entende o que aconteceu pela mensagem, e a ferramenta de busca ainda consegue filtrar pelos campos do objeto.
Níveis de log, usados com intenção
Log estruturado também ganha mais valor quando o nível de severidade é usado de forma consistente, não só console.log pra tudo:
debug: detalhe útil só durante investigação ativa, normalmente desligado em produção.info: evento normal do sistema funcionando (login, pedido criado, e-mail enviado).warn: algo fora do esperado, mas que não impediu o sistema de continuar (tentativa de login falha, retry de uma chamada externa).error: falha real, que impediu uma operação de completar.
Com isso configurado corretamente, um ambiente de produção pode rodar só exibindo info pra cima, sem perder debug detalhado que só faria barulho sem necessidade, e uma ferramenta de alerta pode disparar notificação automaticamente sempre que um error aparecer, sem precisar vasculhar log manualmente pra descobrir que algo quebrou.
Contexto que se repete, não re-escrito em cada linha
Uma informação como requestId (um identificador único por requisição) ajuda bastante a juntar todas as linhas de log relacionadas a uma mesma chamada específica, mesmo que ela passe por várias funções diferentes. Em vez de passar isso manualmente em todo log, o Pino permite criar um logger filho, já carregando esse contexto:
app.use((req, res, next) => {
req.log = logger.child({ requestId: crypto.randomUUID() });
next();
});
app.post("/pedidos", (req, res) => {
req.log.info({ pedidoId: pedido.id }, "pedido criado");
// ...
});Toda chamada feita a partir de req.log já carrega o mesmo requestId, sem precisar repetir isso em cada linha manualmente, o que facilita reconstruir o caminho completo de uma requisição específica, do início ao fim, numa ferramenta de busca.
Fechando
console.log não é errado pra desenvolvimento local, onde você está olhando o terminal na hora. Em produção, onde o log é a única fonte de verdade depois que o problema já aconteceu, estruturar cada linha como um objeto com campo específico é o que transforma um amontoado de texto em algo que dá pra filtrar, cruzar e alertar de forma automática, em vez de depender de olhar linha por linha esperando achar o que importa.
Direto na sua
caixa de entrada.
Um aviso por e-mail sempre que eu publicar um post novo. Sem spam, sem newsletter chata, só isso.
Leia também
- Artboards no Claude Code: a skill /design e como evitar o AI Slop7 de outubro de 2026 · 5 min
- Busca full-text: como implementar pesquisa de verdade7 de outubro de 2026 · 4 min
- Teste unitário na prática com Vitest6 de outubro de 2026 · 4 min
- Gauntlet Loop: como usar múltiplos agentes para melhorar o design no Claude Code4 de outubro de 2026 · 8 min