Contents

    Guides

    How to use PM2 for monitoring production Node JS applications

    Anton Franzen

    My name is Anton, I'm an expert javascript developer who specializes in React JS/Next JS. Let’s connect: antonfranzen.dev

    Published on

    June 30, 2022
    How to use PM2 for monitoring production Node JS applications

    If you have built multiple rest APIs with Node JS, you’ll understand the importance of solid error handling, logging, and monitoring.

    This tutorial will demonstrate how to handle errors in a Node JS app properly and set up logging & monitoring so you can sleep at night. In the end, we'll have a working Node JS express app with logging and monitoring, so let's get started!

    The importance of handling errors correctly

    If you implement error handling the wrong way, debugging the app will be a nightmare. If you don't have logging in place, figuring out what went wrong will be even more complicated when the app is in use by many users.

    You should aim to implement your error handling in a way that makes debugging easier. For example, you should set up logs that log inbound and outbound requests so you can go back and check what happened when an issue occurred. You should also ensure that the server does not experience any downtime. You can do this by restarting the node process immediately after it has been shut down, as well as running many processors at the same time.

    To handle errors correctly and successfully, we must first learn about the node js runtime and the process life cycle.

    The Node JS runtime

    To begin, Node JS is a javascript runtime that executes and runs javascript in an environment.

    We could spend all day discussing the ins and outs of Node JS runtime. Still, the main thing to remember is that it is a runtime in which many different pieces interact and execute our software. Node runs in a single-threaded process, which means it runs code in a single process and has a load limit. So, if we want to avoid downtime and increase the demand on our server, we must run multiple processes.

    So let’s talk about the Node JS process and its life cycle.

    The Node JS process life cycle

    Have you ever wondered how a node server remains running even when no requests are coming in? This is because the Node JS process that runs the program only runs because of the event loop. The event loop executes events on the call stack and keeps the process running, but if the event loop runs out of things to do, the process will exit. So, for example, when you start your server with the express, the event loop is tricked into thinking it has jobs to accomplish. The process then continues to run as a result. Quite cool, right?

    Why does a process exit?

    As we now know, the Node JS process is running because the event loop is busy, and the process will terminate when the event loop has run out of tasks to complete.

    A Node JS process might also terminate in other ways, such as when an uncaughtException happens, which this article is all about: handling those errors properly.

    The goal is to understand what to do when a process terminates and how to log when it happens, so let's go over the two most common error events in Node and what causes them.

    Different errors events

    To properly implement error handling, you must first understand the various error events and what causes them. In Node JS, there are two primary sorts of errors:

    uncaughtException

    I'm sure you've seen this error before, uncaughtException is something that you, the developer, have messed up. For example, an API call is done without catching the error. It occurs when a javascript error is not correctly handled — when an error happens, and it is not caught. And as we know, the process will shut down if it encounters a uncaughtException.

    unhandledRejection

    This error is a promise that rejects and that you haven’t appropriately handled most of the time.
    So, for example, it could happen if you have a promise that does a network request and something goes wrong, causing it to reject, and you never catch it. And as we know, the process will shut down if that encounters.

    Best practices for handling Node JS errors

    There are ways to avoid the process exiting if any of these errors occur. Still, you should not keep the process running if an uncaughtException occurs by chance. Why? First and foremost, something far worse may occur a short time later. This is because something obviously went wrong when it encountered that problem. Since you have no idea what it may have been, allowing it to continue operating could result in serious damage. Allowing the process to terminate and then rapidly restarting it is what you want to do instead. Doing this ensures that everything begins in a clean state and works as expected.

    So as a conclusion for the error events

    Errors can occur, and the node process will shut down if we miss them. On the other hand, if we do nothing, the server will experience downtime, which we obviously do not want. Now that we've covered everything to know about the node process, let's look at why we need logging and monitoring and how we can use logging and monitoring to manage those error occurrences.

    Monitoring & logging

    Monitoring

    So after what we went through, it's pretty clear to see why we would need monitoring for our node server that checks if the processes are alive and manage them properly. Moreover, to see the load the node server is getting, the number of requests to certain endpoints, amount of requests per second, CPU usage, memory usage, and a few other things.

    There are various tools available for monitoring our node server; however, we will utilize pm2, an open-source monitoring tool for this article. I used pm2 specifically for this purpose in my previous production node program. It supports everything I said, and it has worked flawlessly so far.

    Logging

    So let's say a process shuts down. You need to save the logs to see what happened to go into the code and fix it. So you need to implement a robust error logging system on shutdowns, etc., and save the stack error, where it happened, and other important log data, among other things.

    pm2 does offer to log, but I'm not sure it's good enough to allow you to customize and obtain precisely what you want when you want it. As a result, for logging, we'll utilize Winston, an open-source logging tool for Node JS projects.

    What problem does Winston solve that we can't solve ourselves?

    The problem is that while we can use the native built-in logs such as console.log(), console.warn(), etc., there is one drawback: it only writes to the console, which isn't a reliable way to store logs. So instead, we'd want to save them in a file on the server or in a remote database. And that's where Winston comes in. It lets us choose where we want to save our logs, among other useful things.

    Implementation of everything above

    Now the fun begins! So what are we going to do?

    Well, a few things:

    1. Setup a simple node express server
    2. Setup monitoring with pm2
    3. Restart process on errors
    4. Setup logging

    Setup a simple node express server

    First, let’s create a new project...

    mkdir error-handling

    Navigate to the new project directory:

    cd error-handling

    Initialize a new project:

    npm init -y

    Next, let's install express:

    npm i express

    Let’s spin up a express server:

    const express = require("express");
    const app = express();
    
    app.get("/", (req, res, next) => {
      res.status(200).json("Success");
    });
    
    app.listen(5000, () => {
      console.log("Is running");
    });

    This will create a local server at localhost 5000 which we can make get requests to.

    Setup monitoring with pm2

    Now when we have our express server running, let’s install pm2 and setup monitoring:

    npm i pm2 -g

    Next, let's start our server with pm2, which is literally as easy as this:

    pm2 start index.js

    This will start our local server at localhost 5000 too, but not its managed by pm2.

    And again pm2 makes it so easy for us to set up a badass monitoring with 1 line:

    pm2 plus

    pm2 plus will create a website for us with a lovely server monitoring dashboard, which is extremely cool! This is what it contains:

    • A real-time Monitoring Web Interface
    • Issues & Exception Tracking
    • Deployment reporting
    • Realtime logs
    • Email & Slack notifications
    • Custom Metrics Monitoring
    • Custom Actions Center

    This is what it looks like now that I've run the command:

    View of the server monitoring dashboard created by pm2 plus

    Now that we've connected PM2 to our server for monitoring, the following step is to ensure that the server restarts when it encounters an error.

    Restart process on errors

    The nice thing about pm2 is that it will automatically restart the process if it exits. So, if you utilize PM2, you have that feature right out of the box.

    If you would have a exit.process somewhere in your code, normally the process would terminate as we talked about before. But with PM2 it will shut down and restart directly again, which is exactly what we wanted!

    Although it is important to note that even if a process is restarted, it may take some time in certain scenarios, which is why you should definitely run numerous processes at the same time, not only to optimize for zero downtime, but also to spread the burden on your server across different processes.

    Setup logging

    Now it's time to set up logging for our error events as well as other vital logs!

    First, let’s install the logging library winston:

    npm i winston

    Next let’s create a logging file which we will create a logging module:

    touch logger.js
    const { createLogger, transports, format } = require("winston");
    
    const mainLogger = createLogger({
      level: "info",
      transports: [
        new transports.File({
          filename: "info-logs.log",
          level: "info",
          format: format.combine(format.timestamp(), format.json()),
        }),
        new transports.File({
          filename: "error-logs.log",
          level: "error",
          format: format.combine(format.timestamp(), format.json()),
        }),
      ],
    });
    
    module.exports = mainLogger;

    We're going to make a logger module that has one type of logger that we can name an info log or an error log, and it will be saved to a file. We now import and use this module whenever we need to log something.

    Let's add this to index.js so we can utilize it to make loggs:

    const express = require("express");
    const app = express();
    const logger = require("./logger");
    
    app.get("/", (req, res, next) => {
      const query = req.query;
      const log = JSON.stringify(query);
      logger.info(`Incoming request: ${log}`);
      res.status(200).json("Success");
      logger.info(`Outgoing request: Success`);
    });
    
    app.listen(5000, () => {
      console.log("Is running");
    });

    Here we log the query on incoming requests and the outgoing response data.


    It gets a little trickier if we want to log something when an uncaughtException occurs with express, because by default, all uncaughtException error events are caught by an express middleware. So, when we use express, we can't complete the operation. instead, we add our own middleware as an error handler, and if an uncaughtException error occurs or any other error that isn’t caught elsewhere, it will be logged in the middleware1

    To do this, lets add this code to our index.js file:

    app.use((err, req, res, next) => {
      console.log("uncaughtException: ", err);
      logger.error(`Error: ${err}`);
    });

    This middleware will now handle any errors that are not caught.

    Conclusion

    That’s it. Today, you learned about the Node JS process, how to handle Node JS errors and setup monitoring and logging for our Node server. If you want a more in depth guide how to set up logs with winston, we recommend this tutorial and for logging in general, we recommend this tutorial.

    The entire source code for this tutorial is available in this GitHub repository.

    Data-rich bug reports loved by everyone

    Get visual proof, steps to reproduce and technical logs with one click

    Make bug reporting 50% faster and 100% less painful

    Rating LogosStars
    4.6
    |
    Category leader

    Liked the article? Spread the word

    Put your knowledge to practice

    Try Bird on your next bug - you’ll love it

    “Game changer”

    Julie, Head of QA

    star-ratingstar-ratingstar-ratingstar-ratingstar-rating

    Overall rating: 4.7/5

    Try Bird later, from your desktop