Implementar logs com Pino no Node.js permite registrar eventos estruturados em JSON com baixo overhead. Em produção, logs precisam responder o que aconteceu, em qual serviço, durante qual requisição e com quais consequências, sem bloquear o event loop nem expor credenciais.
Pino separa geração de logs da formatação visual. A aplicação escreve JSON rapidamente para stdout, enquanto uma ferramenta externa coleta, transporta e apresenta os eventos. Essa abordagem combina bem com containers, systemd, Kubernetes e plataformas centralizadas.
Neste guia, você aprenderá níveis, child loggers, serialização de erros, redaction, request IDs, transporte, pretty printing, integração HTTP, AsyncLocalStorage, testes e estratégias para controlar custo e volume.
O que é Pino?
Pino é uma biblioteca de logging estruturado para Node.js. A documentação oficial do Pino apresenta configuração e transports. O repositório oficial do Pino contém referências de API e exemplos.
Para saída padrão, consulte Console no Node.js. Para contexto por requisição, veja AsyncLocalStorage no Node.js.
Instalando
npm install pinoPara desenvolvimento legível:
npm install --save-dev pino-prettyLogger básico
const pino = require('pino');
const logger = pino({
level: process.env.LOG_LEVEL || 'info'
});
logger.info('Aplicação iniciada');A saída contém timestamp, nível, PID, hostname e mensagem, conforme a configuração e versão.
Campos estruturados
logger.info({
orderId: 842,
status: 'created',
durationMs: 37
}, 'Pedido criado');Campos estruturados permitem filtros e métricas. Evite concatenar tudo na mensagem.
Níveis
trace: detalhes muito granulares;debug: diagnóstico de desenvolvimento;info: eventos normais importantes;warn: situação inesperada recuperável;error: operação falhou;fatal: processo não pode continuar.
Produção costuma usar info ou warn. Alterar para debug aumenta volume e custo.
Não use error para tudo
Uma resposta 404 esperada não é necessariamente erro operacional. Níveis incorretos geram alertas inúteis e escondem incidentes reais.
Erros JavaScript
try {
await processPayment();
} catch (error) {
logger.error({ err: error }, 'Falha no pagamento');
throw error;
}Usar a chave err ativa a serialização padrão de Error, incluindo mensagem, tipo e stack.
Erro como mensagem
logger.error(error);Embora possa funcionar, o formato explícito com { err: error } permite adicionar contexto sem perder a stack.
Serializers
const logger = pino({
serializers: {
user(value) {
return {
id: value.id,
role: value.role
};
}
}
});Serializers controlam campos, mas não devem executar consultas ou trabalho pesado.
Redaction
const logger = pino({
redact: {
paths: [
'password',
'token',
'req.headers.authorization',
'config.databaseUrl'
],
censor: '[REDACTED]'
}
});Redaction precisa cobrir caminhos reais. Um objeto aninhado em formato diferente pode escapar.
Não registre corpos completos
Corpos podem conter senha, cartão, token, documento e dados pessoais. Prefira campos selecionados e IDs internos.
Child logger
const requestLogger = logger.child({
requestId,
route: '/api/orders',
method: 'POST'
});
requestLogger.info('Requisição iniciada');O child adiciona contexto a todas as mensagens sem repetir argumentos.
Logger por módulo
const paymentLogger = logger.child({
component: 'payment-service'
});Use nomes estáveis para facilitar filtros.
Request ID
Aceite um ID externo apenas se formato e tamanho forem válidos; caso contrário, gere outro:
const requestId = isValidRequestId(header)
? header
: crypto.randomUUID();Envie o valor na resposta e para serviços downstream.
AsyncLocalStorage
const { AsyncLocalStorage } = require('node:async_hooks');
const storage = new AsyncLocalStorage();
function getLogger() {
const context = storage.getStore();
return context?.logger || logger;
}Na entrada HTTP:
storage.run({
logger: logger.child({ requestId })
}, () => next());Funções internas usam getLogger() sem encaminhar requestId manualmente.
Integração com Fastify
Fastify possui integração com Pino:
const fastify = require('fastify')({
logger: {
level: process.env.LOG_LEVEL || 'info',
redact: ['req.headers.authorization']
}
});Consulte Fastify com Node.js.
Integração com Express
O pacote pino-http cria logs de requisição:
const pinoHttp = require('pino-http');
app.use(pinoHttp({
logger,
genReqId(req, res) {
const id = crypto.randomUUID();
res.setHeader('x-request-id', id);
return id;
}
}));Rotas de saúde
Health checks frequentes podem gerar ruído. Configure autoLogging para ignorar:
autoLogging: {
ignore: req => req.url.startsWith('/health')
}Consulte Health Checks no Node.js.
Timestamp
Pino usa timestamp numérico por padrão. Para ISO:
const logger = pino({
timestamp: pino.stdTimeFunctions.isoTime
});JSON numérico é mais compacto; a plataforma pode converter na visualização.
Base fields
const logger = pino({
base: {
service: 'orders-api',
version: process.env.APP_VERSION,
environment: process.env.NODE_ENV
}
});Não inclua hostname como label de alta cardinalidade se a plataforma já adiciona metadados do container.
Pretty printing
node app.js | pino-prettyPretty printing é para desenvolvimento. Em produção, mantenha JSON e formate na ferramenta de observabilidade.
Transport
const transport = pino.transport({
target: 'pino-pretty',
options: {
colorize: true
}
});
const logger = pino(transport);Transports podem executar em worker thread. Valide shutdown para não perder mensagens pendentes.
stdout versus arquivo
Em containers e systemd, escreva para stdout. A plataforma cuida de rotação e envio.
Veja systemd com Node.js e Docker Multi-stage para Node.js.
Destino de arquivo
const destination = pino.destination({
dest: '/var/log/my-api/app.log',
sync: false
});Se usar arquivos, configure permissões, rotação e espaço em disco. Não grave na camada efêmera do container sem plano.
Logging síncrono
Saída síncrona aumenta segurança em eventos finais, mas pode bloquear o processo. Use com critério.
Fatal e shutdown
process.on('uncaughtException', error => {
logger.fatal({ err: error }, 'Exceção não capturada');
process.exitCode = 1;
});Depois de uncaughtException, inicie shutdown e não continue atendendo normalmente.
Unhandled rejection
process.on('unhandledRejection', reason => {
logger.fatal({ err: reason }, 'Promise rejeitada');
process.exitCode = 1;
});Flush
Antes de saída forçada, aguarde o destino quando a API usada permitir. Não chame process.exit() imediatamente após logger.fatal.
Graceful shutdown
Pare requisições, conclua operações e finalize transport. Consulte Graceful Shutdown no Node.js.
Logs e OpenTelemetry
Inclua traceId e spanId quando houver contexto:
logger.info({
traceId,
spanId,
orderId
}, 'Pedido processado');Veja OpenTelemetry no Node.js.
Cardinalidade
Logs aceitam IDs, mas índices de plataforma podem ficar caros. Configure quais campos são indexados e retenção por nível.
Amostragem
Em endpoints muito movimentados, registre todos os erros e uma amostra de sucessos. Não use amostragem que esconda auditoria obrigatória.
Log de auditoria
Auditoria possui requisitos diferentes de log operacional. Registre ator, ação, recurso e resultado em armazenamento protegido, sem incluir segredos.
Performance
Evite construir objetos caros quando o nível está desabilitado:
if (logger.isLevelEnabled('debug')) {
logger.debug({ detail: buildExpensiveDetail() });
}Serialização circular
Não envie objetos enormes ou com referências circulares sem serializer. Selecione propriedades relevantes.
Testes
Crie um destino em memória e analise cada linha JSON. Verifique:
- nível;
- mensagem;
- requestId;
- erro e stack;
- redaction;
- campos base;
- ausência de senha;
- health check ignorado;
- shutdown;
- volume de logs.
Teste de redaction
test('remove authorization', () => {
logger.info({
req: {
headers: {
authorization: 'Bearer secret'
}
}
});
assert.equal(output.includes('secret'), false);
});Erros comuns
- Pretty em produção: CPU e parsing aumentam.
- Corpo completo: dados pessoais vazam.
- Error sem err: stack pode ser perdida.
- Nível debug permanente: custo explode.
- Sem requestId: eventos não são correlacionados.
- Arquivo sem rotação: disco enche.
- process.exit imediato: logs finais são perdidos.
Boas práticas
- Use JSON em produção.
- Defina níveis corretos.
- Adicione contexto com child logger.
- Use request ID.
- Configure redaction.
- Registre erros com
err. - Envie para stdout.
- Filtre health checks.
- Controle volume e retenção.
- Teste ausência de segredos.
Conclusão
Usar logs com Pino no Node.js oferece saída estruturada e eficiente, adequada a aplicações de alto volume. Child loggers, request IDs e serializers transformam eventos isolados em uma narrativa pesquisável.
A qualidade depende da disciplina: níveis corretos, redaction, campos estáveis e nenhum dado sensível. Com JSON em stdout, coleta externa e correlação com traces, Pino fornece observabilidade sem transformar logging em gargalo do event loop.




