Winston Logger - Complete Node.js Logging Guide [2026]

Updated Apr 21, 202614 min read

Winston is a popular Node.js logging library. It is designed to be a simple and universal tool for managing and storing logs across multiple destinations in Node.js applications.

Using Winston, you can set up multiple transports in a single logger, where each transport acts as a storage device for your logs. Each transport can be configured to handle specific log levels, allowing you to route different logs to different places. For example, you might configure transport so that error logs go to a database, while all logs just print to the console during development. Winston also gives you full control over log formatting and lets you define custom severity levels to suit your application's needs.

Key Features of Winston Logger

Here are the key features of Winston:

  1. Multiple Transports: Send logs to different destinations (console, files, databases, HTTP endpoints) simultaneously from a single logger.

  2. Log Levels: Comes with built-in severity levels like error, warn, info, debug, etc. You can also define your own custom levels.

  3. Custom Formatting: Control how your log messages look using built-in formatters like json, simple, colorize, timestamp, or combine them to build your own format.

  4. Exception & Rejection Handling: Automatically catch and log uncaught exceptions and unhandled promise rejections so your app doesn't silently crash.

  5. Querying Logs: Supports querying and streaming logs from specific transports, useful for reviewing historical log data.

  6. Profiling: Winston has built-in support for measuring how long operations take between two points of code. It is helpful in tracking performance.

  7. Child Loggers: You can create child loggers that inherit the parent's configuration while adding extra metadata, useful for adding context such as a request ID or module name.

Getting Started with Winston Logger

In this section, we will install Winston and create a basic logger that logs messages to the console.

Prerequisites

Before you proceed with the next steps, make sure you have Node.js installed on your machine.

Step 1: Install Winston

npm install winston

Step 2: Create a Basic Logger

Create a file called logger.js and add the following code:

logger.js
const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.simple(),
  transports: [
    new winston.transports.Console()
  ]
});

logger.info('This is an info message');
logger.warn('This is a warning message');
logger.error('This is an error message');

Step 3: Run and verify output

node logger.js
Output
info: This is an info message
warn: This is a warning message
error: This is an error message

Now that we have a basic logger up and running, let's explore each of Winston's core features in detail, starting with logging levels.

Logging Levels in Winston

By default, logging levels in Winston follow the severity ordering specified by RFC5424: all levels are assumed to be numerically ordered from most to least important.

By default, Winston uses NPM style logging levels, prioritized from 0 to 6 (highest to lowest):

{
  error: 0,    // Something broke
  warn: 1,     // Something might be wrong
  info: 2,     // General information
  http: 3,     // HTTP request logs
  verbose: 4,  // Detailed information
  debug: 5,    // Debugging details
  silly: 6     // Log absolutely everything
}

You can log a message in one of two ways: either call the level name directly as a method, or pass it as a string to log():

// Using the level as a method (preferred, cleaner)
logger.info('User signed up successfully');
logger.error('Failed to connect to database');

// Using log() with the level as a string
logger.log('info', 'User signed up successfully');
logger.log('error', 'Failed to connect to database');

Both approaches do exactly the same thing. The method-based style is more common because it's shorter and easier to read.

Winston also allows you to add levels on transport so that Winston will only log that level and everything above it (i.e., everything with a lower number). Messages below that threshold are ignored. For example, if you set a transport's level to warn, it will log warn (1) and error (0), but skip info (2), debug (5), and everything else. This lets you keep your console clean during production while still capturing detailed logs in a file.

Custom Logging Levels in Winston

Along with the pre-defined  npmsyslog, and cli levels available in winston, you can also choose to define your own. Just pass an object mapping level names to numeric priorities:

logger.js
const winston = require('winston');

const customLevels = {
  critical: 0,
  important: 1,
  normal: 2,
  trivial: 3
};

const logger = winston.createLogger({
  levels: customLevels,
  level: 'normal',
  transports: [
		new winston.transports.Console()
	]
});

// Winston automatically creates methods for each custom level
logger.critical('System is on fire');
logger.normal('User updated their profile');
logger.trivial('Button hover event tracked');
Output
{"level":"critical","message":"System is on fire"}
{"level":"normal","message":"User updated their profile"}

Log Formatting in Winston

In Winston (specifically Winston 3.x), formatting refers to the mechanism that dictates how log messages are transformed, structured, and serialized before they are output to a specific transport (like the console, a file, or a network stream).

At its core, a format in Winston is a function that takes an info object (which always contains at least level and message properties) and modifies it.

{
  level: 'info',
  message: 'Server started on port 3000'
}

The transport then takes this modified info object and writes it to the destination.

