Node.js logging libraries

Node.js logging libraries

Sprkl
Sprkl 17 Min Read

Sooner than later, you’ll have to record and keep track of the events that occur when a piece of your code runs. Node.js devs use logging libraries as crucial components to building robust applications that serve thousands of users simultaneously. So we decided to write this article and tell you everything we know and think you need to know about Node.js logging libraries.

We’ll start with the critical question of how much data and what data you should log, move on to the five log levels, and then list the most common libraries available. We at Sprkl use Winston, but it doesn’t mean we prefer it over another library or recommend it. Each library has different features and should meet your unique needs.

Before we start: Quick overview on logging and log files

Logging is the act of recording and keeping track of the events that occur when a piece of software runs. Although developers only deploy applications after undergoing rigorous testing, something could break in production. For this reason, developers need efficient ways of detecting and resolving bugs in Node.js applications. There is no better way to do this than to maintain a standard logging system using a Node.js logging library of their choice.

Log files contain essential information such as user actions, timestamps, and events. From a security point of view, logging information can also be of use in detecting a security breach that may impact your application. With proper application monitoring and analytics tools in place, logging could be an important source of data points that we can leverage to improve application performance and user experience. 

Let’s start with how much and what data should you Log

Node.js logging libraries make it easy to store and manage logs; therefore, it is tempting to try and log every bit of your application. However, excessive logging may also impact the performance of your application. A high frequency in logging and very complicated messages could consume more CPU cycles affecting your application performance. Too much log information could also mean extra storage costs. If not well managed, too much log data could challenge developers who try to understand them. Therefore, organizations need to strike a balance between logging relevant information for observability and other reasons and also cause less impact on performance.

Some developers believe that you should log as much information as possible. While this may be appropriate in certain scenarios, it is essential to remember that you should never log sensitive data. Sensitive data includes passwords, addresses, email addresses, social security numbers, and financial information such as credit card numbers.

Let’s continue with the Node.js log five levels

Using appropriate and meaningful log levels is also a critical practice when logging using Node.js logging libraries. Log levels are important information that allows developers to distinguish different log events. Log levels also indicate the urgency of a log event allowing developers to sort out and attend to the events that need immediate attention. Here are the most common log levels that are available across different Node.js logging libraries:

  • Error 
  • Warning
  • Info
  • Verbose
  • Debug

Node.js Logging libraries

Console.log()

Let’s start with the console.log() method, widely used to perform basic debugging in Node.js applications. Like the console.log method on the browser side, this method prints out a message to the stdout. Other methods provided by the console module for logging purposes in Node.js include:

console.error(), console.log(), console.warn() and console.trace() methods.

In the example below, we use the console.info() method, which prints to the process.stdout, and console.error() which prints to the process.stderr to create logs tracking how items are added to an empty array.

const companies = ['Tesla','BMW','Nissan','Chevrolet','Ford','Volkswagen','Kia'];

const electricCarCompanies = [];

function addToCarCompany (company) {
  if (electricCarCompanies.length < 4) {
    console.info(`Adding "${company}" to electric car companies`);
    electricCarCompanies.push(company);
  } else {
    console.error(`List of Top EV Companies full !`);

  }
}

for (const company of companies) {
  addToCarCompany(company);
}

We can run the command below to direct log messages in the stdout and stderr to a single log for observability purposes.

node cars.js > main.log 2>&1
Adding "Tesla" to electric car companies
Adding "BMW" to electric car companies
Adding "Nissan" to electric car companies
Adding "Chevrolet" to electric car companies
node : List of Top EV Companies full !
At line:1 char:1
+ node cars.js > main.log 2>&1
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (List of Top EV Companies full !:String) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError
 
List of Top EV Companies full !
List of Top EV Companies full !

While these methods could be useful for basic debugging and logging, they do not match the Node.js logging libraries. In addition, console methods lack features and configuration options that support applications in a production environment, such as timestamps, log levels, and structured logging. For example, although console methods print messages to the stderr and stdout, they do not indicate the log severity levels.

