Using Winston, a versatile logging library for Node.js

Winston Today, we will explore Winston, a versatile logging library for Node.js. Winston can be used in numerous contexts including in Node web frameworks such as Express, and Node CLI apps. We will also dive into features that make Winston a good fit for IoT applications such as logging timestamped entries to files. This article has been updated to reflect the latest generation of Winston at the time of this writing which is Winston 3.x.

Article contents

Getting started with Winston

Let's first create a new project folder so we can take Winston for a test drive.  I recommend that you choose a directory name such as winston-test rather than winston to ensure that npm does not yield an error and refuse to install a package as a dependency of itself.

Next, create a blank package.json file that automatically accepts all the defaults without prompting you.  (We are, after all, just creating a quick test project.)

$ npm init -y

We are now positioned to install Winston and save it as a dependency in our package.json file:

$ npm install --save winston

Create a file called index.js and add the following contents:

'use strict';
const { createLogger, format, transports } = require('winston');

const logger = createLogger({
  level: 'debug',
  format: format.simple(),
  // You can also comment out the line above and uncomment the line below for JSON format
  // format: format.json(),
  transports: [new transports.Console()]
});

logger.info('Hello world');
logger.debug('Debugging info');

This enables us to log messages to the console by defining a "transport" (in Winston parlance) to specify where we want to output our messages.  We use require to load the Winston module and we can then start logging messages to the console.

Next, run the program you just created from the console:

$ node index.js

You should see the following output:

info: Hello world
debug: Debugging info

Success - you are logging messages to the console!

As noted in the program comments above, we can also change the format of the log output messages and use JSON rather than the simple format.  We'll talk about this more later in the article.

Winston logging levels

As described in greater detail in the documentation, Winston provides different logging levels with associated integer values.  In our example above, we utilized the "info" and "debug" logging levels.  By default, Winston uses logging levels utilized by npm:

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

Logging levels benefit us since we can choose logging level thresholds to determine what logging messages will be displayed.  For example, you might use a different logging level threshold for logging to the console versus logging to a file, or you might choose to temporarily increase the threshold level of logging messages to aid in troubleshooting.

Let's take our previous example and log a message using the silly logging threshold:

'use strict';
const { createLogger, format, transports } = require('winston');

const logger = createLogger({
  level: 'debug',
  format: format.simple(),
  // You can also comment out the line above and uncomment the line below for JSON format
  // format: format.json(),
  transports: [new transports.Console()]
});

logger.info('Hello world');
logger.debug('Debugging info');
logger.silly('Very verbose silly message');

When we invoke this code, we don't see the silly logger level message. ☹️ What's going on here?

We must also change the level property in our createLogger options parameter to increase our logging threshold from debug to silly and enable the silly logging level to be logged to the output. Let's do that now:

'use strict';
const { createLogger, format, transports } = require('winston');

const logger = createLogger({
  // Change the level on the next line from 'debug' to 'silly' to enable messages logged
  // with the silly logging threshold to be logged to the output.
  level: 'silly',
  format: format.simple(),
  // You can also comment out the line above and uncomment the line below for JSON format
  // format: format.json(),
  transports: [new transports.Console()]
});

logger.info('Hello world');
logger.debug('Debugging info');
logger.silly('Very verbose silly message');

Excellent - now we can see that silly logging message! 😺

Logging levels can be very helpful to us. We could, for example, choose to dial back the level property to a different logging threshold such as warn if we only wanted to emit logging messages that are warn or lower (which would include error) to view a smaller subset of logging messages.

Winston provides other types of logging levels such as syslog levels, and you can even create your own custom levels.  We will use the default npm levels in this tutorial, but, rest assured, other options are available if you need them.

Colorize Winston console log output

Why not colorize our console log output to add an additional dimension of fun 🎉to our projects? Here's how it's done:

'use strict';
const { createLogger, format, transports } = require('winston');

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

logger.info('Hello world');
logger.debug('Debugging info');

In this example, we modify the Winston "transport" for the console to add an additional format.colorize() function. To utilize multiple formats, Winston requires that we wrap the format functions inside a format.combine function as shown above. Run this example, and you will see colorized output in your console that varies by the logging level of the message.

Add timestamps to the log entries

Adding a timestamp to each log entry will prove to be very useful for IoT applications—or any application for that matter.  Here's the code needed to bring timestamps to life:

'use strict';
const { createLogger, format, transports } = require('winston');

const logger = createLogger({
  level: 'debug',
  format: format.combine(
    format.colorize(),
    format.timestamp({
      format: 'YYYY-MM-DD HH:mm:ss'
    }),
    format.printf(info => `${info.timestamp} ${info.level}: ${info.message}`)
  ),
  transports: [new transports.Console()]
});

