Guides
Winston and Morgan

A Complete Guide to Winston Logging in Node.js

Better Stack Team
Updated on May 4, 2022

Winston is the most popular logging library for Node.js. It aims to make logging more flexible and extensible by decoupling different aspects such as log levels, formatting, and storage so that each API is independent and many combinations are supported. It also uses Node.js streams to ensure that logging does not impact your application performance.

In this tutorial, we will explain how to install, set up, and use the Winston logger in a Node.js application. We'll go through all the options it provides and show how to customize them in various ways. Finally, we'll describe how to use it in conjunction with Morgan middleware for logging incoming requests in Express server.

Prerequisites

Before proceeding with the rest of this article, ensure that you have a recent version of Node.js and npm installed locally on your machine. This article also assumes that you are familiar with the basic concepts of logging in Node.js.

Getting started with Winston

Winston is available as an npm package, so you can install it through the command below:

npm install winston
Copied!

Next, import it into your Node.js application:

const winston = require('winston');
Copied!

Although Winston provides a default logger that you can use by calling a level method on the winston module, it the recommended way to use it is to create a custom logger through the createLogger() method:

const winston = require('winston');

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

Afterwards, you can start logging through methods on the logger object:

logger.info('Info message');
logger.error('Error message');
logger.warn('Warning message');
Copied!

This yields the following output:

Output
{"level":"info","message":"Info message"}
{"level":"error","message":"Error message"}
{"level":"warn","message":"Warning message"}

In subsequent sections, we'll examine all the properties that you can use to customize your logger so that you will end up with an optimal configuration for your Node.js application.

Log levels in Winston

Winston supports six log levels by default. It follows the order specified by the RFC5424 document. Each level is given an integer priority with the most severe being the lowest number and the least one being the highest.

{
  error: 0,
  warn: 1,
  info: 2,
  http: 3,
  verbose: 4,
  debug: 5,
  silly: 6
}
Copied!

The six log levels above each correspond to a method on the logger:

logger.error('error');
logger.warn('warn');
logger.info('info');
logger.verbose('verbose');
logger.debug('debug');
logger.silly('silly');
Copied!

You can also pass a string representing the logging level to the log() method:

logger.log('error', 'error message');
logger.log('info', 'info message');
Copied!

The level property on the logger determines which log messages will be emitted to the configured transports (discussed later). For example, since the level property was set to info in the previous section, only log entries with a minimum severity of info (or maximum integer priority of 2) will be written while all others are suppressed. This means that only the info, warn, and error messages will produce output with the current configuration.

To cause the other levels to produce output, you'll need to change the value of level property to the desired minimum. The reason for this configuration is so that you'll be able to run your application in production at one level (say info) and your development/testing/staging environments can be set to a less severe level like debug or silly, causing more information to be emitted.

The accepted best practice for setting a log level is to use an environmental variable. This is done to avoid modifying the application code when the log level needs to be changed.

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.json(),
  transports: [new winston.transports.Console()],
});
Copied!

With this change in place, the application will log at the info level if the LOG_LEVEL variable is not set in the environment.

Customizing log levels in Winston

Winston allows you to readily customize the log levels to your liking. The default log levels are defined in winston.config.npm.levels, but you can also use the Syslog levels through winston.config.syslog.levels:

const logger = winston.createLogger({
  levels: winston.config.syslog.levels,
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.cli(),
  transports: [new winston.transports.Console()],
});
Copied!

The syslog levels are shown below:

{
  emerg: 0,
  alert: 1,
  crit: 2,
  error: 3,
  warning: 4,
  notice: 5,
  info: 6,
  debug: 7
}
Copied!

Afterwards, you can log using the methods that correspond to each defined level:

winston.emerg("Emergency");
winston.crit("Critical");
winston.warning("Warning");
Copied!

If you prefer to change the levels to a completely custom system, you'll need to create an object and assign a number priority to each one starting from the most severe to the least. Afterwards, assign that object to the levels property in the configuration object passed to the createLogger() method.

