Complete Winston Logger Guide With Hands-on Examples

Logging is critical for monitoring and troubleshooting your Node.js project. The open-source Winston logger helps take a load off our shoulders by making it easier to centralize, format, enrich, and distribute the logs to fit a particular need.

Winston creates custom logger instances which can be configured to act as centralized logging entities. Essentially, the internal architecture of the module decouples the actual event logging from the implementation of the storage logic.

A simple console.log sends everything to the default standard output (i.e. to your console screen), and redirecting that flow into a centralized, managed location is not a simple task. An external plugin such as Winston conditionally redirects your logging activities’ destination. This gives developers great flexibility when it comes to choosing, and even switching between different storage options.

Why do we need a logger like Winston?

A logger offers many benefits over any type of manual logging techniques, such as the good old console.log. Winston offers:

1. Centralized control over how and when to log

With Winston, you can change your code in a single place and customize the format of your logs (e.g., logging in JSON format, or adding a timestamp). These are far superior options compared to those available when you use a console.log command, which requires logging code to be spread across your entire code base, breaking DRY principles.

2. Control where your logs are sent

Controlling where logs are saved is crucial for organizing the data and making it easier to query. Although console.log allows the redirection of logs via command line redirections, there are limitations (such as not being able to send them to a 3rd party system). With Winston, you can save your logs to multiple destinations (such as Elasticsearch, MongoDB, etc.). You can conditionally select the output destination and more.

3. Custom logging formats

When logging into a standard output, such as your terminal window, you can control the format for your logs. This enables you to improve readability by choosing the color of your text and format of your logging messages such as pre-fixing with a timestamp, or the logging level.

4. Extra context

This is particularly useful when the output arises from a distributed architecture. When you’re sending logs from different places into a single platform, you may add context to your log messages to identify their origin. For example, the source IP, or the server’s ID; anything that identifies where the data is coming from.

I’m sure you can think of specific benefits for your particular use case given a centralized logging library as powerful as Winston is.

Winston vs. Morgan vs. Bunyan

Although Winston logger is definitely one of the most powerful and flexible options out there, there are others that might better fit your needs depending on your particular use case.

Morgan

Morgan works with frameworks that are compatible with modules such as Express.js. As opposed to Winston, which is a general-purpose logger capable of great flexibility and customization, Morgan is intended to be used as middleware to customize the format of the HTTP-request event logline (and any associated errors during the processing of such requests).

As mentioned before, by default, all logs are written to the standard output, as with console.log, but Morgan allows you to override that and provide a custom stream and storage destination. If you’re looking for a basic logger for your web server, this might be a good choice.

Bunyan

Bunyan is very similar to the Winston logger from a feature set point of view. This is not a simple logger. On the contrary, it provides fine-tuned control over both the data being logged and its output destination.

Bunyan’s output is in JSON format. A downside is this makes visual inspection a bit difficult, however, it does simplify the automated interpretation of the loglines.

Human vs. Machine Destined Logs

Here is a comparison of a human-destined log (free text, with minimum formatting) vs. a machine-destined log (JSON):

Human Destined Message

info: [2020-03-10 13:03:23hs] Hello there, this is an info message! error: [2020-03-12 02:20:32hs] There was an error on line 26 of file index.js please review!

Machine Destined Message

{"message":"Info message easy to parse","level":"info"}

