Skip to main content
Technology & EngineeringLogging Services372 lines

Winston Logger

Winston: versatile Node.js logger — transports (Console, File, HTTP), formats, custom levels, metadata, daily rotate file, and error handling

Quick Summary18 lines
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 lines
Paste into your CLAUDE.md or agent config

Winston 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

  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.

Anti-Patterns

  1. Omitting errors({ stack: true }) — the most common Winston mistake. Without it, logger.error("fail", err) produces {"message":"fail"} with no stack trace.
  2. Using format.simple() in production — it produces unstructured text that log aggregators cannot parse. Use format.json().
  3. Creating multiple independent loggers — use logger.child() or a single shared instance. Multiple createLogger calls create separate transport pools and fragment your log stream.
  4. Synchronous logging to files without rotation — eventually fills the disk and crashes the process at 3 AM.
  5. Logging sensitive data without a masking format — add a redaction format early in the chain; do not rely on developers remembering to omit fields.
  6. Catching exceptions with exceptionHandlers but setting exitOnError: true — the process dies anyway, making the handler mostly pointless. Decide on a strategy: crash-and-restart or log-and-continue.
  7. 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

Get CLI access →