Add Server Logs to your Node.js App with Morgan and Winston

By Luke Pate on May. 6th, 2020

By building a log to store exceptions and errors, we have a better chance of understanding bugs within our code as the users experience them. In this guest blog post by Luke Pate, learn how to write meaningful logs that explain your application and its activity to support future work by creating a simple Node API to capture requests and write exceptions to a logging file to be stored and viewed at a later time.

Why do we need logging?

Any good application with active users needs to be maintained. Part of that maintenance phase consists of analyzing your application's activity for potential issues or bugs. Effective logging, in some cases, may become the last line of defense when debugging a critical production issue. Ask anyone who's ever had to debug any serious production bug without efficient logging and they'll most likely have some war story that could have been prevented by effective error logging.

In this tutorial, we'll be building a log to support your future work. We'll create a simple Node API that captures its requests and writes exceptions to a logging file to be stored and viewed at a later time.

What info should we log?

Before we start logging we should define what we want to log. Obviously, we don't want to observe every piece of activity in our application, but by storing the exceptions and errors, we might have a better chance of understanding bugs within our code as the users experience them. Best practice is to write meaningful logs that explain your application and its activity. Cryptic logs are often just as bad as having no logs at all.

As for what exactly to put in the logs, that's more particular to your application and its individual components. Generally speaking, these types of things are safe to log.

  • User IDs
  • Timestamps
  • Log level/severity
  • Error messages
  • Status codes
  • Methods

How are we going to log?

This might be the hardest part to logging: picking an appropriate logger. There's no shortage of logging options out there in the JavaScript ecosystem and most are going to get the job done. My suggestion would be to start with one that has a particular feature you know you need and decent documentation. Today we'll be using Morgan and Winston; both have great documentation.

Morgan

If your application communicates via HTTP, you'll want some kind of middleware to monitor incoming requests. Morgan is a request logger that does just that. It's easy to install and has a lot of customizing and formatting benefits. We'll be using Morgan to communicate our web traffic requests to our application in a reasonable format.

Winston

Once we've captured our requests, we'll want some kind of way to persist that data to view at a later time. For this example we'll be using Winston. Winston is a powerful library that makes logging to file simple. It supports multiple transports, child loggers, custom levels and much more. Winston at its core is designed to be simple to use, so we'll be using it to catch our errors and write them to file.

Step 1 :  Setup

To start off, we'll clone this starter repo containing a simple Express API. (What Is the Express.js Node.js Framework?)

Clone this repo if you want to follow along with the tutorial.

Make sure to install all of the project’s dependencies:

npm install 

Step 2 :  Install Winston

Now that our app is up and running, let’s install Winston.

npm install winston

We'll go ahead and create two folders: one for our Winston code and one for our log outputs.

mkdir ./logs ./config 

Create a file for our Winston logger configuration. This is to simply to organize our loggers and keep them in their own directory.

touch ./config/winston.js

We'll start by creating a new logger with the winston.createLogger method. CreateLogger accepts a few parameters such as 'format' and 'silent', but for now we're going to pass 'level', 'exitOnError and 'transports' to create a basic logger implementation. You can see in our transports that we're defining a filename and path to send our logging.

const winston = require('winston');
// creates a new Winston Logger
const logger = new winston.createLogger({
  level: 'info' 
  transports: [
    new winston.transports.File({ filename: './logs/error.log', level: 'error' }),
  ],
  exitOnError: false
});
module.exports = logger;

Step 3:   Add logs

Now that we have our logger installed, we'll need a way to use it. In our App.js file we’re going to bring in Morgan for capturing HTTP requests. We'll use Morgan’s standard "combined" format and pass the stream to our Winston logger.

In our app.get we're going to intentionally have the route throw an exception to stream that error to our Winston logger. You can see we're using the error method and sending a custom string of some of the request’s data. This is the information that we're going to see on our log files, so feel free to customize it to your app's needs.

Your app.js file should look like this:

const express = require('express');
const app = express();
const morgan = require('morgan');
const logger = require('./config/winston');
const port = 8080
app.use(morgan("combined", { stream: logger.stream.write }));
app.get('/', function(req, res) {
    throw new Error('error thrown navigating to');
});
app.use(function(err, req, res, next) {
  logger.error(`${req.method} - ${err.message}  - ${req.originalUrl} - ${req.ip}`);
  next(err)
})  
app.listen(port, console.log(`Listening on port ${port}!`));

Now we're ready to start our server and see our logs in action. In one terminal window, start the application with:

node app.js 

Then, open a new terminal window and tail the error file. This will allow you to see new logs being written to our file. Tail outputs the content of a file as lines are added to it.

Run the following command in the second terminal window:

tail -f ./logs/error.log

Now if we navigate our browser to http://localhost:8080 we can see errors as they are written to /logs/error.log. After you load the page, you should see something like this:

{"message":"GET - / - error thrown navigating to - ::1","level":"error"}

Now we have a simple API logging exceptions with Winston as they are thrown. I highly recommend jumping into the Winston docs and learning more about its capabilities. There are many more logging features to explore, but this is a good starting point. Good luck!

Additional resources

Previous post:

The modular system in Node.js allows developers to keep their code organized and clean, and promotes reusability of code and adoption of best practices across Node.js applications. In this guest blog post by @sunnyenotick, learn about the different types of modules, and how to use different modules in your own Node project.

By Anna Mykhailova on May. 6th, 2020
Next post:

The Nodejs.org community is redesigning the Nodejs.org website. The current website hasn’t had a major overhaul since 2014. Node.js, its place in the industry, and the community around it have grown quite a bit in that time. This project relies on volunteers to bring it to life, and you can get involved to help provide a better home for the Node.js community.

By Jon Church on Jul. 22nd, 2020