logger.info('Hello world');
logger.debug('Debugging info');

To enable timestamps to appear in the log entry, we change our format from format.simple() to format.printf. We also take it up a notch by specifying a timestamp format to gain precise control over the format of the timestamp.

Log to a file in addition to the console

We now begin to see the power of Winston transports in action as we add a second transport to log to a file in addition to logging to the console:

'use strict';
const { createLogger, format, transports } = require('winston');
const fs = require('fs');
const path = require('path');

const env = process.env.NODE_ENV || 'development';
const logDir = 'log';

// Create the log directory if it does not exist
if (!fs.existsSync(logDir)) {
  fs.mkdirSync(logDir);
}

const filename = path.join(logDir, 'results.log');

const logger = createLogger({
  // change level if in dev environment versus production
  level: env === 'development' ? 'debug' : 'info',
  format: format.combine(
    format.timestamp({
      format: 'YYYY-MM-DD HH:mm:ss'
    }),
    format.printf(info => `${info.timestamp} ${info.level}: ${info.message}`)
  ),
  transports: [
    new transports.Console({
      level: 'info',
      format: format.combine(
        format.colorize(),
        format.printf(
          info => `${info.timestamp} ${info.level}: ${info.message}`
        )
      )
    }),
    new transports.File({ filename })
  ]
});

logger.info('Hello world');
logger.warn('Warning message');
logger.debug('Debugging info');

As shown above, we create a log directory if it does not exist.  We also add the second transport for a file.  Notice also that we can specify different levels (thresholds) for our transports.  In this context, if we are running in a development environment, we use a level of debug and thus send more messages to the log file than we send to the console which is configured with a level of info.

When you run this code, you should see a log file get created before your eyes.  Feel free to experiment with the levels when writing log entries and see how the log output varies between the console and the log file.

Log to console in standard text format and log to file in JSON format

We can also tailor the individual transports to log to the console using standard text format and log to a file using JSON format. The JSON file format might be handy if you had some tools for filtering the data using JSON format, for example.

'use strict';
const { createLogger, format, transports } = require('winston');
const fs = require('fs');
const path = require('path');

const env = process.env.NODE_ENV || 'development';
const logDir = 'log';

// Create the log directory if it does not exist
if (!fs.existsSync(logDir)) {
  fs.mkdirSync(logDir);
}

const filename = path.join(logDir, 'results.log');

const logger = createLogger({
  // change level if in dev environment versus production
  level: env === 'development' ? 'debug' : 'info',
  format: format.combine(
    format.timestamp({
      format: 'YYYY-MM-DD HH:mm:ss'
    }),
    format.json()
  ),
  transports: [
    new transports.Console({
      level: 'info',
      format: format.combine(
        format.colorize(),
        format.printf(
          info => `${info.timestamp} ${info.level}: ${info.message}`
        )
      )
    }),
    new transports.File({ filename })
  ]
});

logger.info('Hello world');
logger.warn('Warning message');
logger.debug('Debugging info');

In this case, we change the global value for log format to format.json() and use format.printf to specify a different format in the transport for the console.

Log to a file that rotates daily

As a final example, we will add an npm module to automatically create a new log file every day.  This same functionality can be accomplished other ways including through the use of the logrotate command in the Linux world; however, we will demonstrate a way to make this happen here in the context of Winston.

We're going to leverage the winston-daily-rotate-file npm module to make this happen. We will first install the winston-daily-rotate-file package from npm using the following command:

$ npm install --save winston-daily-rotate-file

After the npm install is complete, we are ready to implement the code for the daily log file:

'use strict';
const { createLogger, format, transports } = require('winston');
require('winston-daily-rotate-file');
const fs = require('fs');
const path = require('path');

const env = process.env.NODE_ENV || 'development';
const logDir = 'log';

// Create the log directory if it does not exist
if (!fs.existsSync(logDir)) {
  fs.mkdirSync(logDir);
}

const dailyRotateFileTransport = new transports.DailyRotateFile({
  filename: `${logDir}/%DATE%-results.log`,
  datePattern: 'YYYY-MM-DD'
});

const logger = createLogger({
  // change level if in dev environment versus production
  level: env === 'development' ? 'verbose' : 'info',
  format: format.combine(
    format.timestamp({
      format: 'YYYY-MM-DD HH:mm:ss'
    }),
    format.printf(info => `${info.timestamp} ${info.level}: ${info.message}`)
  ),
  transports: [
    new transports.Console({
      level: 'info',
      format: format.combine(
        format.colorize(),
        format.printf(
          info => `${info.timestamp} ${info.level}: ${info.message}`
        )
      )
    }),
    dailyRotateFileTransport
  ]
});

logger.debug('Debugging info');
logger.verbose('Verbose info');
logger.info('Hello world');
logger.warn('Warning message');
logger.error('Error info');

