By Pavel Sheida
Last Validated on May 5 2021 · Originally Published on May 5, 2021 · Viewed 2.9k times

Introduction

Winston is the most popular logging library for Node.js and is designed to be a simple and universal tool, making the logging process more flexible and extensible. It provides a multi-transport asynchronous logging system with rich configuration abilities.

Furthermore, Winston has its extensible log level system and customizable logging targets. Learn more about Winston’s log level system on their GitHub.

In this tutorial, we will explain how to install, setup, and use Winston logger with Morgan middleware for an Express server.


Prerequisites

You will need:

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

Step 1 — Creating And Configuring NPM Package

NPM package is a file or directory that is described by a package.json file. We are going to use one to control the application's dependencies and Node.js behavior.

Let's create an NPM package via npm command:

npm init

In the process, you will be asked to enter some package data. If you want to skip it, just click ENTER several times.

In the tutorial, we are going to use ES6 modules. You can read more about them on the Mozilla pages. To enforce the package to use ES6 modules you have to edit the package.json file. Let's do it with nano:

nano package.json

In the file, you need to add the following line: "type": "module",. We will add it after the "main": "index.js", line.

To exit nano press CTRL+X, then, to save the changes, press Y, then ENTER.

After the insertion, the package.json file should look like:

{
  "name": "your_package_name",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "test": "echo \\"Error: no test specified\\" && exit 1"
  },
  "author": "",
  "license": "ISC",
}

Step 2 — Installing Winston, Morgan And Express

Now, you have to install Winston, Morgan, and Express.

The installation of the libraries is pretty simple. We can do it in one command via npm:

npm install winston morgan express

The process may take a while, be ready to wait up to 5 mins.


Step 3 — Creating Express Server

After the installation, you are ready to create an Express server. Express is a fast, un-opinionated, minimalist back-end web framework for Node.js. You can read more about Express on its officials pages.

The server will have  5 routes and will be run on port 3000. The following code needs to be written to the index.js file.

index.js
// import the express library
import express from "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 request has been handled!");
});

// handle GET request coming on "/development"
app.get("/development", (req, res) => {
  // send a response message
  res.send("You've accessed a debug link!");
});

// handle GET request coming on "/unsafe"
app.get("/unsafe", (req, res) => {
  // send a response message
  res.send("You've accessed an unsafe link!");
});

// handle GET request coming on "/dangerous"
app.get("/dangerous", (req, res) => {
  // send a response message
  res.send("You've accessed a dangerous link!");
});

// handle GET request coming on "/fatal"
app.get("/fatal", (req, res) => {
  // send a response message
  res.send("You've accessed a fatal link!");
});

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

Step 4 — Creating Logger

Now, let's create a Winston logger.

In Winston, every log message has an associated log level. The log level helps you understand the severity and urgency of the message. Each log level has an assigned integer representing the severity of the message.

Due to the rich configuration abilities of Winston, we can create an own log levels system for the application.

Based on the functionality of our server, we've decided to create the following system:s

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

Implementation of a Winston logger with the log levels system should look like:

logger.js
// import the winston library
import winston from "winston";

// log levels system
const levels = {
  fatal: 0,
  error: 1,
  warn: 2,
  info: 3,
  debug: 4,
  http: 5,
};

// create a Winston logger
const logger = winston.createLogger({
    // specify the own log levels system
    levels
  }
});

// export the logger
export default logger;

In Winston, transports are used for specifying logging targets. Each instance of a Winston logger can have multiple transports configured at different levels. In the tutorial, we will cover basic usage of the winston-transports library. You can find additional information in the documentation.

There are 4 built-in core transports in Winston: Console, File, HTTP, Stream. Also, winston-transports library provides a simple way of adding custom transports.

Winston transports default behavior is logging everything with a severity level lower or equal to the specified one. So, if you'd like to log just one level, you should apply a filtering format function.

We will implement this function as follows:

// filter function,
// that will allow logging only the specified log level
const filter = (level) => winston.format((info) => {
  if (info.level === level) {
    return info;
  }
})();

You can read more about filtering info objects in the documentation.