Formatting in Winston can be categorized into three main types based on how they are constructed and applied:

  1. Built-in Formats

    Winston provides a standard library of pre-defined formatters accessible via winston.format. They handle common logging requirements out of the box.

    Key built-in formats include:

    • winston.format.json(): Serializes the info object into a JSON string. Standard for production environments, parsing logs to systems like ELK or Datadog.
    • winston.format.simple(): Returns a basic string representation: level: message.
    • winston.format.colorize(): Adds ANSI colour codes to the level or message for terminal readability.
    • winston.format.timestamp(): Appends a timestamp property to the info object.
    • winston.format.printf(): Allows you to define a custom string layout using a callback function.
    • winston.format.errors({ stack: true }): Catches standard JavaScript Error objects and ensures their stack traces are included in the log output.

    For example:

    logger.js
    const winston = require('winston');
    
    const jsonLogger = winston.createLogger({
      level: 'info',
      format: winston.format.json(),
      transports: [new winston.transports.Console()]
    });
    
    jsonLogger.info('Server started', { port: 8080 });
    // Output: {"port":8080,"level":"info","message":"Server started"}
    
  2. Combined Formats (winston.format.combine)

    Winston allows you to chain multiple formats together using winston.format.combine(). The formats are executed in the order they are defined, operating on the info object sequentially like a pipeline.

    For example:

    logger.js
    const winston = require('winston');
    const { combine, timestamp, printf, colorize } = winston.format;
    
    // Define a custom string format for the final output
    const myFormat = printf(({ level, message, timestamp }) => {
      return `${timestamp} [${level}]: ${message}`;
    });
    
    const pipelineLogger = winston.createLogger({
      level: 'debug',
      format: combine(
        colorize(),          // 1. Colorize the level
        timestamp(),         // 2. Add a timestamp property
        myFormat             // 3. Format the final string
      ),
      transports: [new winston.transports.Console()]
    });
    
    pipelineLogger.debug('Debugging database connection');
    // Output: 2026-04-21T06:16:41.000Z [\u001b[34mdebug\u001b[39m]: Debugging database connection
    
  3. Custom Formats

    If the built-in formats do not meet your exact requirements, you can create a custom format using winston.format(transformFn). The transform function takes the info object and an optional opts object, modifies info, and returns it. If the function returns false, the log message is completely ignored (filtered out).

    For example:

    logger.js
    const winston = require('winston');
    
    // Create a custom format that redacts sensitive data
    const redactData = winston.format((info, opts) => {
      if (info.password) {
        info.password = '[REDACTED]';
      }
      if (info.creditCard) {
        info.creditCard = '**-**-****-' + info.creditCard.slice(-4);
      }
      return info; // Must return the modified info object
    });
    
    const customLogger = winston.createLogger({
      level: 'info',
      format: winston.format.combine(
        redactData(),
        winston.format.json()
      ),
      transports: [new winston.transports.Console()]
    });
    
    customLogger.info('User registration attempt', { 
      user: 'admin', 
      password: 'supersecretpassword123' 
    });
    
    //Output: {"level":"info","message":"User registration attempt","password":"[REDACTED]","user":"admin"}
    

Transports in Winston

A transport is simply a destination where your logs get sent. The most basic example is logging to the console, where you configure a single transport, and every log message flows to that one place. This is useful during development when you just want to see output in your terminal.

logger.js
const winston = require('winston');

const logger = winston.createLogger({
  transports: [
    new winston.transports.Console()
  ]
});

logger.info('Hello from basic transport!');

A single Winston logger instance can utilize multiple transports simultaneously. This allows you to route logs of different severity levels to different destinations, or to apply different formats depending on the destination (e.g., colorized text for the console, pure JSON for a file). This is helpful in production where you want real-time visibility in the terminal while also persisting logs to disk for later analysis. Each transport can have its own log level, so you could send only errors to a file but all messages to the console.

logger.js
const winston = require('winston');

const logger = winston.createLogger({
  transports: [
    new winston.transports.Console({ level: 'info' }),
    new winston.transports.File({ filename: 'app.log', level: 'error' })
  ]
});

logger.info('Shown only in console');
logger.error('Shown in console AND saved to app.log');

// Output: 
// {"level":"info","message":"Shown only in console"}
// {"level":"error","message":"Shown in console AND saved to app.log"}

You can also build a custom transport yourself by extending the base Transport class, giving you full control over where and how logs are handled, such as sending them to a database, Slack, or an external API. You implement a log() method that defines exactly what happens when a message arrives. This is the go-to approach when built-in transports don't fit your specific needs.

logger.js
const Transport = require('winston-transport');
const winston = require('winston');

class MyCustomTransport extends Transport {
  log(info, callback) {
    console.log(`[CUSTOM] ${info.level}: ${info.message}`);
    callback();
  }
}

const logger = winston.createLogger({
  transports: [new MyCustomTransport()]
});

logger.info('Handled by my custom transport!');

//Output: [CUSTOM] info: Handled by my custom transport!

There are dozens of community-maintained external transports for routing logs directly to databases (MongoDB, Redis), cloud services (AWS CloudWatch, Datadog), or message brokers.

Exceptions and Rejections in Winston

Winston logger intercepts and logs unhandled errors that would otherwise cause your Node.js process to crash. By default, Winston only logs what you explicitly tell it to. However, these two features allow it to "listen" for global failures.

Handling Uncaught Exceptions