const logLevels = {
  fatal: 0,
  error: 1,
  warn: 2,
  info: 3,
  debug: 4,
  trace: 5,
};

const logger = winston.createLogger({
  levels: logLevels,
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.json(),
  transports: [new winston.transports.Console()],
});
Copied!

You can now use methods on the logger that correspond to your custom log levels as shown below:

logger.fatal('fatal!');
logger.trace('trace!');
Copied!

Formatting your log messages

Winston outputs its logs in JSON format by default, but it also supports other formats which are accessible on the winston.format object. For example, if you're creating a CLI application, you may want to switch this to the cli format which will print a color coded output:

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.cli(),
  transports: [new winston.transports.Console()],
});
Copied!

Winston CLI format

The formats available in winston.format are defined in the logform module. This module provides the ability to customize the format used by Winston to your heart's content. You can create a completely custom format or modify an existing one to add new properties.

Here's an example that adds a timestamp field to the each log entry:

const winston = require('winston');
const { combine, timestamp, json } = winston.format;

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: combine(timestamp(), json()),
  transports: [new winston.transports.Console()],
});
Copied!

When you use a level method on the logger, you'll see a datetime value formatted as new Date().toISOString().

logger.info('Info message')
Copied!
Output
{"level":"info","message":"Info message","timestamp":"2022-01-25T15:50:09.641Z"}

You can change the format of this datetime value by passing an object to timestamp() as shown below. The string value of the format property below must be one acceptable by the fecha module.

timestamp({
  format: 'YYYY-MM-DD hh:mm:ss.SSS A', // 2022-01-25 03:23:10.350 PM
})
Copied!

You can also create a entirely different format as shown below:

const winston = require('winston');
const { combine, timestamp, printf, colorize, align } = winston.format;

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: combine(
    colorize({ all: true }),
    timestamp({
      format: 'YYYY-MM-DD hh:mm:ss.SSS A',
    }),
    align(),
    printf((info) => `[${info.timestamp}] ${info.level}: ${info.message}`)
  ),
  transports: [new winston.transports.Console()],
});

logger.info('Info message');
logger.error('Error message');
logger.warn('Warning message');
Copied!

The combine() method merges multiple formats into one, while colorize() assigns colors to the different log levels so that each level is easily identifiable. The timestamp() method outputs a datatime value that corresponds to the time that the message was emitted. The align() method aligns the log messages, while printf() defines a custom structure for the message. In this case, it outputs the timestamp and log level followed by the message.

Output
[2022-01-27 06:37:27.653 AM] info:      Info message
[2022-01-27 06:37:27.656 AM] error:     Error message
[2022-01-27 06:37:27.656 AM] warn:      Warning message

While you can format your log entries in any way you wish, the recommended practice for server applications is to stick with a structured logging format (like JSON) so that your logs can be easily machine readable for filtering and gathering insights.

Configuring transports in Winston

Transports in Winston refer to the storage location for your log entries. Winston provides great flexibility in choosing where you want your log entries to be outputted to. The following transport options are available in Winston by default:

  • Console: output logs to the Node.js console.
  • File: store log messages to one or more files.
  • HTTP: stream logs to an HTTP endpoint.
  • Stream: output logs to any Node.js stream.

Thus far, we've demonstrated the default Console transport to output log messages to the Node.js console. Let's look at how we can store logs in a file next.

const winston = require('winston');
const { combine, timestamp, json } = winston.format;

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: combine(timestamp(), json()),
  transports: [
    new winston.transports.File({
      filename: 'combined.log',
    }),
  ],
});

logger.info('Info message');
logger.error('Error message');
logger.warn('Warning message');
Copied!

The snippet above configures the logger to output all emitted log messages to a file named combined.log. When you run the snippet above, this file will be created with the following contents:

{"level":"info","message":"Info message","timestamp":"2022-01-26T09:38:17.747Z"}
{"level":"error","message":"Error message","timestamp":"2022-01-26T09:38:17.748Z"}
{"level":"warn","message":"Warning message","timestamp":"2022-01-26T09:38:17.749Z"}
Copied!

In a production application, it may not be ideal to log every single message into a single file as that will make filtering critical issues harder since it will be mixed together with inconsequential messages. A potential solution would be to use two File transports, one that logs all messages to a combined.log file, and another that logs messages with a minimum severity of error to a separate file.

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: combine(timestamp(), json()),
  transports: [
    new winston.transports.File({
      filename: 'combined.log',
    }),
    new winston.transports.File({
      filename: 'app-error.log',
      level: 'error',
    }),
  ],
});
Copied!

With this change in place, all your log entries will still be outputted to the combined.log file, but a separate app-error.log will also be created and it will contain only error messages. Note that the level property on the File() transport signifies the minimum severity that should be logged to the file. If you change it from error to warn, it means that any message with a minimum severity of warn will be logged to the app-error.log file (warn and error levels in this case).

A common need that Winston does not enable by default is the ability to log each level into different files so that only info messages go to an app-info.log file, debug messages into an app-debug.log file, and so on (see this GitHub issue. The way to get around this is to use a custom format on the transport to filter the messages by level. This is possible on any transport (not just File), since they all inherit from winston-transport.

const errorFilter = winston.format((info, opts) => {
  return info.level === 'error' ? info : false;
});

const infoFilter = winston.format((info, opts) => {
  return info.level === 'info' ? info : false;
});

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: combine(timestamp(), json()),
  transports: [
    new winston.transports.File({
      filename: 'combined.log',
    }),
    new winston.transports.File({
      filename: 'app-error.log',
      level: 'error',
      format: combine(errorFilter(), timestamp(), json()),
    }),
    new winston.transports.File({
      filename: 'app-info.log',
      level: 'info',
      format: combine(infoFilter(), timestamp(), json()),
    }),
  ],
});
Copied!

The above code logs only error messages in the app-error.log file and info messages to the app-info.log file. What happens is that the custom format (infoFilter() and errorFilter()) checks if the level of a log entry matches the desired level and returns false if they do not match which causes the message not to be emitted wherever the function is used. You can customize this further or create other filters as you see fit.

Log rotation in Winston

Logging into files can quickly get out of hand if you keep logging to the same file as the file can get extremely large and become cumbersome to manage. This is where log rotation can come in handy. The main idea behind log rotation is to restrict the size of your log files and create new ones based on some predefined criteria. For example, you can create a new log file every day and automatically delete those older than a time period (say 30 days).

Winston provides the winston-daily-rotate-file module for this purpose. It is a transport that logs to a rotating file that is configurable based on date or file size, while older logs can be auto deleted based on count or elapsed days.

Install it through npm as shown below:

npm install winston-daily-rotate-file
Copied!

Once you install it, you can use it to replace the default File transport as shown below:

const winston = require('winston');
require('winston-daily-rotate-file');

const { combine, timestamp, json } = winston.format;

const fileRotateTransport = new winston.transports.DailyRotateFile({
  filename: 'combined-%DATE%.log',
  datePattern: 'YYYY-MM-DD',
  maxFiles: '14d',
});

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: combine(timestamp(), json()),
  transports: [fileRotateTransport],
});
Copied!

The datePattern property controls how often the file should be rotated (every day), and the maxFiles property ensures that log files that are older than 14 days are automatically deleted. You can also listen for the following events on a rotating file transport if you want to perform some action on cue:

// fired when a log file is created
fileRotateTransport.on('new', (filename) => {});
// fired when a log file is rotated
fileRotateTransport.on('rotate', (oldFilename, newFilename) => {});
// fired when a log file is archived
fileRotateTransport.on('archive', (zipFilename) => {});
// fired when a log file is deleted
fileRotateTransport.on('logRemoved', (removedFilename) => {});
Copied!

Custom transports in Winston