Node.js logging libraries are the best option for managing logs for an application in production. They provide more flexibility and advanced features such as sending logs over HTTP and output logs to multiple destinations. There are a ton of Node.js libraries that provide you with these features out of the box. However, to choose the proper library, make sure that the library meets all you need in terms of log recording, formatting, and storage.

We will explore some of the most used Node.js logging libraries for the rest of this article. However, this does not mean that your choices should be limited to the libraries below. Instead, we suggest doing your research and choosing one that most appeals to your needs.

Winston

As we mentioned before – we use Winston and it’s one of the most popular Node.js logging libraries, with an average of 8,066,552 weekly downloads when writing this article. Its popularity is due to its ability to support multiple storage options(transports) that can be configured to different logging levels. Winston is also highly extensible, flexible yet simple, and performant, allowing users to create custom log levels and formats. 

Use npm and install Winston:

npm install winston 

To use Winston, you need to create your logger, and the recommended way to do that is by using the winton.createLogger() function. The logger function accepts the following parameters: log level, format, transports, etc. In the logger below, all the logs with the log severity of the error and less are written to a file named error.log while logs with a log level of info and less are written to the file named combine.log. The logs are formatted in JSON.

const winston = require('winston');
 
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  defaultMeta: { service: 'user-service' },
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' }),
  ],
});
{"level":"error","message":"This is the first error!","service":"user-service"}
{"level":"error","message":"This is the second the second error!","service":"user-service"}
{"level":"info","message":"Here is some info some info!","service":"user-service"}

If you’re building an express js application, the express-winston middleware allows you to perform logging for your requests and errors. Below is an example of how to use this middleware, but first, we need to install the middleware as shown here.

npm install winston express-winston
const winston = require('winston');
const expressWinston = require('express-winston');
const express = require('express');
const port = 8080;
const app = express();
 
app.use(expressWinston.logger({
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' }),
  ],
  format: winston.format.combine(
    winston.format.json()
  ),
  meta: false,
  msg: "HTTP  ",
  expressFormat: true,
  ignoreRoute: function (req, res) { return false; }
}));
 
app.get('/', (req, res) => {
  res.send(' Winston has been logged');
});
 
 
app.get('/newpage', (req, res) => {
  res.json({'message': 'Hello Developer!'});
});
 
