Abandone o console.log: Observabilidade com Winston e Logs Estruturados
Winston configurado com transports por nível, logs estruturados em JSON, correlação de requests via AsyncLocalStorage, sanitização de dados sensíveis e integração com Loki/Elasticsearch para busca em produção.
console.log('aqui 1'), console.log('usuario:', user), console.log('erro???'). Esses logs manuais são inúteis em produção: sem timestamp, sem nível de severidade, sem contexto da requisição, impossíveis de filtrar em um servidor com múltiplas instâncias. Quando um bug acontece às 3h da manhã, você precisa de logs que contam a história da requisição com falha — não de uma sequência de strings aleatórias.
Observabilidade é a capacidade de entender o estado interno de um sistema a partir de suas saídas externas. Logs estruturados em JSON são a base — cada evento tem campos indexáveis como userId, requestId, method, statusCode, durationMs. Com isso, você pode buscar no Elasticsearch ou Grafana Loki: 'todos os erros 500 do usuário X nas últimas 2 horas'.
Configuração do Winston com JSON Estruturado
npm install winstonimport { createLogger, format, transports, type Logger } from 'winston';
const isProd = process.env.NODE_ENV === 'production';
const logLevel = process.env.LOG_LEVEL ?? (isProd ? 'info' : 'debug');
// Formato JSON para produção — indexável pelo Elasticsearch/Loki
const jsonFormat = format.combine(
format.timestamp({ format: 'YYYY-MM-DDTHH:mm:ss.sssZ' }),
format.errors({ stack: true }), // Inclui stack trace em erros
format.json()
);
// Formato human-readable para desenvolvimento
const devFormat = format.combine(
format.colorize(),
format.timestamp({ format: 'HH:mm:ss' }),
format.printf(({ level, message, timestamp, ...meta }) => {
const metaStr = Object.keys(meta).length
? '\n' + JSON.stringify(meta, null, 2)
: '';
return `[${timestamp as string}] ${level}: ${message as string}${metaStr}`;
})
);
export const logger: Logger = createLogger({
level: logLevel,
// Os níveis e suas severidades (menor = mais grave):
// error: 0, warn: 1, info: 2, http: 3, verbose: 4, debug: 5, silly: 6
format: isProd ? jsonFormat : devFormat,
defaultMeta: {
service: process.env.SERVICE_NAME ?? 'api',
version: process.env.npm_package_version,
env: process.env.NODE_ENV,
},
transports: [
new transports.Console(),
...(isProd ? [
// Em produção: arquivo separado para erros (facilita alertas)
new transports.File({
filename: '/var/log/app/error.log',
level: 'error',
maxsize: 10 * 1024 * 1024, // Rotaciona ao atingir 10MB
maxFiles: 5, // Mantém os últimos 5 arquivos
}),
new transports.File({
filename: '/var/log/app/combined.log',
maxsize: 50 * 1024 * 1024,
maxFiles: 10,
}),
] : []),
],
// Captura exceções não tratadas e rejeições de Promise
exceptionHandlers: [
new transports.File({ filename: '/var/log/app/exceptions.log' }),
],
rejectionHandlers: [
new transports.File({ filename: '/var/log/app/rejections.log' }),
],
});
export { logger as log };Request ID: Correlacionando Logs com AsyncLocalStorage
O AsyncLocalStorage do Node.js resolve o problema de propagar o requestId sem passar por todos os parâmetros das funções. Funciona como um "contexto da requisição": o middleware define o requestId no início da requisição, e qualquer código assync dentro daquela requisição (mesmo em serviços aninhados) consegue ler o requestId sem receber como parâmetro. Isso é fundamental para correlacionar todos os logs de uma requisição com falha: filtrar por requestId no Elasticsearch mostra exatamente o que aconteceu desde o controller até o banco.
import { AsyncLocalStorage } from 'async_hooks';
import { randomBytes } from 'crypto';
interface RequestContext {
requestId: string;
userId?: string;
method?: string;
path?: string;
}
// Store que sobrevive por toda a cadeia assíncrona da requisição
export const requestContextStore = new AsyncLocalStorage<RequestContext>();
export function getRequestContext(): RequestContext | undefined {
return requestContextStore.getStore();
}
export function generateRequestId(): string {
return randomBytes(8).toString('hex'); // 16 chars hex
}import type { Request, Response, NextFunction } from 'express';
import { requestContextStore, generateRequestId } from '../infra/requestContext';
import { logger } from '../infra/logger';
export function requestLoggerMiddleware(
req: Request,
res: Response,
next: NextFunction
): void {
const requestId = (req.headers['x-request-id'] as string) ?? generateRequestId();
const startTime = Date.now();
// Popula o contexto da requisição
const context = {
requestId,
method: req.method,
path: req.path,
userId: req.user?.id, // Populado pelo ensureAuthenticated
};
// Adiciona o requestId no header de resposta (útil para debug pelo cliente)
res.setHeader('X-Request-Id', requestId);
// Executa o resto da cadeia DENTRO do store da requisição
requestContextStore.run(context, () => {
logger.info('Request started', {
requestId,
method: req.method,
path: req.path,
ip: req.ip,
userAgent: req.headers['user-agent'],
});
res.on('finish', () => {
const durationMs = Date.now() - startTime;
const level = res.statusCode >= 500 ? 'error' : res.statusCode >= 400 ? 'warn' : 'info';
logger[level]('Request completed', {
requestId,
method: req.method,
path: req.path,
statusCode: res.statusCode,
durationMs,
userId: req.user?.id,
});
});
next();
});
}import { getRequestContext } from './requestContext';
// Wrapper que injeta o requestId automaticamente em todos os logs
export const log = {
info: (message: string, meta?: Record<string, unknown>) =>
logger.info(message, { ...getRequestContext(), ...meta }),
warn: (message: string, meta?: Record<string, unknown>) =>
logger.warn(message, { ...getRequestContext(), ...meta }),
error: (message: string, err?: unknown, meta?: Record<string, unknown>) => {
const errorMeta = err instanceof Error
? { errorMessage: err.message, stack: err.stack, errorName: err.name }
: { error: err };
logger.error(message, { ...getRequestContext(), ...errorMeta, ...meta });
},
debug: (message: string, meta?: Record<string, unknown>) =>
logger.debug(message, { ...getRequestContext(), ...meta }),
};
// Uso nos Use Cases — requestId incluído automaticamente:
// log.info('Usuário criado', { userId: user.id });
// log.error('Falha ao enviar e-mail', err, { userId: user.id });Sanitização: Nunca Logue Dados Sensíveis
const SENSITIVE_KEYS = new Set([
'password', 'passwordHash', 'token', 'accessToken', 'refreshToken',
'secret', 'apiKey', 'cvv', 'cardNumber', 'authorization',
]);
export function sanitize<T>(data: T): T {
if (!data || typeof data !== 'object') return data;
if (Array.isArray(data)) {
return data.map(sanitize) as T;
}
return Object.fromEntries(
Object.entries(data as Record<string, unknown>).map(([key, value]) => [
key,
SENSITIVE_KEYS.has(key.toLowerCase())
? '[REDACTED]'
: sanitize(value),
])
) as T;
}
// Uso ao logar req.body ou dados do usuário:
// log.debug('Corpo da requisição', sanitize(req.body));
// Output: { email: 'user@example.com', password: '[REDACTED]' }Nunca logue senhas, tokens, dados de cartão ou PII em texto puro. Em produção, logs são gravados em arquivos, enviados para Elasticsearch ou Datadog — ambientes que podem ter acesso mais amplo que o banco de dados. Aplique sanitize() em qualquer dado que vem do usuário antes de logar.
Enviando Logs para Grafana Loki
Para múltiplas instâncias e busca centralizada, use Grafana Loki (alternativa open-source ao Elasticsearch, integrada ao Grafana):
npm install winston-lokiimport LokiTransport from 'winston-loki';
// Adicione aos transports do createLogger em produção:
new LokiTransport({
host: process.env.LOKI_URL ?? 'http://localhost:3100',
labels: {
service: process.env.SERVICE_NAME ?? 'api',
env: process.env.NODE_ENV ?? 'production',
},
json: true,
format: format.json(),
replaceTimestamp: true,
onConnectionError: (err) => console.error('[Loki] Erro de conexão:', err),
})
// No Grafana, você pode buscar:
// {service="api", env="production"} |= "requestId" | json | statusCode >= 500
// {service="api"} | json | userId = "uuid-123" | level = "error"Conclusão
Logs estruturados em JSON com requestId de correlação transformam o debugging em produção de uma busca cega em uma pesquisa indexada. Com Winston + AsyncLocalStorage, cada Use Case loga automaticamente o contexto da requisição sem passagem manual de parâmetros. Sanitize dados sensíveis, leve os logs para Loki/Elasticsearch, e você passa de 'não sei o que aconteceu' para 'encontrei a causa raiz em 2 minutos' na próxima vez que um bug bater em produção.