Winston supports the ability to create your own transports or utilize one made by the community. A custom transport may be used to store your logs in a database, log management tool, or some other location. Here are some custom transports that you might want to check out:

Adding metadata to your logs

Winston supports the addition of metadata to log messages. You can add default metadata to all log entries, or specific metadata to individual logs. Let's start with the former which can be added to a logger instance through the defaultMeta property:

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  defaultMeta: {
    service: 'admin-service',
  },
  format: winston.format.json(),
  transports: [new winston.transports.Console()],
});

logger.info('Info message');
logger.error('Error message');
Copied!

When you log any message using the logger above, the contents of the defaultMeta object will be injected into each entry:

Output
{"level":"info","message":"Info message","service":"admin-service"}
{"level":"error","message":"Error message","service":"admin-service"}

This default metadata can be used to differentiate log entries by service or other criteria when logging to the same location from different services.

Another way to add metadata to your logs is by creating a child logger through the child method. This is useful if you want to add certain metadata that should be added to all log entries in a certain scope. For example, if you add a requestId to your logs entries, you can search your logs and find the all the entries that pertain to a specific request.

const childLogger = logger.child({ requestId: 'f9ed4675f1c53513c61a3b3b4e25b4c0' });

childLogger.info('Info message');
childLogger.info('Error message');
Copied!
Output
{"level":"info","message":"Info message","requestId":"f9ed4675f1c53513c61a3b3b4e25b4c0","service":"admin-service"}
{"level":"error","message":"Error message","requestId":"f9ed4675f1c53513c61a3b3b4e25b4c0","service":"admin-service"}

A third way to add metadata to your logs is to pass an object to the level method at its call site:

const childLogger = logger.child({ requestId: 'f9ed4675f1c53513c61a3b3b4e25b4c0' });

childLogger.info('File uploaded successfully', {
  file: 'something.png',
  type: 'image/png',
  userId: 'jdn33d8h2',
});
Copied!
Output
{"file":"something.png","level":"info","message":"File uploaded successfully","requestId":"f9ed4675f1c53513c61a3b3b4e25b4c0","service":"admin-service","type":"image/png","userId":"jdn33d8h2"}

Handling uncaught exceptions and uncaught promise rejections

Winston provides the ability to automatically catch and log uncaught exceptions and uncaught promise rejections on a logger. You'll need to specify the transport where these events should be emitted to through the exceptionHandlers and rejectionHandlers properties respectively:

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.json(),
  transports: [new winston.transports.Console()],
  exceptionHandlers: [
    new winston.transports.File({ filename: 'exception.log' }),
  ],
  rejectionHandlers: [
    new winston.transports.File({ filename: 'rejections.log' }),
  ],
});
Copied!

The logger above is configured to log uncaught exceptions to an exception.log file, while uncaught promise rejections are placed in a rejections.log file. You can try this out by throwing an error somewhere in your code without catching it.

throw new Error('An uncaught error!');
Copied!

You'll notice that the entire stack trace is included in the log entry for the exception, along with the date and message. Winston will also cause the process to exit with a non-zero status code once it logs an uncaught exception. You can change this by setting the exitOnError property on the logger to false as shown below:

const logger = winston.createLogger({ exitOnError: false });

// or

logger.exitOnError = false;
Copied!

Note that the accepted best practice is to exit immediately after an uncaught error is detected as the program will be in an undefined state. You can use a process manager (such as PM2) to automatically restart your application when such situations occur while you try to debug the problem. Unhandled promise rejections do not currently cause Node.js applications to exit.

Profiling your Node.js code with Winston

Winston also provides basic profiling capabilities on any logger through the profile() method. You can use it to collect some basic performance data in your application hotspots.

// start a timer
const profiler = logger.startTimer();

setTimeout(() => {
  // End the timer and log the duration
  profiler.done({ message: 'Logging message' });
}, 1000);
Copied!

The code above produces the following output:

{"durationMs":1002,"level":"info","message":"Logging message"}
Copied!