In this code example, we change our file transport to use the winston-daily-rotate-file transport that we installed above. When instantiating the dailyRotateFileTransport we are also able to supply options to control the format and location of our log file. Great stuff!

You will also notice in this example that I added some additional log messages at various log levels and changed the file transport to use a logging level of verbose if the machine is in a development environment. You can experiment with these to solidify your understanding of Winston logging levels and observe how the logging messages appear (or don’t appear) on the console and in the log file.

You will find more Winston usage examples in the examples directory on the Winston GitHub repo.

Bonus - Add custom text to log entries for name of file calling Winston logger

One of my article readers (Bob) asked for help in the comments about how to include the name of the file calling the logger. Let's help Bob get a victory and expand our knowledge of Winston too!

We'll start with logging just to the console and expand to file logging in a minute. First, we'll create a Node module by adding a file named logger.js with the following contents:

'use strict';

const { createLogger, format, transports } = require('winston');
const path = require('path');

const logger = createLogger({
  level: 'debug',
  format: format.combine(
    format.label({ label: path.basename(process.mainModule.filename) }),
    format.colorize(),
    format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
    format.printf(
      info => `${info.timestamp} ${info.level} [${info.label}]: ${info.message}`
    )
  ),
  transports: [new transports.Console()]
});

module.exports = logger;

We have now encapsulated the Winston logger functionality in a Node module that we can call from other files. As part of this code, we also introduce a Winston function we haven't covered called format.label:

    format.label({ label: path.basename(process.mainModule.filename) }),

This label function provides some additional text for Winston to display in the log entry. Inside this function, we include an expression that provides the name of the file calling our logger.js module with the help of process.mainModule.filename. This piece of code has been through a couple of iterations. Thanks to Sree Divya Akula and Filippo for providing comments below to notify me of issues. Hopefully, all is good now!

A couple of lines down, we include ${info.label} in our position of choice to render our custom label contents in every log entry.

    format.printf(
      // We display the label text between square brackets using ${info.label} on the next line
      info => `${info.timestamp} ${info.level} [${info.label}]: ${info.message}`
    )

Let's see if the module works!

Create a file called index.js in the same directory as logger.js with the following contents:

const logger = require('./logger');

logger.info('Hello world');
logger.debug('Debugging info');

Next, invoke index.js.

$ node index.js
2018-12-06 19:55:37 info [index.js]: Hello world
2018-12-06 19:55:37 debug [index.js]: Debugging info

Bam! Our custom text of [index.js], the name of the calling file, is now included with every log entry.

For the sake of completeness, I will also show you an example that logs to both the console and to a file. Replace logger.js with the following contents:

'use strict';

const { createLogger, format, transports } = require('winston');
const fs = require('fs');
const path = require('path');

const env = process.env.NODE_ENV || 'development';
const logDir = 'log';

// Create the log directory if it does not exist
if (!fs.existsSync(logDir)) {
  fs.mkdirSync(logDir);
}

const filename = path.join(logDir, 'results.log');

const logger = createLogger({
  // change level if in dev environment versus production
  level: env === 'production' ? 'info' : 'debug',
  format: format.combine(
    format.label({ label: path.basename(process.mainModule.filename) }),
    format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' })
  ),
  transports: [
    new transports.Console({
      format: format.combine(
        format.colorize(),
        format.printf(
          info =>
            `${info.timestamp} ${info.level} [${info.label}]: ${info.message}`
        )
      )
    }),
    new transports.File({
      filename,
      format: format.combine(
        format.printf(
          info =>
            `${info.timestamp} ${info.level} [${info.label}]: ${info.message}`
        )
      )
    })
  ]
});

module.exports = logger;

I'll point out just a couple of items here. We are able to include the format.label function near the top of the createLogger function and share it among both the Console and File transports. Since the Console transport uses format.colorize, we must declare a separate format.printf function in each Transport so we can omit format.colorize in the File transport and avoid rendering funky ANSI escape codes in logs that are saved to files.

Finally, create a file called index2.js with the following contents:

const logger = require('./logger');

logger.info('Hello world');
logger.debug('Debugging info');

Invoke index2.js....

$ node index2.js
2018-12-06 20:14:21 info [index2.js]: Hello world
2018-12-06 20:14:21 debug [index2.js]: Debugging info

...and lo and behold! We should see output logged to both the console and to a file with today's date—including the name of our calling program (index2.js).

Bob, I hope this answers your question. Thanks for taking the time to ask it as you have ultimately helped all of us deepen our Winston knowledge!

Conclusion

We’ve only scratched the surface of the many features in Winston. I hope this guide has made you a little smarter and equipped you to use Winston for your Node.js file logging endeavors!

