
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.
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 X and/or subscribe to my RSS feed to stay up to date with the latest tutorials and tech articles.