Winston Logger
Winston: versatile Node.js logger — transports (Console, File, HTTP), formats, custom levels, metadata, daily rotate file, and error handling
Winston is the most widely adopted logging library in the Node.js ecosystem, built around maximum flexibility. Its architecture separates **formats** (how a log looks) from **transports** (where a log goes), allowing you to compose pipelines that write human-readable output to the console while simultaneously shipping structured JSON to a file or remote service. Winston treats every log entry as an `info` object — a plain JavaScript object with at least `level` and `message` — and every format and transport operates on that object.
## Key Points
- **Transport multiplexing** — send different log levels to different destinations simultaneously.
- **Format composition** — chain small, single-purpose format functions (timestamp, colorize, printf) via `format.combine`.
- **Custom log levels** — define domain-specific severity levels beyond the syslog/npm defaults.
- **Metadata-first** — attach arbitrary structured data to every log entry as first-class fields.
1. **Always include `errors({ stack: true })` in your format chain** — without it, Error objects lose their stack traces during serialization.
2. **Place `errors()` before `json()`** in `format.combine` — order matters; the errors format must enrich the info object before JSON serialization.
3. **Use `defaultMeta`** to stamp every log with service name and environment — this is essential for filtering in multi-service architectures.
4. **Create child loggers per request** rather than passing metadata to every `.info()` call.
5. **Set `exitOnError: false`** to prevent the process from crashing after a logged uncaught exception; pair with process health monitoring.
6. **Use `winston-daily-rotate-file`** for production file logging — unbounded log files eventually fill the disk.
7. **Separate transports by level** — errors to a dedicated file makes incident investigation faster.
8. **Keep message strings static** — put variable data in the metadata object, not interpolated into the message.skilldb get logging-services-skills/Winston LoggerFull skill: 372 linesWinston Logger
Core Philosophy
Winston is the most widely adopted logging library in the Node.js ecosystem, built around maximum flexibility. Its architecture separates formats (how a log looks) from transports (where a log goes), allowing you to compose pipelines that write human-readable output to the console while simultaneously shipping structured JSON to a file or remote service. Winston treats every log entry as an info object — a plain JavaScript object with at least level and message — and every format and transport operates on that object.
Key tenets:
- Transport multiplexing — send different log levels to different destinations simultaneously.
- Format composition — chain small, single-purpose format functions (timestamp, colorize, printf) via
format.combine. - Custom log levels — define domain-specific severity levels beyond the syslog/npm defaults.
- Metadata-first — attach arbitrary structured data to every log entry as first-class fields.
Setup
Installation
// Core
// npm install winston
// Daily file rotation
// npm install winston-daily-rotate-file
// Types (winston ships its own, but rotate needs this)
// npm install -D @types/node
Basic Configuration
import winston, { format, transports, type Logger } from "winston";
const { combine, timestamp, errors, json, colorize, printf } = format;
// Human-readable format for development console
const devFormat = combine(
colorize({ all: true }),
timestamp({ format: "HH:mm:ss" }),
errors({ stack: true }),
printf(({ timestamp, level, message, stack, ...meta }) => {
const metaStr = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : "";
return `${timestamp} ${level}: ${stack ?? message}${metaStr}`;
}),
);
// Structured JSON for production
const prodFormat = combine(
timestamp({ format: "YYYY-MM-DDTHH:mm:ss.SSSZ" }),
errors({ stack: true }),
json(),
);
export const logger: Logger = winston.createLogger({
level: process.env.LOG_LEVEL ?? "info",
defaultMeta: { service: "api-gateway" },
format: process.env.NODE_ENV === "production" ? prodFormat : devFormat,
transports: [new transports.Console()],
exceptionHandlers: [new transports.File({ filename: "logs/exceptions.log" })],
rejectionHandlers: [new transports.File({ filename: "logs/rejections.log" })],
});
Transport Configuration
import winston, { format, transports } from "winston";
import DailyRotateFile from "winston-daily-rotate-file";
const logger = winston.createLogger({
level: "info",
format: format.combine(format.timestamp(), format.errors({ stack: true }), format.json()),
transports: [
// Console — all levels
new transports.Console({
format: format.combine(format.colorize(), format.simple()),
}),
// Combined log file — info and above
new transports.File({
filename: "logs/combined.log",
maxsize: 10 * 1024 * 1024, // 10 MB
maxFiles: 5,
tailable: true,
}),
// Error-only log file
new transports.File({
filename: "logs/error.log",
level: "error",
}),
// Daily rotation
new DailyRotateFile({
filename: "logs/app-%DATE%.log",
datePattern: "YYYY-MM-DD",
maxSize: "20m",
maxFiles: "14d",
zippedArchive: true,
level: "info",
}),
// HTTP transport — ship to a remote log aggregator
new transports.Http({
host: "logs.example.com",
port: 443,
ssl: true,
path: "/ingest",
level: "warn",
batchInterval: 5000,
}),
],
});
Key Techniques
Custom Log Levels
import winston from "winston";
const customLevels = {
levels: {
fatal: 0,
error: 1,
warn: 2,
info: 3,
http: 4,
debug: 5,
trace: 6,
},
colors: {
fatal: "red bold",
error: "red",
warn: "yellow",
info: "green",
http: "magenta",
debug: "cyan",
trace: "grey",
},
};
winston.addColors(customLevels.colors);
interface CustomLogger extends winston.Logger {
fatal: winston.LeveledLogMethod;
http: winston.LeveledLogMethod;
trace: winston.LeveledLogMethod;
}
const logger = winston.createLogger({
levels: customLevels.levels,
level: "trace",
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
transports: [
new winston.transports.Console({
format: winston.format.combine(winston.format.colorize(), winston.format.simple()),
}),
],
}) as CustomLogger;
logger.fatal("database connection pool exhausted");
logger.http("GET /api/users 200 45ms");
logger.trace("entering parseToken()");
Child Loggers with Metadata
// Winston uses .child() to create contextual loggers
const requestLogger = logger.child({
requestId: "req-abc-123",
userId: "user-42",
});
requestLogger.info("payment initiated", { amount: 4999, currency: "usd" });
// Output includes requestId, userId, amount, currency
function processOrder(orderId: string, log: winston.Logger) {
const orderLog = log.child({ orderId });
orderLog.info("validating order");
orderLog.info("charging payment");
orderLog.info("order completed");
}
processOrder("order-789", requestLogger);
Custom Formats
import { format } from "winston";
// Strip ANSI from file output
const stripAnsi = format((info) => {
if (typeof info.message === "string") {
info.message = info.message.replace(/\x1b\[[0-9;]*m/g, "");
}
return info;
});
// Add caller location
const callerInfo = format((info) => {
const stack = new Error().stack;
const callerLine = stack?.split("\n")[3]?.trim();
if (callerLine) {
const match = callerLine.match(/\((.+)\)/);
if (match) info.caller = match[1];
}
return info;
});
// Mask sensitive fields
const maskSensitive = format((info) => {
const sensitiveKeys = ["password", "token", "secret", "authorization", "ssn"];
for (const key of sensitiveKeys) {
if (key in info) {
(info as Record<string, unknown>)[key] = "***REDACTED***";
}
}
return info;
});
const logger = winston.createLogger({
format: format.combine(
maskSensitive(),
callerInfo(),
format.timestamp(),
format.json(),
),
transports: [new winston.transports.Console()],
});
Express Middleware Integration
import express, { type Request, type Response, type NextFunction } from "express";
import { randomUUID } from "crypto";
const app = express();
// Attach a child logger to every request
app.use((req: Request, _res: Response, next: NextFunction) => {
const requestId = (req.headers["x-request-id"] as string) ?? randomUUID();
req.log = logger.child({ requestId, method: req.method, path: req.path });
req.log.info("request started");
next();
});
// Log response timing
app.use((req: Request, res: Response, next: NextFunction) => {
const start = Date.now();
res.on("finish", () => {
const duration = Date.now() - start;
const level = res.statusCode >= 500 ? "error" : res.statusCode >= 400 ? "warn" : "info";
req.log[level]("request completed", {
statusCode: res.statusCode,
durationMs: duration,
});
});
next();
});
// Augment Express Request type
declare global {
namespace Express {
interface Request {
log: winston.Logger;
}
}
}
Error Handling Patterns
import winston, { format } from "winston";
const logger = winston.createLogger({
format: format.combine(
format.timestamp(),
format.errors({ stack: true }), // critical: must come before json()
format.json(),
),
transports: [new winston.transports.Console()],
// Catch unhandled exceptions and rejections
exceptionHandlers: [
new winston.transports.File({ filename: "logs/exceptions.log" }),
],
rejectionHandlers: [
new winston.transports.File({ filename: "logs/rejections.log" }),
],
exitOnError: false, // do not exit after logging an uncaughtException
});
// Logging errors — pass the Error as the first argument
try {
await riskyOperation();
} catch (err) {
// Winston detects Error objects and applies the errors() format
logger.error("operation failed", err as Error);
}
// Or attach as metadata
function handleError(error: Error, context: Record<string, unknown>) {
logger.error(error.message, {
stack: error.stack,
code: (error as NodeJS.ErrnoException).code,
...context,
});
}
Silence and Filtering
import { format } from "winston";
// Filter out health-check noise
const ignoreHealthChecks = format((info) => {
if (info.path === "/health" || info.path === "/ready") return false;
return info;
});
// Rate-limit repeated messages
const rateLimiter = format(() => {
const seen = new Map<string, number>();
return format((info) => {
const key = `${info.level}:${info.message}`;
const now = Date.now();
const lastSeen = seen.get(key) ?? 0;
if (now - lastSeen < 5000) return false; // suppress if seen within 5 s
seen.set(key, now);
return info;
})();
})();
const logger = winston.createLogger({
format: format.combine(ignoreHealthChecks(), format.timestamp(), format.json()),
transports: [new winston.transports.Console()],
});
Best Practices
- Always include
errors({ stack: true })in your format chain — without it, Error objects lose their stack traces during serialization. - Place
errors()beforejson()informat.combine— order matters; the errors format must enrich the info object before JSON serialization. - Use
defaultMetato stamp every log with service name and environment — this is essential for filtering in multi-service architectures. - Create child loggers per request rather than passing metadata to every
.info()call. - Set
exitOnError: falseto prevent the process from crashing after a logged uncaught exception; pair with process health monitoring. - Use
winston-daily-rotate-filefor production file logging — unbounded log files eventually fill the disk. - Separate transports by level — errors to a dedicated file makes incident investigation faster.
- Keep message strings static — put variable data in the metadata object, not interpolated into the message.
Anti-Patterns
- Omitting
errors({ stack: true })— the most common Winston mistake. Without it,logger.error("fail", err)produces{"message":"fail"}with no stack trace. - Using
format.simple()in production — it produces unstructured text that log aggregators cannot parse. Useformat.json(). - Creating multiple independent loggers — use
logger.child()or a single shared instance. MultiplecreateLoggercalls create separate transport pools and fragment your log stream. - Synchronous logging to files without rotation — eventually fills the disk and crashes the process at 3 AM.
- Logging sensitive data without a masking format — add a redaction format early in the chain; do not rely on developers remembering to omit fields.
- Catching exceptions with
exceptionHandlersbut settingexitOnError: true— the process dies anyway, making the handler mostly pointless. Decide on a strategy: crash-and-restart or log-and-continue. - Over-logging in hot paths — logging inside tight loops or per-item in batch processing generates enormous volume. Log summaries instead.
Install this skill directly: skilldb add logging-services-skills
Related Skills
Better Stack / Logtail
Better Stack (Logtail) logging — structured log ingestion, live tail, SQL-based querying, alerting, and uptime monitoring
Datadog Logging
Datadog log management — agent setup, library integration, log pipelines, facets, monitors, and APM correlation
Fluentd
Fluentd unified logging — input/output plugins, routing with tags, buffering, Kubernetes DaemonSet, and Fluent Bit
Logstash / ELK Stack
ELK Stack logging — Logstash pipelines, Elasticsearch indexing, Kibana dashboards, and Filebeat shippers
Papertrail
Papertrail cloud logging — syslog forwarding, live tail, search, alerts, and integration with app frameworks
Pino Logger
Pino: fast JSON logger for Node.js — child loggers, serializers, transports (pino-pretty, pino-http), redaction, Next.js integration, and log levels