Follow @thisDaveJ (Dave Johnson) on Twitter to stay up to date with the latest tutorials and tech articles.

Additional articles

Guide to Installing Node.js on a Raspberry Pi Making Interactive Node.js Console Apps That Listen for Keypress Events How to Watch for Files Changes in Node.js How to Count Unique Items in JavaScript Arrays

Last updated Jun 25 2019

Share

52 thoughts on “Using Winston, a versatile logging library for Node.js

    1. You are very welcome, Alexandros. Thanks for taking the time to provide the positive feedback!

    1. Please try again, Kieran. I updated the tutorial to ensure everything works with Winston 3.x. This should resolve the issue you were experiencing with the log file naming format. Let me know if you continue to have issues.

  1. Thanks for the great tutorial. It is very helpful. Currently, I’m facing with a problem. I’m trying to include the path of the file that the logger is being called. Do you have any idea? Is there any option in Winston that I can use to include the path?

    Thanks,

  2. Thanks, clear concise and up to date! You should include how to use it with express and combine it with Morgan logging, since I feel like that’s a really common use case. But I really like how you gradually introduce features instead of throwing in a lot of features and other npm modules. Super helpful

  3. Seems like there is an error in displaying the file name of the caller function in the logger. Only one file name gets displayed (The first file name say x.js) always irrespective of whichever file calls the logger.

    1. Sree, thanks for pointing out this bug! I updated the code in the “Bonus – Add custom text to log entries for name of file calling Winston logger” section to resolve the issue.

    1. Travis, you can follow the instructions from this article to integrate winston with morgan. As another option, you can use express-winston in lieu of morgan to provide the middleware for the request and error logging of your express.js application.

  4. Thanks Dave. I am able to create a logging utility in 15 mins using your article. I am able log levels – info, error and warning. But I am not able to log debug, verbose and silly levels. I am not getting any exceptions, so could not find the root cause of this issue. Can you help. I am using winston v3.2.1.

    1. Justin, excellent question. You must also change the level property in the createLogger options parameter to increase the logging threshold from “debug” to “silly” and enable the silly logging level to be logged to the output. I added a code example and explanatory text under the Winston logging levels section above because this subtlety is likely to confuse other people too – including me. 🙂

  5. you really write a great article but you can add some other useful things which we can do with Winston like saving log into database or sending emails on errors or send logs to aws

  6. Thanks for this tutorial, it helps me a lot! I was looking how to print the filename of the caller to the log and your example works.
    But I found an issue doing this way: any call to the “createLogger” (1 per file usually) will instantiate a new logger that add an EventEmitter listener, bringing the application into the “MaxListenersExceededWarning”.
    Can you confirm this or it’s just my personal implementation? Thx

    1. Filippo, thanks for taking the time to point out this issue! You are exactly right. I updated the code and I’m expecting the newest version above will no longer cause max listeners exceeded warnings. Let me know if you experience otherwise.

      1. Thanks for Tutorial . It really help me as Node.js beginner to implement logging in the application. But I faced issue here, it prints same file name as a label even logger called from different file. Where I will get the latest code?

        1. Thanks for the feedback. I’m not sure why it does not work for you. I just now tested the code in my tutorial listed under the Bonus section and it works. Be sure you use the Logger.js code listed in the Bonus section and then create a couple .js files that reference the Logger.js (const logger = require(‘./logger’) as described in the tutorial to test. You may want to copy and paste the Logger.js code to make sure you have included the full syntax.

  7. When running `const logger = require(‘./logger’)(__filename);` I got “require(…) is not a function”.
    It seems you need to wrap the exported logger into a function like:

    // logger.js
    const logger = function(callingFile) {
    return createLogger({…});
    }
    module.exports = logger;

    Also, it would be great to have the logger to print stack trace.
    Thanks for the amazing article!

    1. Thanks for pointing out this issue! The use of the __filename parameter was a remnant from a previous version of this tutorial. Change the line from const logger = require(‘./logger’)(__filename); to const logger = require(‘./logger’); and all should be good.

      1. I have multiple js files laid out across dirs in my project where function from /file1.js calls functions in /file2.js which in turn calls functions in /file3.js, in such scenario label with process.mainModule.filename always returns file1.js from where node application was started and thus logs coming out from file2.js and file3.js are labelled with the same name ‘file1.js’ which is not undesired. I would appreciate any suggestions to handle this scenario. By the way, logger.js in this case is in /logger.js (next to file1.js), thus file2.js and file3.js are acquiring the logger using ‘const logger = require(‘../logger’);’ statement.

        1. Project Structure:
          app-root: file1.js, logger.js
          app-root/dir1: file2.js
          app-root/dir3:file3.js

Leave a Reply

Your email address will not be published. Required fields are marked *