According to the application logic, we need, at least, 4 logging targets:

  • error.log: For storing logs with error and fatal levels.
  • combined.log: For storing combined server logs of different levels.
  • http.log: For storing only HTTP logs.
  • Console: For displaying all logs.

Let's create the Winston transports for the application. Your logger.js file should look like the following:

logger.js
// import the winston library
import winston from "winston";

// filter function,
// that will allow logging only the specified log level
const filter = (level) => winston.format((info) => {
  if (info.level === level) {
    return info;
  }
})();

// log levels system
const levels = {
  fatal: 0,
  error: 1,
  warn: 2,
  info: 3,
  debug: 4,
  http: 5,
};

const transports = [
  // create a logging target for errors and fatals
  new winston.transports.File({
    filename: "error.log",
    level: "error",
    format: winston.format.combine(
      // add a timestamp
      winston.format.timestamp(),
      // use JSON form
      winston.format.json()
    )
  }),
  // create a logging target for logs of different levels
  new winston.transports.File({
    filename: "combined.log",
    level: "info",
    // use simple form
    format: winston.format.simple()
  }),
  // create a logging target for HTTP logs
  new winston.transports.File({
    filename: "http.log",
    level: "http",
    // process only HTTP logs
    format: filter("http"),
  }),
  // create a logging target for debug logs
  new winston.transports.Console({
    level: "debug",
    // specify format for the target
    format: winston.format.combine(
      // process only debug logs
      filter("debug"),
      // colorize the output
      winston.format.colorize(),
      // add a timestamp
      winston.format.timestamp(),
      // use simple form
      winston.format.simple()
    )
  }),
];

// create a Winston logger
const logger = winston.createLogger({
  // specify the own log levels system
  levels,
  // specify the logging targets
  transports
});

// export the logger
export default logger;

At the moment, you have all the logger's code written.


Step 5 — Logging

Now, you can start logging via the created Winston logger. Let's append our code with logging. The index.js file will look as follows:

index.js
// import the express library
import express from "express";

// import the logger
import logger from "./logger.js";

// 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
  logger.info("A root link has been accessed!");
  res.send("Your request has been handled!");
});

// handle GET request coming on "/development"
app.get("/development", (req, res) => {
  logger.debug("A debug link has been accessed!");
  // send a response message
  res.send("You've accessed a debug link!");
});

// handle GET request coming on "/unsafe"
app.get("/unsafe", (req, res) => {
  logger.warn("An unsafe link has been accessed!");
  // send a response message
  res.send("You've accessed an unsafe link!");
});

// handle GET request coming on "/dangerous"
app.get("/dangerous", (req, res) => {
  logger.error("A dangerous link has been accessed!");
  // send a response message
  res.send("You've accessed a dangerous link!");
});

// handle GET request coming on "/fatal"
app.get("/fatal", (req, res) => {
  logger.fatal("A fatal link has been accessed!");
  // send a response message
  res.send("You've accessed a fatal link!");
});

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

Step 6 — Creating And Applying Morgan Middleware

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

Also, Morgan has good compatibility with Winston. It helps you to leverage the power and flexibility of Winston for the requests logs.

The basic example of Morgan usage looks as follows:

// import the morgan library
import morgan from "morgan";

// create a Morgan middleware instance
const morganMiddleware = morgan("combined", {
  // specify a function for skipping requests without errors
  skip: (req, res) => res.statusCode < 400
});

We have specified the function to skip requests with status codes below 400. Because requests with status codes from 400 to 599 contain an error message. You can read more about the HTTP response codes on the Mozilla pages.

The next thing to do is to integrate Morgan with Winston. Everything you need is to specify a writable Winston's stream for Morgan, for example, we will send the request logs to Winston with the "http" log level:

// import the morgan library
import morgan from "morgan";

// import the logger
import logger from "./logger.js";

// create a Morgan middleware instance
const morganMiddleware = morgan("combined", {
  // specify a function for skipping requests without errors
  skip: (req, res) => res.statusCode < 400,
  // specify a stream for requests logging
  stream: {
    write: (msg) => logger.http(msg)
  }
});

After the creating of the middleware, you have to apply it via app.use. At the moment, your index.js file should look like this:

index.js
// import the express library
import express from "express";
// import the morgan library
import morgan from "morgan";

// import the logger
import logger from "./logger.js";

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

// create a Morgan middleware instance
const morganMiddleware = morgan("combined", {
  // specify a function for skipping requests without errors
  skip: (req, res) => res.statusCode < 400,
  // specify a stream for requests logging
  stream: {
    write: (msg) => logger.http(msg)
  }
});

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

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

// handle GET request coming on "/development"
app.get("/development", (req, res) => {
  logger.debug("A debug link has been accessed!");
  // send a response message
  res.send("You've accessed a debug link!");
});

// handle GET request coming on "/unsafe"
app.get("/unsafe", (req, res) => {
  logger.warn("An unsafe link has been accessed!");
  // send a response message
  res.send("You've accessed an unsafe link!");
});

// handle GET request coming on "/dangerous"
app.get("/dangerous", (req, res) => {
  logger.error("A dangerous link has been accessed!");
  // send a response message
  res.send("You've accessed a dangerous link!");
});

// handle GET request coming on "/fatal"
app.get("/fatal", (req, res) => {
  logger.fatal("A fatal link has been accessed!");
  // send a response message
  res.send("You've accessed a fatal link!");
});

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

Step 7 — Testing

Now, it's time to test our code. We will imitate users' requests via cURL — a command-line tool that developers use to transfer data to and from a server.

First things first, start the server via node:

node index.js

The command will occupy your terminal. To continue testing, you have to open a new terminal window.


Step 7.1 — Testing HTTP Logging

The application will log only the requests with an error. Therefore, we will try to access non-existing paths  http://localhost:3000/foo and http://localhost:3000/boo via curl:

curl "<http://localhost:3000/foo>"
curl "<http://localhost:3000/boo>"

Now, let's display the contents of the http.log file. The simplest way to do it is cat. The command prints the contents of the specified file to the console.

cat http.log

The output should be similar to:

Output
{"message":"::1 - - [27/Mar/2021:00:35:49 +0000] \\"GET /foo HTTP/1.1\\" 404 142 \\"-\\" \\"curl/7.71.1\\"\\n","level":"http"}
{"message":"::1 - - [27/Mar/2021:00:35:53 +0000] \\"GET /boo HTTP/1.1\\" 404 142 \\"-\\" \\"curl/7.71.1\\"\\n","level":"http"}

Step 7.2 — Testing Server Logging

There are 5 handled paths in the application. We will try to access each of them via curl:

curl "<http://localhost:3000/>"
curl "<http://localhost:3000/development>"
curl "<http://localhost:3000/unsafe>"
curl "<http://localhost:3000/dangerous>"
curl "<http://localhost:3000/fatal>"

The output should look like:

Output
Your request has been handled!
You've accessed a debug link!
You've accessed an unsafe link!
You've accessed a dangerous link!
You've accessed a fatal link!

Also, if you open the terminal with the server running, you will notice a colorized debug log message. It should be something like this:

Output
debug: A debug link has been accessed! {"timestamp":"2021-03-27T00:48:35.072Z"}

To ensure, that everything works correctly, let's check the log files via cat. First, look at the contents of the error.log file:

cat error.log

The output will look like:

Output
{"message":"A dangerous link has been accessed!","level":"error","timestamp":"2021-03-27T01:11:33.254Z"}
{"message":"A fatal link has been accessed!","level":"fatal","timestamp":"2021-03-27T01:11:37.674Z"}

Second, check the combined.log file:

cat combined.log

The output will look like:

Output
info: A root link has been accessed!
warn: An unsafe link has been accessed!
error: A dangerous link has been accessed!
fatal: A fatal link has been accessed!

Now, you should stop the server by pressing CTRL+C in the server terminal.


Conclusion

Servers, like any application, require timely bug fixes and support. This becomes a serious problem without a good logging system. Besides, by logging HTTP requests, you can analyze users' common problems and provide timely assistance.

In the tutorial, you've configured an Express server with a complex logging system using Winston — a simple and universal Node.js logging library, and Morgan — a fast and flexible HTTP request logger middleware for Node.js.

The server logging won't be a daunting task for you anymore!