pino: logging estruturado em Node.js

    pino é o logger de mais alta performance para Node.js — até 10x mais rápido que winston, com output em JSON por padrão. Logs estruturados em JSON são indexáveis, filtráveis e fáceis de enviar para sistemas como Loki, Elasticsearch ou Datadog. Este guia cobre desde a configuração básica até contexto por request e integração com Grafana Loki.

    Configurar pino no Express

    Setup inicial com níveis e serializers customizados:

    typescript
    // npm install pino pino-http pino-pretty
    
    // lib/logger.ts:
    import pino from 'pino'
    
    export const logger = pino({
      level: process.env.LOG_LEVEL ?? 'info',
    
      // Em desenvolvimento: output legível (pino-pretty)
      // Em produção: JSON puro (mais performático)
      ...(process.env.NODE_ENV !== 'production' && {
        transport: {
          target: 'pino-pretty',
          options: {
            colorize: true,
            translateTime: 'SYS:standard',
            ignore: 'pid,hostname',
          },
        },
      }),
    
      // Serializers: controlar como objetos são logados
      serializers: {
        err: pino.stdSerializers.err,  // format padrão para erros
        req: (req) => ({
          method: req.method,
          url: req.url,
          // NUNCA logar: headers de auth, body com dados pessoais
        }),
        res: (res) => ({ statusCode: res.statusCode }),
      },
    
      // Campos base em todos os logs:
      base: {
        app: 'minha-api',
        env: process.env.NODE_ENV,
        version: process.env.npm_package_version,
      },
    })
    
    // Níveis disponíveis (crescente):
    // trace → debug → info → warn → error → fatal
    // Em produção: 'info' (debug/trace são muito verbosos)

    Logging por request com contexto

    Adicionar request_id a todos os logs de uma requisição:

    typescript
    // npm install pino-http
    
    import pinoHttp from 'pino-http'
    import { logger } from './lib/logger'
    import { randomUUID } from 'crypto'
    
    // Middleware pino-http — adiciona logger por request com request_id:
    export const httpLogger = pinoHttp({
      logger,
      genReqId: () => randomUUID(),  // ID único por requisição
    
      // Customizar campos logados:
      customLogLevel: (req, res, err) => {
        if (err || res.statusCode >= 500) return 'error'
        if (res.statusCode >= 400) return 'warn'
        return 'info'
      },
    
      // Redactar informações sensíveis antes de logar:
      redact: {
        paths: [
          'req.headers.authorization',
          'req.headers.cookie',
          'req.body.senha',
          'req.body.password',
          'req.body.cartao',
        ],
        censor: '[REDACTED]',
      },
    
      // Não logar health checks (muito verbosos):
      autoLogging: {
        ignore: (req) => req.url === '/health' || req.url === '/metrics',
      },
    })
    
    app.use(httpLogger)
    
    // Nos handlers: usar req.log (logger com contexto da requisição)
    app.post('/api/pedidos', autenticar, async (req, res) => {
      req.log.info({ userId: req.usuario.userId }, 'Criando pedido')
    
      try {
        const pedido = await criarPedido(req.body)
        req.log.info({ pedidoId: pedido.id, valor: pedido.total }, 'Pedido criado')
        res.status(201).json(pedido)
      } catch (err) {
        req.log.error({ err }, 'Erro ao criar pedido')
        throw err
      }
    })

    Context propagation com AsyncLocalStorage

    Propagar o request_id por toda a cadeia de chamadas (sem passar por parâmetro):

    typescript
    // lib/async-context.ts — propagar contexto sem drill-down:
    import { AsyncLocalStorage } from 'async_hooks'
    import pino from 'pino'
    import { logger } from './logger'
    
    interface RequestContext {
      requestId: string
      userId?: string
    }
    
    const storage = new AsyncLocalStorage<RequestContext>()
    
    // Getter do logger com contexto do request atual:
    export function getLogger(): pino.Logger {
      const ctx = storage.getStore()
      if (!ctx) return logger
    
      return logger.child({
        requestId: ctx.requestId,
        userId: ctx.userId,
      })
    }
    
    // Middleware para iniciar contexto por request:
    export function contextMiddleware(req: Request, res: Response, next: NextFunction) {
      const ctx: RequestContext = {
        requestId: req.headers['x-request-id'] as string ?? randomUUID(),
        userId: undefined,
      }
    
      storage.run(ctx, () => {
        // Adicionar userId após autenticação:
        const originalJson = res.json.bind(res)
        res.json = (...args) => {
          const store = storage.getStore()
          if (store && req.usuario) store.userId = req.usuario.userId
          return originalJson(...args)
        }
        next()
      })
    }
    
    // Usar nos services (sem precisar receber o logger por parâmetro):
    class PedidosService {
      async criar(dados: CriarPedidoDto) {
        const log = getLogger()  // automaticamente tem requestId e userId
        log.info({ dados }, 'Iniciando criação de pedido')
        // ...
      }
    }

    Enviar logs para Grafana Loki com Promtail

    Coletar logs JSON do Node.js e indexar no Loki:

    yaml
    # Produção: gravar logs em arquivo e coletar com Promtail
    
    # app: logar para stdout (Docker coleta automaticamente)
    # Para arquivo: pino com pino-roll (rotação):
    # npm install pino-roll
    # node server.js | pino-roll /var/log/minha-api/app.log --frequency daily --size 100m
    
    # promtail.yml — coletar logs de containers Docker:
    server:
      http_listen_port: 9080
    
    positions:
      filename: /tmp/positions.yaml
    
    clients:
      - url: http://loki:3100/loki/api/v1/push
    
    scrape_configs:
      # Coletar logs de containers Docker:
      - job_name: docker
        docker_sd_configs:
          - host: unix:///var/run/docker.sock
            refresh_interval: 5s
            filters:
              - name: status
                values: [running]
        relabel_configs:
          - source_labels: [__meta_docker_container_name]
            target_label: container
          - source_labels: [__meta_docker_compose_service]
            target_label: service
        pipeline_stages:
          # Parsear JSON (saída do pino):
          - json:
              expressions:
                level: level
                msg: msg
                time: time
                requestId: requestId
                userId: userId
          - labels:
              level:
              service:
          - timestamp:
              source: time
              format: Unix
    
    # Queries LogQL no Grafana para buscar logs:
    # Todos os erros da API:
    # {service="minha-api"} | json | level="error"
    # Logs de um request específico:
    # {service="minha-api"} | json | requestId="abc-123"

    Boas práticas de logging em produção

    O que logar, como logar e o que nunca logar:

    typescript
    // ✅ O QUE LOGAR:
    
    // Eventos de negócio importantes:
    logger.info({ userId, pedidoId, valor }, 'Pedido criado com sucesso')
    logger.info({ userId, plano }, 'Assinatura ativada')
    logger.warn({ userId, tentativas }, 'Muitas tentativas de login falhadas')
    
    // Erros e exceções (sempre com o erro completo):
    logger.error({ err, userId, endpoint: req.url }, 'Erro inesperado no handler')
    
    // Eventos de infraestrutura:
    logger.info({ dbPool: pool.totalCount }, 'Conexão com PostgreSQL estabelecida')
    logger.warn({ fila: 'emails', tamanho: queue.size }, 'Fila de emails crescendo')
    
    // ❌ O QUE NUNCA LOGAR:
    // Senhas, tokens, hashes de senha:
    // logger.info({ senha: req.body.senha })  ← NUNCA
    
    // Tokens JWT completos (podem ser usados para autenticação):
    // logger.info({ token: req.headers.authorization })  ← NUNCA
    
    // Dados pessoais sensíveis (CPF, cartão, endereço completo):
    // logger.info({ cpf: usuario.cpf })  ← configurar redact ou mascarar
    
    // Payloads enormes (podem causar out-of-memory):
    // logger.debug({ body: req.body })  ← usar apenas campos específicos
    
    // ✅ MASCARAR DADOS SENSÍVEIS:
    function mascararCpf(cpf: string): string {
      return cpf.replace(/(d{3})d{3}d{3}(d{2})/, '$1.***.***-$2')
    }
    
    logger.info({
      cpf: mascararCpf(usuario.cpf),  // "123.***.***-09"
      email: usuario.email.replace(/(?<=.{3}).(?=[^@]*@)/, '*'),
    }, 'Usuário autenticado')

    $ runstack deploy --plan starter

    Não quer configurar manualmente?

    Não quer configurar manualmente? Implante o VPS para Node.js em menos de 3 minutos com a Runstack. Infraestrutura da OPEN DATACENTER, com servidores no Brasil.

    Perguntas frequentes