As you can see, the durationMs property contains the timers' duration. Also, profiling log entries are set to the info level by default, but you can change this by setting the level property in the argument to profiler.done().

profiler.done({ message: 'Logging message', level: 'debug' });
Copied!

Working with multiple loggers in Winston

A large application will often have multiple loggers with different settings for logging in different areas of the application. This is exposed in Winston through winston.loggers:

const serviceALogger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  defaultMeta: {
    'service': 'service-a'
  },
  format: winston.format.cli(),
  transports: [new winston.transports.File(
    {
      filename: "service-a.log"
    }
  )],
});

const serviceBLogger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  defaultMeta: {
    'service': 'service-b'
  },
  format: winston.format.json(),
  transports: [new winston.transports.Console()],
});


winston.loggers.add('serviceALogger', serviceALogger)
winston.loggers.add('serviceBLogger', serviceBLogger)
Copied!

Once you've preconfigured the loggers, as shown above, you can access them in any file using the winston.loggers.get():

const winston = require('winston');

const serviceALogger = winston.loggers.get('serviceALogger');
const serviceBLogger = winston.loggers.get('serviceBLogger');

serviceBLogger.warn('logging to the console');
serviceALogger.debug('logging to a file');
Copied!

Logging in an Express application using Winston and Morgan

Morgan is an HTTP request logger middleware for Node.js that automatically logs the details of incoming requests to the server (such as the remote IP Address, request method, HTTP version, response status, user agent, etc.), and generate the logs in the specified format. The main advantage of using Morgan is that it saves you the trouble of writing a custom middleware for this purpose.

Here's a simple program demonstrating Winston and Morgan being used together in an Express application. When you connect Morgan with Winston, all your logging is formatted the same way and goes to the same place.

const winston = require('winston');
const express = require('express');
const morgan = require('morgan');
const axios = require('axios');

const app = express();

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

const morganMiddleware = morgan(
  ':method :url :status :res[content-length] - :response-time ms',
  {
    stream: {
      // Configure Morgan to use our custom logger with the http severity
      write: (message) => logger.http(message.trim()),
    },
  }
);

app.use(morganMiddleware);

app.get('/crypto', async (req, res) => {
  try {
    const response = await axios.get(
      'https://api2.binance.com/api/v3/ticker/24hr'
    );

    const tickerPrice = response.data;

    res.json(tickerPrice);
  } catch (err) {
    logger.error(err);
    res.status(500).send('Internal server error');
  }
});

app.listen('5000', () => {
  console.log('Server is running on port 5000');
});
Copied!

The morgan() middleware function takes two arguments: a string describing the format of the log message and the configuration options for the logger. The format is composed up of individual tokens, which can be combined in any order. You can also use a predefined format instead. In the second argument, the stream property is set to log the provided message using our custom logger at the http severity. This reflects the severity that all Morgan events will be logged at.

Before you execute this code, install all the required dependencies first:

npm install winston express morgan axios
Copied!

Afterwards, you can start the server and send requests to the /crypto route. You should see the following output from Morgan:

Output
http:    GET /crypto 200 981932 - 2792.889 ms
http:    GET /crypto 200 981946 - 2761.354 ms
http:    GET /crypto 200 981914 - 2637.608 ms
http:    GET /crypto 200 981415 - 3076.901 ms

Conclusion

In this article, we've examined the Winston logging package for Node.js applications. It provides everything necessary in a logging framework, such as structured (JSON) logging, colored output, log levels, and the ability to log to several locations. It also has a simple API and is easy to customize which makes it a suitable solution for any type of project.

I hope this article has helped you learn about everything that you can do with Winston, and how it may be used to develop a good logging system in your application. Don't forget to check out it's official documentation pages to learn more.

Thanks for reading, and happy coding!

Centralize all your logs into one place.
Analyze, correlate and filter logs with SQL.
Create actionable
dashboards with Grafana.
Share and comment with built-in collaboration.
Got an article suggestion? Let us know
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.