{"message":"There was an error, please review the details","level":"error", "details": { "file": "index.js", "line": 12, "error_msg": "Parse error"}

If you’re looking for a flexible logger that works well with other systems (as opposed to people) then Bunyan might be a good option for you.

Winston & Morgan Make a Great Partnership

Morgan and Winston are highly compatible, and leveraging both modules provides the best of both worlds. Morgan provides the middleware for Express.js (and similar frameworks), capable of logging HTTP requests–with outputs directed to the standard terminal window. Whereas, the Winston logger allows you to overwrite, and customize, the output channel/s.

So, if you currently do something like this:

const morgan = require('morgan')

//...

app.use(morgan('tiny'))

Now, you can change it to something like:

//defining a custom logger
let logger = new (winston.Logger)({
    exitOnError: false,
    level: 'info',
    transports: [
        new (winston.transports.Console)(),
        new (winston.transports.File)({ filename: 'app.log'})
    ]
})

//using the logger and its configured transports, to save the logs created by Morgan
const myStream = {
    write: (text: string) => {
        logger.info(text)
    }
}

app.use(morgan('combined', { stream: myStream }));

Every single HTTP Request received, and any subsequent error response is logged. Then, with a custom logger defined (more on this in a second), and configured to save loglines to both the console and a file, you can output logs to several places simultaneously.

How to Use The Winston Logger

Winston is a highly intuitive tool that is easy to customize. Next, we will dive a little deeper into some commands to perform functions that you may find useful.

How to Define a Custom Logger

We will create a custom instance of Winston–which gives us the ability to customize its properties (e.g., the colors used, the logging levels, the storage device for logs, etc.).

In this example, to create a custom logger, we’ll use the createLogger method provided by Winston:

const config = require("config")

const { createLogger, format, transports } = require('winston');
const { combine, splat, timestamp, printf } = format;

const myFormat = printf( ({ level, message, timestamp , ...metadata}) => {
  let msg = `${timestamp} [${level}] : ${message} `  
  if(metadata) {
	msg += JSON.stringify(metadata)
  }
  return msg
});

const logger = createLogger({
  level: 'debug',
  format: combine(
	format.colorize(),
	splat(),
	timestamp(),
	myFormat
  ),
  transports: [
	new transports.Console({ level: 'info' }),
	new transports.File({ filename: config.get("app.logging.outputfile"), level: 'debug' }),
  ]
});
module.exports = logger

More on the details down below, but first, let’s break down the code provided. The formatter function myFormat ensures that the logline has the required information and format. While the createLogger function defines parameters such as the maximum level to take into consideration and the list of storage devices to use for different log levels.

Levels, and Custom levels

The level defines the severity of a logged incident. There are many reasons why you might add logs to your code at any given point. These reasons are usually tied to a particular level. For example,the Debug level is usually applied when adding logging lines to understand a particular behavior. While adding a logging line to record when an error occurs is usually associated with the Error level.

The full list of default levels that come out-of-the-box with Winston are:

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

Notice the values; these signify the severity associated with the level; the lower the severity of the logged event, the higher the value. So, a debug message is considerably less important than a warning message.

With Winston, you can customize the levels if the default offerings don’t suit. That means that you can do the following:

const myLevels = {
	superImportant: 0,
	mediocre: 1,
	whoCares: 2
}

const logger = createLogger({
  levels: myLevels
});

Now, for the icing on this level-flavored cake, you can then reference the level you want directly by using its name as a method from the custom logger you just created:

logger.superImportant(“This message needs to be seen, something happened!”)
logger.whoCares(“Blah!”)

The same call can be done with the default levels; they are available as methods for you to use.

Formats and Custom Formats

When it comes to defining how your log messages look, the Winston logger provides flexibility. By default, the log message is not formatted and is printed as a JSON string with two parameters, message and level (where the message contains the actual text you’re logging and level a string with the name of the log level, such as ‘info’). However, overwriting that and adding parameters such as predefined tokens, timestamps, etc. is straightforward.

Winston uses logform to handle the log-formatting aspects. If you want to read the full list of predefined formats follow the link to their full documentation.

For example, let’s say you wanted to format your logs, so you get a timestamp and a custom label, and everything turned into a single string (instead of the default JSON). You can do something like:

const { createLogger, format, transports } = require('winston');
const { splat, combine, timestamp, label, printf, simple } = format;

const logger = createLogger({
  format: combine(
	label({ label: 'CUSTOM', message: true }),
	timestamp(),
    	simple()
  ),
  transports: [new transports.Console()]
});


logger.info("Hello there!")
logger.error("Testing errors too!")
info: [CUSTOM] Hello there! {"timestamp":"2020-03-13T05:37:32.071Z"}
error: [CUSTOM] Testing errors too! {"timestamp":"2020-03-13T05:37:32.074Z"}

As you can see, the level and the actual message are correctly formatted. However, we’re still getting an object-like structure at the end. That is because the timestamp format function adds that property to the log object (which by default only has message and level properties).

To solve that issue, you can create a custom formatter function, such as:

const myFormat = printf( ({ level, message, timestamp }) => {
  return `${timestamp} ${level}: ${message}`;
});

And then, instead of calling the simple formatter, we use myFormat, like so:

const logger = createLogger({
  format: combine(
	label({ label: 'CUSTOM', message: true }),
	timestamp(),
    	myFormat()
  ),
  transports: [new transports.Console()]
});

The output now becomes reader-friendly:

2020-03-13T07:02:26.607Z info: [CUSTOM] Hello there!
2020-03-13T07:02:26.609Z error: [CUSTOM] Testing errors too!

Filtering

You can even use the format configuration to filter out log messages you don’t wish to save. This works by adding a specific property to the logged object. For instance, the following logger will ignore any log that has an “ignore” property set to true:

const ignoreWhenTrue = format((info, opts) => {
  if (info.ignore) { return false; }
  return info;
});

const logger = createLogger({
  format: format.combine(
    ignoreWhenTrue(),
    format.json()
  ),
  transports: [new transports.Console()]
});

Now, you can temporarily add the attribute to silence one particular log message that’s not important, like so:

logger.log({
  private: true,
  level: 'error',
  message: 'This is top secret - hide it.'
});

Notice how this is accomplished with the log method, instead of using the custom methods added dynamically based on the level.

Transporters

Personally, transporters are my favorite feature from Winston because they allow you to switch between storage destinations for logs with ease. You can even have transporters directing logs to several storage devices simultaneously; either sending all logs or sending logs conditionally to various targets.

The internal architecture enables users of the module to create and publish their own independent transports. There are over 30 transports options, which include logging out into a single file, the console, or to 3rd party systems, such as AWS’s S3, Elasticsearch, MySQL, MongoDB, and many more.

You may have noticed that the code samples from before all contained at least one transport. To define them, you can add a transports array (which can contain as many transports as you’d like), when you configure the createLogger function.

Each transport sends data to storage devices that will have their own custom properties, depending on what they do with the data (you’ll need to read through their docs to get those details). But, there are two parameters that all transports implement:

  • Level: This attribute sets the specific level for this transport to take into consideration. Any message that has a different level will be ignored. So although it’s optional, if you use it, ensure you don’t accidentally leave log levels without a transport assigned.
  • Format: Just like the general format from above, you can also customize the format of specific transporters, giving you further control over the way you output data.

For example, this logger will only save logs to files, however, which file is conditional upon on the log level:

const logger = winston.createLogger({
transports: [
new winston.transports.File({
filename: 'error.log',
level: 'error',
format: winston.format.json()
}),
new transports.Http({
level: 'warn',
format: winston.format.json()
}),
new transports.Console({
level: 'info',
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
})
]
});

Streaming

One particular transport that you can use is the stream transport, which allows you to open up any Node.js stream and send logs into it.

This particular transport supports the following configuration properties:

  • Stream: The Node.js stream to add logs to. If objectMode is set to true, then the entire log object will be logged (including the message, the level, and any other extra attributes added by the formatters). Otherwise, only the message attribute will be sent
  • Level: This defines which level/s the transport should log. If you don’t set it, then the logger object will use it’s own configuration to decide which to log and which to ignore
  • Silent: If set to true, it suppresses output. By default it’s set to false
  • EOL: The end-of-line character to use. By default, it applies os.EOL

Profile Messages

Another interesting feature that Winston brings to the table is the ability to profile and debug your code. This module has the required methods to simplify that task for you.

If you’re looking to profile a piece of code, then you want to measure the time a piece of code takes to execute. Without a built in function, you would normally do something like:

let start = Date.now()

setTimeout(_ => {
    	let end = Date.now()
    	console.log("This took: ", (end - start) / 1000, " seconds to execute")
}, 1000)

With an output like the following:

This took: 1.003 seconds to execute

Of course, the example is over simplified, but you get the point. With Winston, however, we can do the following:

logger.profile("testing")
setTimeout(_ => {
    	logger.profile("testing")
}, 1000)

Assuming your logger was already created, you can use the profile method, which automatically starts and ends the timer for you and logs the message, as you can see below, with an INFO level message.

 {"level":"info","durationMs":1007,"message":"testing"}

You can read here about other ways you can profile your code with Winston, but remember that, by customizing the format and other properties of the custom logger, you can affect the output from this method.

The Winston Logger as an Overall Logging Solution

If you are choosing an overall logging solution for your application, be it centralized or distributed, Winston is your bread and butter. Everything should go through your custom loggers and you can tweak the logic behind it by changing just a few lines of code.

Taking it a step further, you might find the need to add more enterprise-level capabilities like ML-powered alerts and hosted, scaled secured ELK stack. Coralogix can help get you there faster. Our integration for Winston makes it a painless process.

Node logging best practices and tips

As is traditional with the JavaScript world, there are a dizzying amount of options for node log monitoring. In this article, I will dig into some of the better and lesser-known options to see what’s on offer and help you log better in your Node applications.

Best practices

Before deciding what tool works best for you, following broad logging best practices will help make anything you do log more useful when trying to track down a problem. Read “The Most Important Things to Log in Your Application Software” and the introduction of “JAVA logging – how to do it right” for more details, but in summary, these rules are:

  • Enable logging: This sounds like an obvious rule in an article about logging, but double check you have enabled logging (in whatever tool you use) before deploying your application and don’t solely rely on your infrastructure logging.
  • Categorize your logs: As an application grows in usage, the amount of logs it generates will grow and the ability to filter logs to particular categories or error levels such as authorization, access, or critical can help you drill down into a barrage of information.
  • Logs are not only for you: Your logs are useful sources of information for a variety of stakeholders including support and QA engineers, and new programmers on your team. Keep them readable, understandable and clear as to their purpose.

And now to everyone’s favorite topic, tools.

Start with console

For many developers, debugging a Node application begins with the console and could still be enough if your application is simple. It offers two main logging outputs:

console.log('Keeping you informed'); // outputs to stdout
console.error('An error!'); // outputs to stderr

And that’s about it. For more useful logs, you need a bit more information including a level, a timestamp, and formats suitable for humans and machines. You could implement some of this yourself based around console by passing in string replacements and some less common methods (read more on MDN), but there are simpler ways.

Using console.log can cause performance issues and cause problems when trying to read these logs from a file into log collection tools as it prints the data with line breaks and causes the log collectors to split multi-line logs into separate entries.

Format JSON output with bunyan

Bunyan is a mature and popular option for node logging, currently at version 1.8.x that has a single purpose. It creates well-formatted JSON versions of your logs, which is particularly useful for those of you using external services to read your logs (see more below).

To get started, create a Logger, assigning it a name. From there bunyan has a logging API to tailor messages to your preferred format. For example:

var bunyan = require('bunyan');
var log = bunyan.createLogger({name: 'express-skeleton'});
log.info('Welcome to the app'); // Just text
log.warn({port: bind}, 'port'); // Text plus metadata

Will produce the following output:

node bunyan

Which isn’t so human readable, but you can pipe this to the bunyan CLI tool, creating output suitable for you and any external services that work better with JSON.

Bunyan CLI

HTTP request logging with Morgan

Morgan is another popular package designed for one specific function, logging HTTP requests in the Express framework.
You’ll see it adding output to other screenshots in this article, and if you follow any basic Express getting started steps, you’re using it already.

You use Morgan by defining a format string. For example the code below uses Morgan’s default dev format that adds coloring, but there are others available:

logger = require('morgan');
app.use(logger('dev'));

Which results in the following output.

Morgan

Debugging with debug

For those of you developing applications that you distribute to others (including NPM packages) you’ll want to view logs whilst developing and testing, but hide them from end users. The debug package is a long-standing and popular tool in any Node developers toolkit that you can have present in your application, but toggle its output for when you need it.

Give your application a name, and then start debug, this will wrap console.error into something more usable. For example in this Express-based application:

var debug = require('debug')('express-node-mongo-skeleton:server');
...
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}

