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:
// 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:
// 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):
// 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:
# 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:
// ✅ 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
Conteúdos relacionados