Is Your Node.js App Missing the Magic of Morgan for Logging?

Mastering Web Application Logging with Morgan in Node.js and Express

Is Your Node.js App Missing the Magic of Morgan for Logging?

Building a web application using Node.js and Express involves a lot of moving parts. One of the critical aspects often overlooked is logging HTTP requests. Proper logging is crucial for debugging, monitoring, and understanding how users interact with your application. Enter Morgan, a popular middleware designed for logging HTTP requests efficiently.

To start using Morgan, you first need to install it. This can be done via npm with a single command:

npm install morgan

With Morgan installed, integrating it into an Express application is straightforward. Let’s take a simple example to get the ball rolling:

const express = require('express');
const morgan = require('morgan');
const app = express();
const port = 3000;

app.use(morgan('combined'));

app.get('/', (req, res) => {
    res.send('Hello, World!');
});

app.listen(port, () => {
    console.log(`Server listening at http://localhost:${port}`);
});

In this snippet, morgan('combined') is used as middleware to log incoming requests in the Apache combined format. When a request hits the root URL (/), Morgan logs the request details to the console.

Morgan offers several predefined logging formats to suit different needs. Here are some of the popular ones:

  • combined: This is the Apache combined log format, containing details like the client’s IP, request method, URL, HTTP version, status code, and more.

    app.use(morgan('combined'));
    

    Example output:

    ::1 - - [26/Apr/2020:16:58:09 +0000] "GET / HTTP/1.1" 200 13 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.122 Safari/537.36"
    
  • common: Similar to the combined format but without the user agent and referrer.

    app.use(morgan('common'));
    

    Example output:

    ::1 - - [26/Apr/2020:16:58:09 +0000] "GET / HTTP/1.1" 200 13
    
  • dev: A more concise format with method, URL, status code, response time, and content length.

    app.use(morgan('dev'));
    

    Example output:

    GET / 200 13.066 ms - 13
    
  • tiny: A minimal format with method, URL, status code, and response time.

    app.use(morgan('tiny'));
    

    Example output:

    GET / 200 13ms - 13
    
  • short: A shorter version of the combined format, excluding the user agent and referrer.

    app.use(morgan('short'));
    

    Example output:

    ::1 - GET / HTTP/1.1 200 13 - 13ms
    

Predefined formats are super convenient, but sometimes you might need more customized logging. That’s where Morgan’s flexibility shines. You can create custom logging formats using tokens or even custom functions.

Here’s an example of using predefined tokens to create a custom format:

app.use(morgan(':method :url :status :res[content-length] - :response-time ms'));

This will log the request method, URL, status code, response content length, and response time.

For more complex needs, you can define a custom function to generate the log entry:

const customFormat = (tokens, req, res) => {
    return [
        tokens.method(req, res),
        tokens.url(req, res),
        tokens.status(req, res),
        tokens.res(req, res, 'content-length'),
        '-',
        tokens['response-time'](req, res),
        'ms'
    ].join(' ');
};

app.use(morgan(customFormat));

This function logs the same information as the previous example but offers more flexibility.

Sometimes, the predefined tokens might not cover all the information you need. Morgan allows the addition of custom tokens using functions. For instance, if you need to log the request host:

morgan.token('host', (req, res) => {
    return req.headers['host'];
});

app.use(morgan(':host :method :url :status :res[content-length] - :response-time ms'));

This will include the request host in the log output.

By default, Morgan logs to the standard output, usually the terminal. However, in a production environment, you might want to log to a file or another output stream. This can be done by specifying a stream in the options:

const fs = require('fs');
const logStream = fs.createWriteStream('access.log', { flags: 'a' });

app.use(morgan('combined', { stream: logStream }));

This will write the logs to a file named access.log.

Morgan is also smart enough to handle different logging scenarios. For example, you can log different types of requests to different destinations. Here’s how you can do it:

const fs = require('fs');
const errorLogStream = fs.createWriteStream('error.log', { flags: 'a' });
const accessLogStream = fs.createWriteStream('access.log', { flags: 'a' });

app.use(morgan('combined', { stream: accessLogStream }));
app.use(morgan('combined', {
    skip: (req, res) => res.statusCode < 400,
    stream: errorLogStream
}));

In this setup, all requests are logged to access.log, while error responses (status code 400 and above) are logged to error.log.

Morgan is a robust and adaptable tool for logging HTTP requests in Node.js applications. Whether it’s debugging, monitoring traffic, or analyzing performance, it provides vital insights into your application’s operations. By seamlessly integrating into Express and offering various logging options, Morgan proves to be an invaluable tool in any Node.js developer’s arsenal.

Cookie-cutter templates are convenient, but the magic of Morgan lies in its customizability. Whether it’s tweaking formats, creating custom tokens, or directing logs to different destinations, Morgan ensures that logging comes with the flexibility and control essential for maintaining and scaling modern web applications.