Logs em Node.js para produção

Logs são o principal instrumento de diagnóstico em produção. Em Node.js, console.log está longe do ideal para produção, use Winston para logs estruturados em JSON com níveis de severidade, contexto por requisição e integração nativa com Docker. Com logs bem estruturados, você encontra erros em segundos em vez de minutos.

Configurar Winston para produção

Winston é o logger mais popular para Node.js. Configure para emitir JSON em produção e formato legível em desenvolvimento:

typescript
// src/logger.ts
import winston from 'winston'

const isProduction = process.env.NODE_ENV === 'production'

export const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || (isProduction ? 'info' : 'debug'),
  format: isProduction
    ? winston.format.combine(
        winston.format.timestamp(),
        winston.format.errors({ stack: true }),
        winston.format.json()              // JSON em produção
      )
    : winston.format.combine(
        winston.format.colorize(),
        winston.format.simple()            // legível em desenvolvimento
      ),
  transports: [
    new winston.transports.Console(),      // sempre stdout/stderr
  ],
  // Capturar exceções e rejections não tratadas
  exceptionHandlers: [new winston.transports.Console()],
  rejectionHandlers: [new winston.transports.Console()],
})

// Uso:
logger.info('Servidor iniciado', { port: 3000 })
logger.error('Falha ao conectar', { error: err.message, stack: err.stack })

Logs por requisição HTTP com Express

Adicione contexto de request_id, método, rota e duração em cada log de requisição:

typescript
// src/middleware/request-logger.ts
import { Request, Response, NextFunction } from 'express'
import { randomUUID } from 'crypto'
import { logger } from '../logger'

export function requestLogger(req: Request, res: Response, next: NextFunction) {
  const requestId = randomUUID()
  const start = Date.now()

  // Adicionar request_id ao request para uso em logs subsequentes
  req.requestId = requestId

  res.on('finish', () => {
    const duration = Date.now() - start

    logger.info('HTTP request', {
      request_id: requestId,
      method: req.method,
      path: req.path,
      status: res.statusCode,
      duration_ms: duration,
      user_agent: req.headers['user-agent'],
      ip: req.ip,
    })
  })

  next()
}

// app.ts
app.use(requestLogger)

// Output em produção (JSON):
// {"level":"info","message":"HTTP request","method":"GET","path":"/users","status":200,"duration_ms":23}

Capturar erros não tratados

Exceções e rejections não capturadas são os erros mais difíceis de debugar em produção, certifique-se de logá-los antes do processo encerrar:

typescript
// src/index.ts
import { logger } from './logger'

// Exceções síncronas não capturadas
process.on('uncaughtException', (error: Error) => {
  logger.error('Uncaught exception — processo será encerrado', {
    error: error.message,
    stack: error.stack,
  })
  // Dar tempo para o logger fazer flush antes de encerrar
  setTimeout(() => process.exit(1), 1000)
})

// Promises rejeitadas sem .catch()
process.on('unhandledRejection', (reason: unknown) => {
  logger.error('Unhandled promise rejection', {
    reason: reason instanceof Error ? reason.message : String(reason),
    stack: reason instanceof Error ? reason.stack : undefined,
  })
})

// Sinal de encerramento gracioso (SIGTERM do Docker stop)
process.on('SIGTERM', () => {
  logger.info('SIGTERM recebido — encerrando servidor...')
  server.close(() => {
    logger.info('Servidor encerrado com sucesso')
    process.exit(0)
  })
})

Filtrar e buscar logs no Docker

Comandos para trabalhar com logs de containers em produção:

bash
# Ver últimas 100 linhas
docker compose logs app --tail=100

# Seguir em tempo real
docker compose logs app -f

# Filtrar apenas erros (logs em JSON)
docker compose logs app 2>&1 | grep '"level":"error"'

# Buscar por request_id específico
docker compose logs app 2>&1 | grep '"request_id":"abc-123"'

# Filtrar por período (requer Docker 20.10+)
docker logs --since="2026-06-07T10:00:00" --until="2026-06-07T11:00:00" nome_container

# Parsear logs JSON com jq (apt install jq)
docker compose logs app 2>&1 | grep '^{' | jq 'select(.level == "error") | {time: .timestamp, msg: .message, path: .path}'

Evitar os erros mais comuns de logging

Práticas que causam problemas em produção e como evitá-las:

typescript
// ❌ NÃO faça em produção:

// 1. Logar objetos sensíveis
logger.info('Usuário autenticado', { user, password }) // expõe senha

// 2. console.log em vez de logger
console.log('debug:', req.body) // sem nível, sem JSON, sem timestamp

// 3. Strings de erro sem stack trace
logger.error('Erro: ' + err.message) // perde o stack trace

// 4. Logar a cada tick/iteração
setInterval(() => logger.debug('tick'), 100) // ~35k logs/hora

// ✅ FAÇA:

// 1. Sanitizar antes de logar
const { password, token, ...safeUser } = user
logger.info('Usuário autenticado', { user: safeUser })

// 2. Sempre usar o logger configurado
logger.debug('Request body', { body: req.body })

// 3. Passar o objeto Error completo
logger.error('Falha na operação', { error: err })
// Winston extrai message e stack automaticamente com format.errors()

// 4. Logar apenas eventos significativos
// A cada N iterações, ou apenas quando o estado muda

Não quer configurar manualmente?

Implante o VPS em menos de 5 minutos com a Runstack. Infraestrutura da OPEN Datacenter, com servidores em São Paulo.

Perguntas frequentes

Pino: mais performático (10x mais rápido que Winston em benchmarks), JSON nativo, ideal para APIs de alto tráfego. Winston: mais flexível e popular, melhor ecossistema de transports. Morgan: middleware HTTP para Express, registra apenas requisições (não substitui um logger geral). Recomendação: Pino para novas APIs de alto throughput; Winston para projetos existentes ou que precisam de múltiplos transports.

Stack ELK (Elasticsearch + Logstash + Kibana) é a solução completa mas pesada (~4 GB RAM). Alternativa leve: Grafana Loki + Promtail + Grafana (~500 MB). Para VPS pequena: encaminhe logs para um serviço gerenciado (Logtail, Papertrail, Datadog) via driver de log do Docker: logging.driver: loki ou options.loki-url.

Configure level: "info" em produção (não "debug"). Evite logar em loops ou para cada item de um array grande. Use sampling para logs de alta frequência (logar 1 em cada 100 requisições de um endpoint muito chamado). Configure logrotate via Docker (max-size e max-file no daemon.json ou por container no docker-compose).

Use AsyncLocalStorage (Node.js 16+) para propagar o contexto sem passar manualmente: const asyncStorage = new AsyncLocalStorage(); // no middleware: asyncStorage.run({requestId}, next); // no logger: const ctx = asyncStorage.getStore(); log({requestId: ctx?.requestId, ...}). Assim qualquer log feito em qualquer função durante aquela requisição terá o request_id automaticamente.

Sim. Configure o logger para emitir JSON em stdout. PM2 coleta stdout e grava em arquivos de log (configurados em out_file e error_file no ecosystem.config.js). Para filtrar: cat ~/.pm2/logs/app-out.log | jq 'select(.level=="error")'. A desvantagem é que PM2 adiciona um prefixo de timestamp ao log, use merge_logs: true e log_type: "json" para logs limpos.