By Pavel Sheida
Last Validated on May 4 2021 · Originally Published on May 4, 2021 · Viewed 1k times

Introduction

Logging is an important part of every application life cycle. Having a good logging system becomes a key feature that helps developers, sysadmins, and support teams to understand and solve appearing problems.

Every log message has an associated log level. The log level helps you understand the severity and urgency of the message. Usually, each log level has an assigned integer representing the severity of the message.

Though Node.js provides an extendable logging system with great support for structured output, third-party logging libraries are a lot better for advanced logging practices.

In the tutorial we will cover 3 methods of logging in Node.js:

  • Logging with built-in tools
  • Logging with Pino
  • Logging with Winston

Prerequisites

You will need:

  • Ubuntu 20.04 distribution including the non-root user with sudo access.
  • Node.js and NPM installed.

Option 1 — Logging With Built-in Tools

The original way of logging is the console.log function and its variants. All of them print provided objects to the console. Node.js has 4 default log levels and a function for each of them:

  • Error console.error: Used for serious problems occurred during execution of the program.
  • Warning console.warn: Used for reporting non-critical unusual behavior.
  • Debug console.debug: Used for debugging messages with extended information about application processing.
  • Infoconsole.info, console.log: Used for informative messages highlighting the progress of the application for sysadmins and end users.

Let's write some code to explain how it works. We will create a file, named main.js. The following piece of code will be our playground:

// create an array of fruit names
const fruits = [
  "apple",
  "banana",
  "grapefruit",
  "mango",
  "orange",
  "melon",
  "pear"
];
// create an empty array used as a busket
const basket = [];
// specify the basket max size
const basketMaxSize = 5;

// a function that add an item in the basket
function addToBasket(item)
  // check if the basket isn't full
  if (basket.length < basketMaxSize) {
    // log the action
    console.info(`Putting "${item}" in the basket!`);
    // add the item to the basket
    basket.push(item);
  } else {
    // log an error if the basket is full
    console.error(`Trying to put "${item}" in the full basket!`);
  }
}

// call the addToBasket function on each item of the fruit array
for (const fruit of fruits) {
  addToBasket(fruit);
}

// log the current basket state
console.log("Current basket state:", basket);

At the moment, you can run the code via node.

node main.js

The output should look like:

Output
Putting "apple" in the basket!
Putting "banana" in the basket!
Putting "grapefruit" in the basket!
Putting "mango" in the basket!
Putting "orange" in the basket!
Trying to put "melon" in the full basket!
Trying to put "pear" in the full basket!
Current basket state: [ 'apple', 'banana', 'grapefruit', 'mango', 'orange' ]

Now we have printed the log to the simplest logging target — console. However, frequently you'd like to store the logs in files for further processing. Let's redirect the outF$put to the combined.log file.

node main.js > combined.log

Using > combined.log you redirect the first file descriptor — stdout to the > combined.log file.

You can read more about the file descriptors on the wooledge pages.

Now, you can display the contents of the combined.log file. The simplest way to do it is with the cat command. This command prints the contents of the specified file to the console.

cat combined.log

The output should look like:

Output
Putting "apple" in the basket!
Putting "banana" in the basket!
Putting "grapefruit" in the basket!
Putting "mango" in the basket!
Putting "orange" in the basket!
Trying to put "melon" in the full basket!
Trying to put "pear" in the full basket!
Current basket state: [ 'apple', 'banana', 'grapefruit', 'mango', 'orange' ]

We will redirect stdout to the main.log file, and stderr to the err.log file.

node main.js > main.log 2> err.log

Let's break this code down. Using > main.log you redirect the first file descriptor stdout to the main.log file. Then, using 2> err.log you redirect the second filed descriptor — stderr to the err.log file.

Now you can display the contents of the files. First, display contents of the main.log file.

cat main.log

The output will be like this:

Output
Putting "apple" in the basket!
Putting "banana" in the basket!
Putting "grapefruit" in the basket!
Putting "mango" in the basket!
Putting "orange" in the basket!
Current basket state: [ 'apple', 'banana', 'grapefruit', 'mango', 'orange' ]

Second, display contents of the err.log file.

cat err.log

The output will look like:

Output
Trying to put "melon" in the full basket!
Trying to put "pear" in the full basket!

Option 2 — Logging With Pino

Pino is a very low overhead Node.js JSON logger. It provides a simple, fast, and effective way of logging with 6 default log levels:

  • Fatalfatal: Used for reporting errors that are forcing shutdown of the application.
  • Errorerror: Used for logging serious problems occurring during execution of the program.
  • Warning  — warn: Used for reporting non-critical unusual behaviour.
  • Debugdebug: Used for debugging messages with extended information about application processing.
  • Infoinfo: Used for informative messages highlighting the progress of the application for sysadmins and end users.
  • Tracetrace: Used for "tracing" the code.

Let's explain how to work with Pino on a simple example.


Step 1 — Installing Pino

First things first, you have to install Pino. We will install Pino via npm.

npm install pino

Step 2 — Creating Logger

Pino usage is pretty similar to built-in Node.js logging. Let's write some code to understand the basics of logging with Pino. You have to create a file, named main.js.

Now, in the main.js file, you can create a Pino logger.

// create a pino logger object
const logger = require('pino')()

Step 3 — Logging

Continuing to work with the file, we will write some log functions calls.

// create a pino logger object
const logger = require('pino')()

logger.fatal("fatal message")
logger.error("error message")
logger.warn("warn message")
logger.info("info message")

Now, you can run the script with node.

node main.js

The output should be something like this:

Output
{"level":60,"time":1616494431029,"pid":22817,"hostname":"username","msg":"fatal message"}
{"level":50,"time":1616494431030,"pid":22817,"hostname":"username","msg":"error message"}
{"level":40,"time":1616494431030,"pid":22817,"hostname":"username","msg":"warn message"}
{"level":30,"time":1616494431030,"pid":22817,"hostname":"username","msg":"info message"}

