Winston remains the most flexible Node.js logger. Here is how to set it up properly for production.
Quick Start
bun add winston
import winston from "winston";
const logger = winston.createLogger({
level: "info",
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: "error.log", level: "error" }),
new winston.transports.File({ filename: "combined.log" }),
],
});
if (process.env.NODE_ENV !== "production") {
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
),
}));
}
logger.info("Application started", { port: 3000 });
logger.error("Database connection failed", { host: "db.example.com", code: "ECONNREFUSED" });
Custom Formats
const customFormat = winston.format.combine(
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
winston.format.errors({ stack: true }),
winston.format.printf(({ timestamp, level, message, stack, ...meta }) => {
let log = `${timestamp} [${level}]: ${message}`;
if (Object.keys(meta).length) log += ` ${JSON.stringify(meta)}`;
if (stack) log += `\n${stack}`;
return log;
})
);
Log Rotation
bun add winston-daily-rotate-file
import DailyRotateFile from "winston-daily-rotate-file";
const transport = new DailyRotateFile({
filename: "logs/app-%DATE%.log",
datePattern: "YYYY-MM-DD",
maxSize: "20m",
maxFiles: "14d",
zippedArchive: true,
});
const logger = winston.createLogger({
transports: [transport],
});
Multiple Transports
const logger = winston.createLogger({
transports: [
// Console for dev
new winston.transports.Console({ level: "debug" }),
// File for all logs
new winston.transports.File({ filename: "combined.log" }),
// Separate file for errors
new winston.transports.File({ filename: "error.log", level: "error" }),
// HTTP transport for log aggregation
new winston.transports.Http({
host: "logs.example.com",
port: 443,
ssl: true,
level: "warn",
}),
],
});
Child Loggers (Scoped Context)
const requestLogger = logger.child({
requestId: req.headers["x-request-id"],
userId: req.user?.id,
});
requestLogger.info("Processing order");
requestLogger.info("Payment captured", { amount: 99.99 });
// Both logs include requestId and userId automatically
Express Middleware
import expressWinston from "express-winston";
app.use(expressWinston.logger({
winstonInstance: logger,
meta: true,
msg: "HTTP {{req.method}} {{req.url}} {{res.statusCode}} {{res.responseTime}}ms",
expressFormat: false,
}));
// Error logging
app.use(expressWinston.errorLogger({
winstonInstance: logger,
}));
Production Best Practices
- JSON format in production, pretty format in dev
- Rotate logs daily with size limits
- Separate error logs for quick debugging
- Add request context (requestId, userId)
- Never log sensitive data (passwords, tokens, PII)
// Filter sensitive fields
const sensitiveFilter = winston.format((info) => {
if (info.password) info.password = "***";
if (info.token) info.token = "***";
if (info.creditCard) info.creditCard = "***";
return info;
})();
Need structured logging for data pipelines? Check out my web scraping actors on Apify Store — clean data with full audit trails. For custom solutions, email spinov001@gmail.com.
Top comments (0)