Enable it by prefixing your application script command (read the documentation on how to customize this to suit your application), e.g.

DEBUG=* npm start
debug node logging

Log it with Winston

For more complex applications with greater or custom logging needs, Winston enters the fray, and possibly (based on git commits) the oldest option in this article. It allows you to define your own logging levels, custom loggers (and destinations), add metadata, maintain multiple asynchronous logs and more.

For example, to create a custom logger that adds a timestamp, uppercases the logging level and adds any metadata you define in a log message:

// Create custom logger

var logger = new (winston.Logger)({
transports: [
new (winston.transports.Console)({
timestamp: function() {
return Date.now();
},
formatter: function(options) {
return options.timestamp() +' '+ options.level.toUpperCase() +' '+ (options.message ? options.message : '') +
(options.meta && Object.keys(options.meta).length ? 'nt'+ JSON.stringify(options.meta) : '' );
}
})
]
});

And this example shows how to define different message types on the fly and also how to set a log level until you change it again.

function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
logger.info('info', 'Listening on ' + bind);

logger.level = 'debug';
logger.log('debug', 'Now debug messages are written to console!');
}

And for those of you using Express, there’s middleware for Winston.

Gathering intel

A lesser-known option is the intel package, it offers similar features to the other options above, but provides a lot of default logger types, filters, and formatters. One interesting feature is that if an exception is triggered it will include a stack trace as a JSON object, giving you useful information for fixing problems. Also, intel has hierarchical loggers meaning you can pass messages up the logger chain if it meets certain criteria, kind of like levels in a support department.