Unlike built-in tools, Pino, by default, provides JSON structured logs with some additional data, such as log level, timestamp, process ID and hostname.

The default logging target in Pino is the console, but you can redirect the output via bash redirections or specify the logging target with a "transport" tool.


Option 3 — Logging With Winston

Winston is a multi-transport async logging library for Node.js with rich configuration abilities.

Winston is the most popular logging library for Node.js. It's designed to be a simple and universal logging library. It makes the logging process more flexible and extensible.

Also, Winston has its own advanced system of log levels. Read more about the Winston’s log level system on their GitHub.


Step 1 — Installing Winston

Let's start with the installation of Winston. The easiest way to do this is using npm.

npm install winston

Step 2 — Creating Logger

First, you have to create a file, we will name it main.js.

Now, let's start working with Winston. The recommended way to use Winston is by creating a logger using winston.createLogger. Winston is well configurable, but we will pass only one parameter into the winston.createLogger function - transports. This parameter specifies a set of logging targets.

In the example we will specify 2 file targets: combined.log for all non-suppressed logs and err.log for all logs with level "error" and below.

// import winston library
const winston = require('winston');

const logger = winston.createLogger({
  transports: [
    //Write all logs with level `error` and below to `error.log`
    new winston.transports.File({ filename: 'err.log', level: 'error' }),
    // Write all logs with level `info` and below to `combined.log`
    new winston.transports.File({ filename: 'combined.log' }),
  ],
});

Step 3 — Logging

Using the logger, we can start to log some data. Let's append the file with the following commands:

logger.error("error message");
logger.warn("warn message");
logger.info("info message");
logger.info("info message2");

Now, you have to run the script using node.

node main.js

After the script is executed, you are able to display the contents of the logging target files. First, display the contents of the combined.log file.

cat combined.log

The output will be like this:

Output
{"message":"error message","level":"error"}
{"message":"warn message","level":"warn"}
{"message":"info message","level":"info"}
{"message":"info message2","level":"info"}

Second, display the contents of the err.log file.

cat err.log

The output will look like:

Output
{"message":"error message","level":"error"}

Step 4 — Configuring HTTP Logger With Morgan

Morgan is an HTTP request logger middleware for Node.js. It greatly simplifies the process of request logging by generating logs in a specified format. You can think of Morgan as a helper that generates logs requests.

Also, it has good compatibility with Winston giving you the power and flexibility of Winston for Morgan generated specifically formatted HTTP log requests.


Step 4.1 — Installing Morgan And Express

For the example, we will be using Morgan with an Express server.

So, the first thing you have to do is to install Morgan and Express. This can be done with npm:

npm install express morgan 

Step 4.2 — Creating Express Server

After the installation, you are ready to create an Express server. We will write the following code in the main.js file.

// import the express library
const express = require("express");

// create an express application instance
const app = express();
// create a variable to store the port
const port = 3000;

// handle GET request coming on "/"
app.get("/", (req, res) => {
	// send a response message
  res.send("Your reuest has been handled!");
})

// start the server on the specified port
app.listen(port);

Step 4.3 — Creating Logger

Continuing working in the file, let's create a Winston logger with our custom log levels system and the console as the only logging target.

// import the express library
const express = require("express");
// import the winston library
const winston = require("winston");

// create an express application instance
const app = express();
// create a variable to store the port
const port = 3000;

// create a logger
const logger = winston.createLogger({
  // specify own log levels system
  levels: {
    error: 0,
    warn: 1,
    info: 2,
    http: 3,
    debug: 4,
  },
  // an array of loggig targets
  transports: [
    // set the console as a logging target
    // for http and lower log levels
    new winston.transports.Console({level: "http"}),
  ]
});

// handle GET request coming on "/"
app.get("/", (req, res) => {
	// send a response message
  res.send("Your reuest has been handled!");
})

// start the server on the specified port
app.listen(port);

Step 4.4 — Creating And Applying Morgan Middleware

The final step — create a Morgan middleware. It's pretty simple to integrate Morgan with Winston. Everything you need is to specify a writable Winston's stream for Morgan. We will send the request logs to Winston with the "http" log level.

// import the express library
const express = require("express");
// import the morgan library
const morgan = require("morgan");
// import the winston library
const winston = require("winston");

// create an express application instance
const app = express();
// create a variable to store the port
const port = 3000;

// create a logger
const logger = winston.createLogger({
  // specify own log levels system
  levels: {
    error: 0,
    warn: 1,
    info: 2,
    http: 3,
    debug: 4,
  },
  // an array of loggig targets
  transports: [
    // set the console as a logging target
    // for http and lower log levels
    new winston.transports.Console({level: "http"}),
  ]
});

// create a morgan middleware instance
const morganMiddleware = morgan({
  // specify a stream for requests logging
  stream: {
    write: msg => logger.http(msg)
  }
});

// apply the morgan middleware
app.use(morganMiddleware);

// handle GET request coming on "/"
app.get("/", (req, res) => {
	// send a response message
  res.send("Your reuest has been handled!");
})

// start the server on the specified port
app.listen(port);

Conclusion

In the tutorial, you've comprehended the built-in logging system — the simplest way of logging with the basic functionality. It can be a good choice for little projects or demo examples.

You've also configured 2 loggers using the third-party logging libraries:

  • Pino — the fastest JSON logger with a command line interface similar to Bunyan. It decouples transports, and has a sane default configuration.
  • Winston — the most popular logging library for Node.js. It has a large and robust feature set, and is very easy to install, configure, and use.

Now developing and maintaining your Node.js applications will be much easier!