Implementar logs com Winston no Node.js permite combinar formatos, níveis e múltiplos destinos em uma única configuração. A aplicação pode escrever JSON no console, separar erros em arquivo, encaminhar eventos a um serviço externo e aplicar metadados comuns como ambiente, versão e nome do serviço.
Essa flexibilidade é útil em sistemas com requisitos diferentes de transporte e retenção. Porém, cada destino adiciona custo e possibilidade de falha. Logging não deve bloquear o event loop, duplicar eventos, preencher o disco ou expor credenciais.
Neste guia, você aprenderá a criar loggers, configurar formatos, transports, níveis, exceções, request IDs, redaction, rotação, containers, testes e shutdown.
O que é Winston?
Winston é uma biblioteca de logging para Node.js baseada em formatos e transports. A documentação oficial do Winston apresenta API, níveis e destinos. O projeto logform contém os formatos utilizados pela biblioteca.
Para entender stdout e stderr, consulte Console no Node.js. Para uma alternativa focada em JSON de alto desempenho, veja Logs com Pino no Node.js.
Instalando
npm install winstonLogger básico
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.json(),
transports: [
new winston.transports.Console()
]
});
logger.info('Aplicação iniciada');Em produção, JSON facilita coleta e pesquisa.
Campos estruturados
logger.info('Pedido criado', {
orderId: 842,
amount: 159.9,
currency: 'BRL'
});Use propriedades separadas em vez de concatenar valores na mensagem.
Níveis padrão
Winston usa níveis npm por padrão:
- error;
- warn;
- info;
- http;
- verbose;
- debug;
- silly.
O logger registra o nível configurado e os mais importantes.
Níveis personalizados
const levels = {
fatal: 0,
error: 1,
warn: 2,
info: 3,
debug: 4,
trace: 5
};Se personalizar, alinhe cores, alertas e plataforma de coleta. Não crie níveis difíceis de interpretar.
Timestamp
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
)Use UTC e deixe a interface converter para o fuso do operador.
Metadados padrão
defaultMeta: {
service: 'billing-api',
version: process.env.APP_VERSION,
environment: process.env.NODE_ENV
}Campos estáveis permitem filtrar eventos de múltiplos serviços.
Erros com stack
const logger = winston.createLogger({
format: winston.format.combine(
winston.format.errors({ stack: true }),
winston.format.timestamp(),
winston.format.json()
),
transports: [new winston.transports.Console()]
});Ao capturar:
try {
await chargeCustomer();
} catch (error) {
logger.error('Falha na cobrança', {
error,
customerId
});
throw error;
}Teste o formato real, porque serializar Error sem o formato adequado pode perder propriedades.
Formatos em desenvolvimento
const developmentFormat = winston.format.combine(
winston.format.colorize(),
winston.format.timestamp(),
winston.format.printf(info => {
return `${info.timestamp} ${info.level}: ${info.message}`;
})
);Não use colorize no JSON de produção.
Console transport
new winston.transports.Console({
stderrLevels: ['error']
})Em containers, console costuma ser o único transport necessário. A plataforma coleta stdout e stderr.
File transport
new winston.transports.File({
filename: '/var/log/my-api/error.log',
level: 'error'
})Arquivos exigem permissões, espaço, rotação e backup apropriados.
Separando logs
transports: [
new winston.transports.Console(),
new winston.transports.File({
filename: 'error.log',
level: 'error'
}),
new winston.transports.File({
filename: 'combined.log'
})
]O evento de erro pode aparecer em todos os destinos. Isso é esperado, mas pode gerar cobrança duplicada em plataformas externas.
Rotação de arquivos
O core do Winston não resolve toda política de rotação. Pacotes como winston-daily-rotate-file podem criar arquivos por data e tamanho.
Em containers, prefira stdout para não manter estado local.
Transport remoto
Enviar diretamente para rede aumenta acoplamento. Se o destino estiver lento, logs podem acumular ou falhar. Agentes externos geralmente são mais resilientes.
Falha do transport
logger.on('error', error => {
process.stderr.write(
`Logger failure: ${error.message}\n`
);
});Evite tentar registrar a falha do próprio logger com o mesmo logger, criando recursão.
Formato condicional
const format = process.env.NODE_ENV === 'production'
? winston.format.json()
: developmentFormat;Redaction
Winston não deve receber dados sensíveis. Crie um formato que remova campos:
const redact = winston.format(info => {
if (info.password) info.password = '[REDACTED]';
if (info.token) info.token = '[REDACTED]';
if (info.headers?.authorization) {
info.headers.authorization = '[REDACTED]';
}
return info;
});Combine antes do JSON:
format: winston.format.combine(
redact(),
winston.format.timestamp(),
winston.format.json()
)Redaction não substitui seleção
É mais seguro construir um objeto pequeno do que registrar request, response ou configuração completos.
Request ID
logger.info('Requisição concluída', {
requestId,
method: req.method,
route: req.route?.path,
statusCode: res.statusCode,
durationMs
});Não use URL completa com query sensível.
Child logger
const requestLogger = logger.child({
requestId,
component: 'http'
});O child inclui metadados em eventos subsequentes.
AsyncLocalStorage
Armazene um child logger por requisição:
const { AsyncLocalStorage } = require('node:async_hooks');
const storage = new AsyncLocalStorage();
function currentLogger() {
return storage.getStore()?.logger || logger;
}Consulte AsyncLocalStorage no Node.js.
Express middleware
app.use((req, res, next) => {
const startedAt = performance.now();
const requestId = crypto.randomUUID();
res.setHeader('x-request-id', requestId);
storage.run({
logger: logger.child({ requestId })
}, () => {
res.on('finish', () => {
currentLogger().info('HTTP concluído', {
method: req.method,
path: req.route?.path,
statusCode: res.statusCode,
durationMs: performance.now() - startedAt
});
});
next();
});
});Health checks
Ignore ou amostre rotas muito frequentes para reduzir ruído. Consulte Health Checks no Node.js.
Exceptions handler
exceptionHandlers: [
new winston.transports.File({
filename: 'exceptions.log'
})
]Registrar não torna seguro continuar após exceção não capturada. Inicie shutdown.
Rejection handler
rejectionHandlers: [
new winston.transports.File({
filename: 'rejections.log'
})
]A política deve ser explícita. Promises rejeitadas sem captura podem indicar estado inconsistente.
Exit on error
A opção exitOnError controla comportamento após exceções tratadas pelo logger. Entenda o fluxo e não dependa de defaults para disponibilidade.
Graceful shutdown
Winston é uma stream de objetos. Para fechar:
await new Promise(resolve => {
logger.on('finish', resolve);
logger.end();
});Nem todo transport possui a mesma semântica. Teste a configuração real.
Consulte Graceful Shutdown no Node.js.
Containers e systemd
Escreva JSON no console. Kubernetes, Docker ou journald adicionam metadados e fazem coleta.
Veja systemd com Node.js.
Correlação com traces
logger.info('Operação concluída', {
traceId,
spanId,
orderId
});Consulte OpenTelemetry no Node.js.
Auditoria
Logs operacionais e trilha de auditoria podem usar destinos e retenção diferentes. Auditoria deve registrar ator, ação, alvo, horário e resultado, com proteção contra alteração.
Cardinalidade e custo
Campos como requestId são úteis em logs, mas não devem necessariamente virar índices. Configure a plataforma para indexar apenas campos usados em busca.
Volume
Reduza:
- logs de sucesso repetitivos;
- objetos grandes;
- health checks;
- debug em produção;
- stack de erros conhecidos;
- duplicidade entre transports.
Amostragem
Mantenha todos os erros e amostre eventos de alta frequência. Não amostre eventos de segurança ou auditoria sem regra formal.
Performance
Cada formato e transport executa trabalho. Pretty printing, serialização profunda e escrita em múltiplos destinos aumentam latência. Faça benchmark.
Backpressure
Transport lento pode acumular mensagens. Monitore memória, erros de escrita e tempo de flush.
Testes
Use um transport em memória ou stream customizada. Verifique:
- JSON válido;
- nível;
- timestamp;
- defaultMeta;
- requestId;
- stack de erro;
- redaction;
- duplicidade;
- falha de transport;
- shutdown.
Teste de segredo
test('não registra token', () => {
logger.info('Login', {
token: 'secret-value'
});
assert.equal(output.includes('secret-value'), false);
});Erros comuns
- Muitos transports: custo e duplicidade aumentam.
- Arquivo em container: logs desaparecem ou enchem a camada.
- Error sem stack: diagnóstico fica incompleto.
- Corpo completo: dados sensíveis vazam.
- Pretty em produção: parsing fica mais caro.
- Sem flush: últimos eventos são perdidos.
- Logger para erro do logger: ocorre recursão.
Boas práticas
- Use JSON em produção.
- Prefira console em containers.
- Adicione defaultMeta.
- Configure stack de erros.
- Use child loggers.
- Implemente redaction.
- Controle transports.
- Monitore falhas e backpressure.
- Feche no shutdown.
- Teste ausência de segredos.
Conclusão
Usar logs com Winston no Node.js oferece flexibilidade para combinar formatos, níveis e destinos. A biblioteca é especialmente útil quando a aplicação precisa de políticas distintas para console, arquivos ou integrações.
Essa flexibilidade precisa ser controlada. JSON, campos estáveis, redaction, request IDs e poucos transports evitam custo e vazamentos. Com stdout em containers e testes de shutdown, Winston fornece logs úteis sem comprometer a estabilidade da aplicação.