app.listen(port, () => {

In this case we are creating logs on HTTP requests to the two endpoints ‘/’ and ‘/newpage’. In this case the transport instance is a file named ‘combined.log’ that will store logs with log level of info and less.

{"level":"info","message":"GET / 304 6ms","meta":{}}
{"level":"info","message":"GET /newpage 304 1ms","meta":{}}
{"level":"info","message":"GET /newpage 304 2ms","meta":{}}
{"level":"info","message":"GET /newpage 304 1ms","meta":{}}
{"level":"info","message":"GET /newpage 304 1ms","meta":{}}

I like using Winston in all my Express projects because, besides the standard features such as log levels, formatting, and the ability to configure different transports(output options). We use Winston together with Morgan. Using Morgan and Winston gives me the flexibility to work with multiple transports and, therefore channel logs from Morgan through multiple transports. Morgan simplifies the process of logging HTTP requests and errors in my Express applications since it has access to the request and response life cycle. 

Above all, another thing about using Winston as the primary Node.js Logging library alongside Morgan is the ability to log a request even when the server crashes by writing the log line on request instead of the response. Finally, another thing that informs my decision to stick to Winston and Morgan is that even when I may decide to use custom HTTP headers, I can still be able to log.

Bunyan

Bunyan is also another popular and fast JSON Node.js logging library. Just like Winston, it also supports logging into multiple transport options. Other features include a neat-printing CLI for logs, a log filter, serializers for rendering objects, snooping system, and the ability to support multiple runtime environments such as NW.js and WebPack. Bunyan enforces the JSON format for logs.

Install Bunyan using npm.

npm install bunyan

Once installed, you can now proceed to create a logger instance before calling methods corresponding to each log severity.

const bunyan = require('bunyan');
 
const log = bunyan.createLogger({
    name: 'myapp',
    streams: [
      {
        level: 'info',
        stream: process.stdout            
      },
      {
        level: 'error',
        path: './myapp-error.log'  
      }
    ]
  });
 
log.info("This is log of level info");
log.error("This is a log of error level");

Logs with the log severity of the error and less you can write to a file named myapp-error.log, while logs with a log level of info and less you can write to the process.stdout. The logs are formatted in JSON.

{"name":"myapp","hostname":"DESKTOP-T8C4TF3","pid":19164,"level":30,"msg":"This is log of level info","time":"2022-05-28T19:38:46.486Z","v":0}
{"name":"myapp","hostname":"DESKTOP-T8C4TF3","pid":19164,"level":50,"msg":"This is a log of 
error level","time":"2022-05-28T19:38:46.487Z","v":0}

If you’re using Express the express-bunyan-logger is a middleware powered by bunyan that you can enable to perform logging for your Express applications.

const express = require('express');
const app = express();
const port = 8080;
 
app.use(require('express-bunyan-logger')({
  name: 'logger',
  format: ":remote-address - :user-agent[major] custom logger",
  streams: [{
      level: 'info',
      stream: process.stdout
  },
  {
    level: 'error',
    path: './newapp-error.log'  
  }
 
]
}));
 
app.get('/', (req, res) => {
  res.send('Express application has been logged !');
});
 
 
app.get('/newpage/', (req, res) => {
  res.json({'message': 'Hello Developer!'});
});
 
app.listen(port, () => {
  console.log(`The application is listening at http://localhost:${port}`);
});

When we execute the code above and hit the end point localhost:8080,  logs with severity level of error or less and those with the severity level of info or less are written to newapp-error.log and the console respectively.In both cases the logs are formatted in JSON.

{"name":"logger","hostname":"DESKTOP-T8C4TF3","pid":12652,"req_id":"3463bc4b-1148-40d3-ac6f-b585fc1195c8","level":30,"remote-address":"::1","ip":"::1","method":"GET","url":"/","referer":"-","user-agent":{"family":"Chrome","major":"101","minor":"0","patch":"4951","device":{"family":"Other","major":"0","minor":"0","patch":"0"},"os":{"family":"Windows","major":"10","minor":"0","patch":"0"}},"http-version":"1.1","response-time":352.2876,"response-hrtime":[0,352287600],"status-code":304,"req-headers":{"host":"localhost:8080","connection":"keep-alive","sec-ch-ua":"\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"101\", \"Microsoft Edge\";v=\"101\"","sec-ch-ua-mobile":"?0","sec-ch-ua-platform":"\"Windows\"","upgrade-insecure-requests":"1","user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) 
Chrome/101.0.4951.64 Safari/537.36 Edg/101.0.1210.53","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9","sec-fetch-site":"none","sec-fetch-mode":"navigate","sec-fetch-user":"?1","sec-fetch-dest":"document","accept-encoding":"gzip, deflate, br","accept-language":"en-US,en;q=0.9","if-none-match":"W/\"25-Lm5uBw7kjYvopFexrPuENa1zO+0\""},"res-headers":{"x-powered-by":"Express","etag":"W/\"25-Lm5uBw7kjYvopFexrPuENa1zO+0\""},"req":{"method":"GET","url":"/","headers":{"host":"localhost:8080","connection":"keep-alive","sec-ch-ua":"\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"101\", \"Microsoft Edge\";v=\"101\"","sec-ch-ua-mobile":"?0","sec-ch-ua-platform":"\"Windows\"","upgrade-insecure-requests":"1","user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36 Edg/101.0.1210.53","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9","sec-fetch-site":"none","sec-fetch-mode":"navigate","sec-fetch-user":"?1","sec-fetch-dest":"document","accept-encoding":"gzip, deflate, br","accept-language":"en-US,en;q=0.9","if-none-match":"W/\"25-Lm5uBw7kjYvopFexrPuENa1zO+0\""},"remoteAddress":"::1","remotePort":62558},"res":{"statusCode":304,"header":"HTTP/1.1 304 Not Modified\r\nX-Powered-By: Express\r\nETag: W/\"25-Lm5uBw7kjYvopFexrPuENa1zO+0\"\r\nDate: Sat, 28 May 2022 20:03:28 GMT\r\nConnection: keep-alive\r\nKeep-Alive: timeout=5\r\n\r\n"},"incoming":"<--","msg":"::1 - 101 custom logger","time":"2022-05-28T20:03:29.021Z","v":0}

Pino

Pino is also a popular Node.js logging libraries alternative for developers. There are claims that it is 5x faster than other Node.js logging libraries besides providing standard features such as the ability to choose a storage medium,  log levels, and formatting capabilities. Pino is also highly extensible, flexible, and easy to integrate with Node.js frameworks such as Express, Fastify, and Restify. The library is a low-overhead Node.js logging library and supports asynchronous logging despite being fast. Pino also gives you the ability to create child loggers. To install Pino, use the command below.

npm install pino
const pino = require('pino');
 
const logger = pino({
    level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
    });
 
logger.info('Hello, Developer!');
const child = logger.child({ a: 'property' })
child.info('Hello Developer!')
{"level":30,"time":1653773121769,"pid":8964,"hostname":"DESKTOP-T8C4TF3","msg":"Hello, Developer!"}
{"level":30,"time":1653773121770,"pid":8964,"hostname":"DESKTOP-T8C4TF3","a":"property","msg":"Hello Developer!"}

Alternatively, you could create this logging instance in a separate file, export it and use it throughout your project. As aforementioned, Pino is also easy to configure Node.js logging library especially when working with other frameworks. For example, using the express-pino logger, you can also log your express application. To install the middleware, we use this command.

npm install express-pino-logger --save

Here is a simple Express web server with two endpoints illustrating how we can use the express-pino-logger middleware.Since Pino logs are very verbose you can also use the pino-pretty module to format the logs. To install the module use the command shown below.

npm install -g pino-pretty
'use strict'
 
const express = require('express');
const app = express();
const pino = require('express-pino-logger')();
const PORT = process.env.PORT || 8080;
 
app.use(pino)
 
app.get('/', (req, res) => {
  res.send('Your express application has been logged');
});
 
app.get('/newpage/', (req, res) => {
  req.log.info();
  res.json({'message': 'Hello Developer!'});
});
 
app.listen(PORT, () => {
  console.log(`The application is listening at http://localhost:${PORT}`);
});
{"level":30,"time":1655129313841,"pid":1944,"hostname":"DESKTOP-T8C4TF3","req":{"id":1,"method":"GET","url":"/","query":{},"params":{},"headers":{"host":"localhost:8080","connection":"keep-alive","sec-ch-ua":"\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"102\", \"Microsoft Edge\";v=\"102\"","sec-ch-ua-mobile":"?0","sec-ch-ua-platform":"\"Windows\"","upgrade-insecure-requests":"1","user-agent":"Mozilla/5.0 (Windows 
NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36 Edg/102.0.1245.39","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9","sec-fetch-site":"none","sec-fetch-mode":"navigate","sec-fetch-user":"?1","sec-fetch-dest":"document","accept-encoding":"gzip, deflate, br","accept-language":"en-US,en;q=0.9","if-none-match":"W/\"25-Lm5uBw7kjYvopFexrPuENa1zO+0\""},"remoteAddress":"::1","remotePort":55374},"res":{"statusCode":200,"headers":{"x-powered-by":"Express","content-type":"text/html; charset=utf-8","content-length":"40","etag":"W/\"28-Js4EGYhbh8u2ztGVpUeS1sSHyC8\""}},"responseTime":16,"msg":"request 
completed"}

LogLevel

A relatively popular, simple, “minimal and lightweight” Node.js  logging library that works both in the browser and for Node.js. According to its documentation, the LogLevel Node.js logging libraries seek to replace the console methods with more features such as the “ability to disable error logging in production and filter logs by their severity”. LogLevel is a Node.js logging library that  has no dependencies and can continue logging even in browsers that don’t have support for the console object. Like other Node.js logging libraries, LogLevel is also extensible and offers log redirection capabilities, formatting, and filtering. For developers that develop in TypeScript, LogLevel already has included type definitions making it even more convenient to use.

Below is an example of using the LogLevel logging library under the CommonJS module specification, particularly on the server-side with Node.js. If you’re using npm, you can install LogLevel using the command below.

npm install loglevel
const log = require('loglevel');
log.warn("This is a warning !");
log.error("This is an error !");

Executing the code above with node index.js should return the following output in the console.

This is a warning !
This is an error 

You can also use the LogLevel Node js logging library for logging with other Node js frameworks, for example in express js without any middleware. Here a simple web server with a single end point.

const express = require('express');
const app = express();
const logger = require('loglevel');
const port = 8080;
 
 
app.get('/', (req, res) => {
  logger.warn('This is a warning');
  res.json({'message': 'Hello Developer !'});
  logger.error('This is an error');
 
});
 
app.listen(port, () => {
  console.log(`The application is listening at http://localhost:${port}`);
});

Log4js

Log4js is another popular Node.js logging library with over three million weekly downloads. Although it has a similar name as the popular Log4j Java logger, its functionalities are not the same. Log4js offers some interesting features out of the box. For example, appenders that support printing log messages to different destinations. Some of the appenders available in Log4js include an SMTP appender, GELF appender, Loggly appender, Logstash UDP appender, Http appender, and a file appender. The Express logger also enables developers to handle logging in to Express servers if you’re using express js. Other features include colored logging, log levels, configuring and formatting log messages, etc.

Install Lo4js by using npm by running the command below.

npm install log4js
const log4js = require("log4js");
log4js.configure({
  appenders: { combined: { type: "file", filename: "combined.log" } },
  categories: { default: { appenders: ["combined"], level: "error" } }
});
 
const logger = log4js.getLogger("combined");
 
logger.info("Hello Developer !");
logger.error("This an error!");
logger.fatal("This is another error !");

Executing the above code using node index.js, you’d realize that only logs with the log severity of error and less are written to a file named combined.log.

[2022-05-29T10:51:41.535] [ERROR] combined - This an error!
[2022-05-29T10:51:41.537] [FATAL] combined - This is another error !

We can also use the express logger to log in Express.js. For example, here is a simple server with a single endpoint using the express logger.

const log4js = require('log4js');
const express = require('express');
const app = express();
const PORT = process.env.PORT || 8080;
 
log4js.configure({
 appenders: {
   console: { type: 'console' },
   file: { type: 'file', filename: 'combined.log' }
 },
 categories: {
   combined: { appenders: ['file'], level: 'info' },
   default: { appenders: ['console'], level: 'info' }
 }
});
 
const logger = log4js.getLogger('combined');
 
app.use(log4js.connectLogger(logger, { level: 'info' }));
app.get('/hello',(req,res) => {
  res.send('Hello Developer !');
});
 
app.listen(PORT, () => {
    console.log(`The application is listening at http://localhost:${PORT}`);
  });

Executing the above code using node index.js, you’d realize that logs with the log severity of info and less will be written to a file named.

[2022-05-29T11:04:20.609] [INFO] combined - ::1 - - "GET / HTTP/1.1" 404 139 "" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36 Edg/101.0.1210.53"

Conclusion

We hope that some of you found this article insightful. Again we’d like to emphasize that this article covered some of the most popular Node.js/Express logging libraries based on our own experience, the number of weekly downloads and GitHub stars. And there are several other Node.js logging libraries available for similar purposes. It’s up to you to choose the library that best serves your needs as a developer or organization while adhering to the best logging practices.

If you have questions about which library to choose, feel free to connect with me: Here.

Share

Share on facebook
Share on twitter
Share on linkedin

Enjoy your reading 17 Min Read

Further Reading