Logging to external services

With all of these options you will still need to parse, process and understand your logs somehow, and numerous open source and commercial services can help you do this (search NPM and you’ll find lots), I’ll highlight those that support Node well.

The YAL package lets you define remote TCP loggers and send your logs there. It supports log levels, sets timestamps, hostnames, and multiple destinations.

log4js-node is a node version of the venerable log4js framework and offers a comprehensive feature list much like the two options above, and adds an API to provide plugins that can connect to external logging providers.

If you’re a fluentd user, then naturally there’s a Node package that also integrates with log4js and Winston. If Kibana is your platform of choice, then this package for Winston will format your logs to better suit it.

Loggly’s package supports tags, JSON objects, arrays, search, and a variety of options for what to log in the first place.

Airbrake’s package is more suited to error and exception handling and also supports Express and the hapi framework.

And of course, Coralogix’ own package allows you to create different loggers, assign a log level to them and other useful metadata. In addition to all standard logging features, such as flexible log querying, email alerts, centralized live tail, and a fully hosted Kibana, Coralogix provides machine learning powered anomaly detection in the context of Heroku builds.

Another benefit is that Coralogix is the only solution which offers straightforward pricing, all packages include all features.

The example below defines a new logger for information, sets an appropriate level and metadata for a class and method name:

var Coralogix = require("coralogix-logger");

const config = new Coralogix.LoggerConfig({
applicationName: "express-skeleton",
privateKey: ""
subsystemName: "node tester sub"
});

Coralogix.CoralogixLogger.configure(config);

const logger = new Coralogix.CoralogixLogger("Information");

const log = new Coralogix.Log({
severity: Coralogix.Severity.info,
className: "expressInit",
methodName: "onListening",
text: "Listening"
});

logger.addLog(log);

Coralogix also has integration options for Winston and Bunyan keeping you covered no matter what NodeJS option you choose.

With Winston, create configuration the same as above and add Coralogix as a transport.

var winston = require("winston");
var CoralogixWinston = require("coralogix-logger-winston");

var config = {…};

CoralogixWinston.CoralogixTransport.configure(config);

winston.configure({
transports: [new CoralogixWinston.CoralogixTransport({
category: "CATEGORY"
})]
});

winston.info("use winston to send your logs to coralogix");

And for Bunyan, again create your configuration and create a logger that streams to Coralogix as raw data, you can also use Coralogix as a child logger of other loggers.

var bunyan = require("bunyan");
var CoralogixBunyan = require("coralogix-logger-bunyan");

var config = {…};

CoralogixBunyan.CoralogixStream.configure(config);

var logger = bunyan.createLogger({
name: 'BUNYAN_ROOT',
streams: [
{
level: 'info',
stream: new CoralogixBunyan.CoralogixStream({category:"YOUR CATEGORY"}),
type: 'raw'
}
]
});

logger.info('hello bunyan');
coralogix node logging