Quando sua API cai às 3h da manhã, você abre os logs e vê: "Error processing request". Sem request ID, sem timestamp padronizado, sem contexto do que estava acontecendo. Esse log não ajuda a resolver nada.

Logging estruturado resolve isso: cada log é um objeto JSON com campos padronizados que ferramentas de observabilidade conseguem indexar, filtrar e correlacionar.

O formato que funciona

Cada log deve ser uma linha JSON com campos consistentes:

{
  "level": "error",
  "timestamp": "2026-12-18T14:30:00.123Z",
  "message": "Failed to process payment",
  "service": "payment-api",
  "request_id": "req_abc123",
  "user_id": "usr_xyz789",
  "error": {
    "name": "PaymentGatewayError",
    "message": "Card declined",
    "stack": "PaymentGateway.ts:45:12"
  },
  "context": {
    "amount": 15000,
    "currency": "BRL",
    "payment_method": "credit_card"
  }
}

Comparar com o log não estruturado: "Erro ao processar pagamento". O formato estruturado permite buscar "todos os erros de pagamento do usuário xyz789", ou "erros de cartão recusado nos últimos 5 minutos", ou "qual a taxa de falha do gateway X".

Os campos que importam

  • level: error, warn, info, debug. Padronize. Nunca use console.log em produção.
  • timestamp: ISO 8601, sempre em UTC. Evita problemas de timezone.
  • service: qual microserviço gerou o log. Obrigatório em arquitetura distribuída.
  • request_id: ID único da requisição. Propaga entre serviços. Permite correlacionar logs do mesmo fluxo.
  • user_id: quando aplicável, o ID do usuário afetado.
  • error: objeto com nome, mensagem, e stack trace.
  • context: dados específicos da operação que ajudam no debug.

Implementação com pino (Node.js)

Pino é o logger padrão para Node.js em produção. Rápido, estruturado, e com suporte nativo a JSON:

import pino from 'pino'

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  formatters: {
    level: (label) => ({ level: label }),
  },
  timestamp: pino.stdTimeFunctions.isoTime,
})

// Middleware de request
app.use((req, res, next) => {
  req.id = crypto.randomUUID()
  req.log = logger.child({
    request_id: req.id,
    method: req.method,
    url: req.url,
  })

  const start = Date.now()
  res.on('finish', () => {
    req.log.info({
      status_code: res.statusCode,
      duration_ms: Date.now() - start,
    }, 'Request completed')
  })

  next()
})

Request ID: o fio condutor

O request_id é o campo mais importante para debug em sistemas distribuídos. Ele precisa nascer na primeira camada e propagar por todas as chamadas:

// Gerar no gateway/API
const requestId = req.headers['x-request-id'] || crypto.randomUUID()

// Propagar em chamadas internas
await fetch('http://user-service/api/users', {
  headers: { 'X-Request-Id': requestId }
})

Com ferramentas como Datadog, Elasticsearch, ou Loki, você pode buscar todos os logs de um request_id específico e ver o fluxo completo de uma requisição, de ponta a ponta.

O que logar e o que não logar

Logar: erros, warnings, eventos de negócio importantes (pagamento aprovado, conta criada), métricas de performance (duração de queries, tempo de resposta).

Não logar: dados sensíveis (senhas, tokens, dados de cartão), dados PII sem necessidade, fluxo normal de operações (excessivo), variáveis de ambiente.

// ❌ Nunca
logger.info({ password: user.password, token: jwt })

// ✅ Correto
logger.info({ user_id: user.id, action: 'login', ip: req.ip })

Log levels na prática

Error: algo falhou e precisa de atenção. Alerta imediato.

Warn: algo inesperado mas não crítico. Merece investigação.

Info: eventos normais do sistema. Pagamento processado, email enviado.

Debug: detalhes para desenvolvimento. Desabilitado em produção.

Em produção, rode com level: 'info'. Ative debug temporariamente via variável de ambiente quando precisar investigar algo específico.

Correlação com métricas

Logs estruturados combinados com métricas (Prometheus, Datadog) permitem dashboards que mostram: taxa de erros por endpoint, latência percentil, throughput por serviço. O log dá o o quê aconteceu, a métrica dá o quanto.