Contents
- Prerequisites
- Getting started with Winston
- Log levels in Winston
- Customizing log levels in Winston
- Formatting your log messages
- Configuring transports in Winston
- Log rotation in Winston
- Custom transports in Winston
- Adding metadata to your logs
- Handling uncaught exceptions and uncaught promise rejections
- Profiling your Node.js code with Winston
- Working with multiple loggers in Winston
- Logging in an Express application using Winston and Morgan
- Conclusion
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
Next, import it into your Node.js application:
const winston = require('winston');
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()],
});
Afterwards, you can start logging through methods on the logger
object:
logger.info('Info message');
logger.error('Error message');
logger.warn('Warning message');
This yields the following 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
}
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');
You can also pass a string representing the logging level to the log()
method:
logger.log('error', 'error message');
logger.log('info', 'info message');
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()],
});
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()],
});
The syslog
levels are shown below:
{
emerg: 0,
alert: 1,
crit: 2,
error: 3,
warning: 4,
notice: 5,
info: 6,
debug: 7
}
Afterwards, you can log using the methods that correspond to each defined level:
winston.emerg("Emergency");
winston.crit("Critical");
winston.warning("Warning");
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()],
});
You can now use methods on the logger
that correspond to your custom log
levels as shown below:
logger.fatal('fatal!');
logger.trace('trace!');
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()],
});
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()],
});
When you use a level method on the logger
, you'll see a datetime value
formatted as new Date().toISOString()
.
logger.info('Info message')
{"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
})
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');
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.
[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');
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"}
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',
}),
],
});
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()),
}),
],
});
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
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],
});
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) => {});
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:
- winston-mongodb - transport logs to MongoDB.
- winston-syslog - transport logs to Syslog.
- winston-telegram - send logs to Telegram.
- @logtail/winston - send logs to Logtail.
- winston-mysql - store logs in MySQL.
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');
When you log any message using the logger
above, the contents of the
defaultMeta
object will be injected into each entry:
{"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');
{"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',
});
{"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' }),
],
});
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!');
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;
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);
The code above produces the following output:
{"durationMs":1002,"level":"info","message":"Logging message"}
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' });
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)
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');
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');
});
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
Afterwards, you can start the server and send requests to the /crypto
route.
You should see the following output from Morgan:
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!
dashboards with Grafana.
dashboards with Grafana.