winston
winston is the general-purpose logger for Node.js. You build a logger with winston.createLogger(), hand it a format and a list of transports (Console, File, Http, or one of the many community transports), and every logger.info() call fans out to all of them. Each transport can carry its own level and its own format, so error lines land in a file while everything goes to stdout. It has been the default answer for Node logging for well over a decade, and the 3.x line has been current since 2018.
Pick winston when you want log routing, formatting, and rotation configured in one place inside your app, and you can spend the CPU that costs. Pick pino when logs are JSON going to a collector and latency is the thing you are protecting.
Use it if
- You want per-destination routing: errors to one file, everything to stdout, warnings to an HTTP endpoint, each with a separate format and level
- You need log file rotation handled inside the process via winston-daily-rotate-file instead of logrotate or a sidecar container
- You want uncaught exceptions and unhandled promise rejections captured into a dedicated destination with exceptionHandlers and rejectionHandlers
- You are inheriting a Node codebase that already logs through winston, possibly with custom transports written against winston-transport
- Throughput matters on a hot request path: winston formats and writes inside your process, and pino's published benchmarks put it several times faster because it pushes that work to worker threads
- You want secret redaction included: winston has no equivalent of pino's redact option, so you write a filtering format yourself and hope nobody logs a token down a path you did not cover
- You need logging in a browser bundle: winston is Node-shaped in practice (File, Http, object-mode streams) and installs about 37 KB gzipped across 11 dependencies
- You subclass the Logger class: the README itself warns that .child() is likely to be bugged in that case because of how the `this` keyword resolves
- You want one obvious path: formats live in a separate package (logform), internal state rides on Symbol keys from triple-beam, and winston 2.x tutorials that no longer work still rank well in search
Setup reality
npm install winston gives you a working logger in five lines, but the default logger exported by require('winston') has no transports at all, and the README warns that leaving it that way can push memory usage up. Real setups mean composing formats with format.combine in the correct order (colorize has to come before whatever renders the text you want colored), installing winston-daily-rotate-file separately if you want rotation, and knowing that printf-style %s interpolation is silently inert unless you enable format.splat(). TypeScript declarations ship in the package. Flushing before process exit is manual: call logger.end() and wait for the finish event.
Patterns
Create a logger with file and console transportscreate-logger
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
defaultMeta: { service: 'user-service' },
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
],
});
logger.info('server started', { port: 3000 });Build your own logger rather than using require('winston') directly: the default logger has zero transports, and the README warns that leaving it empty can raise memory usage.
Add a readable console transport outside productionconsole-in-dev
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple(),
),
}));
}colorize() has to come before the formatter that renders the text, otherwise there is nothing colored yet when it runs.
Write your own line formatcustom-printf-format
const { createLogger, format, transports } = require('winston');
const { combine, timestamp, label, printf } = format;
const line = printf(({ level, message, label, timestamp }) =>
`${timestamp} [${label}] ${level}: ${message}`);
const logger = createLogger({
format: combine(label({ label: 'api' }), timestamp(), line),
transports: [new transports.Console()],
});printf must be last in the combine chain; anything after it operates on an info object whose rendered message is already fixed.
Attach request context to every linechild-logger
app.use((req, res, next) => {
req.log = logger.child({ requestId: req.id });
next();
});
// later
req.log.warn('slow query', { ms: 812 });Child metadata merges into every call. The README flags .child() as unreliable if you have subclassed Logger yourself, so avoid combining the two.
Use printf-style placeholdersstring-interpolation
const logger = winston.createLogger({
format: winston.format.combine(
winston.format.splat(),
winston.format.simple(),
),
transports: [new winston.transports.Console()],
});
logger.info('imported %d rows from %s', 4210, 'orders.csv');Without format.splat() the placeholders are printed literally; this catches people who copy console.log habits into winston.
Define your own levels and colorscustom-levels
const levels = { fatal: 0, error: 1, warn: 2, info: 3, trace: 4 };
const colors = { fatal: 'bold red', error: 'red', warn: 'yellow', info: 'green', trace: 'grey' };
winston.addColors(colors);
const logger = winston.createLogger({ levels, level: 'info' });
logger.fatal('out of disk');Lower number means higher severity (RFC5424 order), and levels can only be set when the logger is created, not swapped later.
Capture uncaught exceptions and rejectionshandle-exceptions
const logger = winston.createLogger({
transports: [new winston.transports.File({ filename: 'combined.log' })],
exceptionHandlers: [new winston.transports.File({ filename: 'exceptions.log' })],
rejectionHandlers: [new winston.transports.File({ filename: 'rejections.log' })],
exitOnError: false,
});winston exits the process after logging an uncaught exception unless exitOnError is false; exitOnError can also be a function so you keep running for specific error codes.
Rotate log files by datedaily-rotate-file
require('winston-daily-rotate-file');
const rotate = new winston.transports.DailyRotateFile({
filename: 'app-%DATE%.log',
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d',
});
logger.add(rotate);Separate npm install; requiring the package registers DailyRotateFile onto winston.transports as a side effect.
Drop log records that match a conditionfilter-logs
const dropHealthChecks = winston.format((info) => {
if (info.path === '/healthz') return false;
return info;
});
const logger = winston.createLogger({
format: winston.format.combine(dropHealthChecks(), winston.format.json()),
transports: [new winston.transports.Console()],
});Returning a falsy value discards the record and stops the rest of the combine chain, which is also how you build redaction since winston has no redact option.
Give each transport its own level and formatper-transport-config
const logger = winston.createLogger({
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error', format: winston.format.json() }),
new winston.transports.Console({ level: 'debug', format: winston.format.simple() }),
],
});
// change one at runtime
logger.transports[1].level = 'warn';A transport level is a maximum verbosity for that destination and is independent of the logger-wide level, so the stricter of the two wins.
Wait for logs to be written before exitingflush-before-exit
logger.on('finish', () => process.exit(0));
logger.error('fatal startup failure');
logger.end();Calling process.exit() straight after logger.error() can truncate File and Http transports; the finish event is the only signal that every transport drained.
Time a block of workprofile-timing
const profiler = logger.startTimer();
await rebuildSearchIndex();
profiler.done({ message: 'index rebuilt' });
// or the named form
logger.profile('import');
await importRows();
logger.profile('import');Profile output is logged at info by default; pass { level: 'debug' } in the metadata object to move it down.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| pino | npm | You log JSON to a collector and want the lowest possible overhead in your process. |
| consola | npm | You are writing a CLI and want pretty human-readable output rather than transport routing. |
| loglevel | npm | You want a tiny leveled logger that works the same in the browser and in Node with no transport layer at all. |