An uncaught exception occurs when a synchronous error is thrown and not caught by a try...catch block. Normally, Node.js prints the stack trace to stderr and exits with code 1. When you configure exceptionHandlers in Winston, the logger attaches a listener to the process.on('uncaughtException') event.

By default, Winston will exit the process after logging an exception. You can override this by setting exitOnError: false, though this is generally discouraged as the process state may be corrupted. You can also direct exceptions to a specific file or remote endpoint separate from your standard application logs.

logger.js
const winston = require('winston');

const logger = winston.createLogger({
  transports: [
    new winston.transports.File({ filename: 'combined.log' })
  ],
  exceptionHandlers: [
    // This will catch any uncaught synchronous errors
    new winston.transports.File({ filename: 'exceptions.log' })
  ]
});

// Triggering an uncaught exception
throw new Error('This will be caught by the exceptionHandler');

Handling Unhandled Rejections

An unhandled rejection occurs when a Promise is rejected (e.g., an API call fails) and there is no .catch() block or await inside a try...catch to handle it.

For example:

logger.js
const winston = require('winston');

const logger = winston.createLogger({
  transports: [
    new winston.transports.Console()
  ],
  rejectionHandlers: [
    // This will catch any unhandled promise rejections
    new winston.transports.File({ filename: 'rejections.log' })
  ]
});

// Triggering an unhandled rejection
Promise.reject(new Error('Async failure captured by rejectionHandler'));

Profiling in Winston

Profiling in Winston is a lightweight mechanism for measuring the execution time of specific code blocks. It works by capturing a high-resolution timestamp when you "start" a profile, calculating the delta (duration) when you "stop" it, and then automatically emitting a log entry.

How does it work?

The profiling system uses a stateful timer keyed by a unique string (the "id"). The profiling can be split into two phases.

Start Phase: When logger.profile(id) is called for the first time with a specific ID, Winston stores the current timestamp in an internal map associated with that ID.

End Phase: When logger.profile(id) is called a second time with the same ID, Winston retrieves the stored timestamp, calculates the difference against the current time, deletes the ID from the internal map to free memory, and finally outputs a log message at the info level (by default) containing the duration in milliseconds.

The following example demonstrates how to profile a synchronous or asynchronous operation without using external hooks or abstractions.

logger.js
const winston = require('winston');

// 1. Setup a basic logger
const logger = winston.createLogger({
  level: 'debug',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [new winston.transports.Console()]
});

async function heavyTask() {
  // 2. Start the timer for 'database-query'
  logger.profile('database-query');

  // Simulate an asynchronous operation (e.g., DB call)
  await new Promise(resolve => setTimeout(resolve, 1500));

  // 3. End the timer. 
  // Winston calculates the delta and logs it automatically.
  logger.profile('database-query', { 
    message: 'Completed fetching user records', 
    level: 'debug' // Optional: override default 'info' level
  });
}

heavyTask();

The output will include a durationMs field, which is injected automatically by the profiling logic.

Output
{
  "level": "debug",
  "message": "Completed fetching user records",
  "timestamp": "2026-04-21T16:20:00.000Z",
  "durationMs": 1501
}

Centralizing Logs in a Log Management Tool

A running Node.js service emits logs from multiple sources: the application process, its dependencies, the runtime, and any sidecars or middleware in the request path. When these logs only live on the local filesystem or inside container stdout, debugging a production incident requires SSH access to individual hosts or kubectl logs against short-lived pods whose output disappears upon recycling. This breaks down as soon as the deployment scales beyond a single instance.

A log management tool solves this by collecting logs from all your services in one place, where you can search, filter, visualise, and set up alerts. For this section, we'll send our Winston logs to SigNoz, an open-source observability platform that supports logs, metrics, and traces under one roof. Because SigNoz is built on OpenTelemetry, the same setup works whether you use SigNoz Cloud or any other OTel-compatible backend without rewriting your logging code.

One of the biggest payoffs of shipping Winston logs through OpenTelemetry is automatic correlation between logs and traces. Logs emitted inside a trace span are enriched with trace_id and span_id, so when you are looking at a slow request in the traces view, you can jump straight to the exact log lines that were produced during that request. You also get structured search over your log metadata, log-based alerts, and dashboards, all without touching your existing logger.info() and logger.error() calls.

For step-by-step setup instructions, including both the no-code auto-instrumentation path and the code-level SDK setup, head over to the official guide: Send logs from Node.js Winston logger to SigNoz.

Conclusion

Winston covers the full range of what a Node.js application needs from a logging library: multiple transports, configurable log levels, a pipeline-style formatting system, automatic handling of uncaught exceptions and unhandled rejections, and built-in profiling for measuring operation duration. These features are enough to handle logging inside a single process, and the flexibility to plug in custom transports and formats means you can shape it to fit almost any workflow.

Once your application moves to production and runs across multiple instances, local log files are no longer useful on their own. Pairing Winston with a centralized log management tool like SigNoz gives you a single place to search, filter, and alert on logs from every service, along with automatic correlation between logs and traces when you ship them through OpenTelemetry.

Was this page helpful?

Your response helps us improve this page.

Tags
loggingnodejs