Measuring Node.js Performance in Production with Performance Hooks
Tags: node, apm
Published: 2024-05-01 11:42:21

In the first part of this series, we toured performance APIs in Node.js. We discussed the capabilities of APIs and how they can diagnose slowdowns or network issues in your Node application.

Now, in this concluding segment, we will embark on a practical journey, applying these performance hooks in a real-world scenario. You will understand how to effectively use these tools to monitor and enhance your application's performance.

Let's dive in and see these concepts in action!

## Prerequisites

Before proceeding with this tutorial, ensure you have a foundational understanding of the [Node.js Performance Measurement APIs](https://nodejs.org/api/perf_hooks.html#performance-measurement-apis) as outlined in part one of this series.

## Continuous Monitoring with Performance Observer for Node

Continuing our exploration of Node.js Performance APIs, we come to the [Performance Observer](https://nodejs.org/api/perf_hooks.html#class-performanceobserver). This API allows you to track new `PerformanceEntry` instances in real time as they are added to the Performance Timeline. Let's examine how to implement it:

```javascript
import { performance, PerformanceObserver } from "node:perf_hooks";

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(entry);
  }
});

observer.observe({ entryTypes: ["resource"] });
```

With this configuration, every time a `PerformanceEntry` with the 'resource' `entryType` is added to the Performance Timeline, it gets logged automatically to the console. To explore all the entry types available for monitoring, you can execute:

```javascript
console.log(PerformanceObserver.supportedEntryTypes);
```

This will display a variety of entry types:

```javascript
[
  "dns",
  "function",
  "gc",
  "http",
  "http2",
  "mark",
  "measure",
  "net",
  "resource",
];
```

To broaden the scope of monitoring to encompass all these entry types, adjust your observer like so:

```javascript
observer.observe({ entryTypes: PerformanceObserver.supportedEntryTypes });
```

A use case for observing the performance timeline is to upload performance measurements to an Application Performance Monitoring (APM) service like [AppSignal](https://www.appsignal.com/tour/performance). This way, you can keep track of how the key metrics you care about are performing and get alerted to any performance degradation.

In the following sections, I'll present a practical example showcasing the use of the Performance Observer in real-world application contexts.

## A Practical Example: Monitoring Third-Party API Integrations for Node

To maintain a seamless user experience within your app, you need to track the performance of the external APIs you rely on. Delays in API responses can lead to slower interactions, adversely affecting your application's responsiveness.

To mitigate this, it's advisable to choose API providers that offer response time guarantees. Additionally, you can implement a strategy to switch to a fallback API or use cached resources when experiencing latency spikes.

Let's explore a practical scenario where your Node.js application interacts with multiple third-party APIs. You need to track each request's latency to establish a performance baseline and monitor adherence to Service-Level Agreements (SLAs).

In such cases, you can use the performance measurement APIs to collect the timing data, format it appropriately, and dispatch it periodically to a performance monitoring tool or a logging system.

This ongoing process allows you to understand the typical performance parameters for each API, so you can quickly identify and resolve any anomalies or unexpected delays in API responses.

In the following example, you'll set up monitoring for a Node.js application's API requests using AppSignal. This setup ensures that you can always track an API's response times and get alerted to performance issues automatically.

Let's begin!

## Setting up the Demo Repository

To follow through with the example below, [clone this repository](https://github.com/damilolaolatunji/nodejs-perf) to your computer with the command below:

```shell
git clone https://github.com/damilolaolatunji/nodejs-perf
```

After cloning, switch to the repository's directory and install the necessary dependencies:

```shell
cd nodejs-perf
npm install
```

Then, open the project in your text editor and navigate to the `server.js` file. This file includes a basic Fastify server setup with one route:

```javascript
import Fastify from "fastify";

const fastify = Fastify({
  logger: true,
  disableRequestLogging: true,
});

fastify.get("/", async (_request, reply) => {
  const res1 = await fetch("https://jsonplaceholder.typicode.com/posts/1");
  await res1.json();

  const res2 = await fetch("https://covid-api.com/api/reports/total");
  await res2.json();

  const res3 = await fetch("https://ipinfo.io/json");
  await res3.json();
});

const port = process.env.PORT || 3000;

fastify.listen({ port }, (err, address) => {
  if (err) {
    fastify.log.error(err);
    process.exit(1);
  }

  fastify.log.info(`Fastify is listening on port: ${address}`);
});
```

This route performs three GET requests and returns a 200 OK response. While this is a simplistic example, it's perfectly suited to demonstrate how performance hooks can be employed in real-world applications.

To start the server for development on port 3000, run:

```shell
npm run start-dev
```

Verify that the server is functioning correctly by sending a request to the root using curl. The command should execute successfully:

```shell
curl http://localhost:3000
```

In the following section, you'll use the `PerformanceObserver` API to monitor each API request continuously.

## Tracking the API Requests

Since the Fetch API automatically adds resource timing data to the Performance Timeline for each request, you can automatically track the measurements using the `PerformanceObserver` API as shown below:

```javascript
// server.js
import Fastify from 'fastify';
import { performance, PerformanceObserver } from 'node:perf_hooks';

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.initiatorType === 'fetch') {
      console.log(entry);
    }
  }

  performance.clearResourceTimings();
});

observer.observe({ entryTypes: ['resource'] });

. . .
```

In this setup, each time a `PerformanceResourceTiming` object is added to the timeline, the script checks if the initiator is a `fetch` request and logs the details to the console. Clearing the resource timings after logging ensures room for new entries.

Once you update the file, the server should restart, and you can repeat the `curl` command from earlier:

```shell
curl http://localhost:3000
```

The server's console should now display three `PerformanceResourceTiming` entries sequentially:

```javascript
PerformanceResourceTiming {
  name: 'https://jsonplaceholder.typicode.com/posts/1',
  entryType: 'resource',
  startTime: 205250.912353009,
  duration: 2046.0582769811153,
  initiatorType: 'fetch',
  nextHopProtocol: undefined,
  workerStart: 0,
  redirectStart: 0,
  redirectEnd: 0,
  fetchStart: 205250.912353009,
  domainLookupStart: undefined,
  domainLookupEnd: undefined,
  connectStart: undefined,
  connectEnd: undefined,
  secureConnectionStart: undefined,
  requestStart: 0,
  responseStart: 0,
  responseEnd: 207296.9706299901,
  transferSize: 300,
  encodedBodySize: 0,
  decodedBodySize: 0
}
PerformanceResourceTiming {
  name: 'https://covid-api.com/api/reports/total',
  entryType: 'resource',
  startTime: 207298.71959400177,
  duration: 4112.256608009338,
  initiatorType: 'fetch',
  nextHopProtocol: undefined,
  workerStart: 0,
  redirectStart: 0,
  redirectEnd: 0,
  fetchStart: 207298.71959400177,
  domainLookupStart: undefined,
  domainLookupEnd: undefined,
  connectStart: undefined,
  connectEnd: undefined,
  secureConnectionStart: undefined,
  requestStart: 0,
  responseStart: 0,
  responseEnd: 211410.9762020111,
  transferSize: 300,
  encodedBodySize: 0,
  decodedBodySize: 0
}
PerformanceResourceTiming {
  name: 'https://ipinfo.io/json',
  entryType: 'resource',
  startTime: 211411.61356300116,
  duration: 2160.16487801075,
  initiatorType: 'fetch',
  nextHopProtocol: undefined,
  workerStart: 0,
  redirectStart: 0,
  redirectEnd: 0,
  fetchStart: 211411.61356300116,
  domainLookupStart: undefined,
  domainLookupEnd: undefined,
  connectStart: undefined,
  connectEnd: undefined,
  secureConnectionStart: undefined,
  requestStart: 0,
  responseStart: 0,
  responseEnd: 213571.7784410119,
  transferSize: 300,
  encodedBodySize: 0,
  decodedBodySize: 0
}
```

For a more focused output, you can modify the logging to include only the relevant details:

```javascript
if (entry.initiatorType === "fetch") {
  console.log(entry.name, entry.duration);
}
```

Resulting in:

```text
https://jsonplaceholder.typicode.com/posts/1 1969.7759200036526
Ghttps://covid-api.com/api/reports/total 3156.867073982954
https://ipinfo.io/json 1885.5162260234356
```

With the ability to track, format, and log each request's timing, the next phase involves integrating these metrics with a monitoring solution like AppSignal.

Before moving to the next section, [sign up for a 30-day free trial of AppSignal](https://appsignal.com/users/sign_up).

## Tracking Node.js Performance Measurements with AppSignal

Once you've signed into your AppSignal account, create a new application and follow the setup instructions. Copy the **Push API Key** when prompted or head over to the settings and find the key in the **Push & Deploy** section.

Back in your project, create a `.env` file at the root directory and add your AppSignal Push API Key:

```text
// .env
APPSIGNAL_PUSH_API_KEY=<your push api key>
```

Once saved, return to your `server.js` file and
[initialize your AppSignal configuration](https://docs.appsignal.com/nodejs/3.x/configuration.html)
as follows:

```javascript
// server.js
import 'dotenv/config';
import { Appsignal } from '@appsignal/nodejs';
import Fastify from 'fastify';
import { performance, PerformanceObserver } from 'node:perf_hooks';

new Appsignal({
  active: true,
  name: 'Node.js Perf Demo',
  pushApiKey: process.env.APPSIGNAL_PUSH_API_KEY,
});

. . .
```

Next, modify the `PerformanceObserver` to start tracking the timing data in AppSignal:

```javascript
// server.js
. . .
const meter = Appsignal.client.metrics();

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.initiatorType === 'fetch') {
      meter.addDistributionValue('external_api_request', entry.duration, {
        url: entry.name,
      });
    }
  }

  performance.clearResourceTimings();
});

observer.observe({ entryTypes: ['resource'] });
. . .
```

The `addDistributionValue()` method is ideal for tracking metrics over time, allowing you to create graphs based on average values or call counts.

Its first argument is the metric name, and the second is the value to be tracked. It also accepts one or more tags in an optional object, which, in this case, is each third-party integration being tracked in the program.

Save the file and wait for the server to restart. Test the setup by sending requests to the `/` route every five seconds using this script:

```shell
while true; do curl http://localhost:4000; sleep 5; done
```

With the script running, head back to your AppSignal application and click **Add dashboard** on the left followed by **Create a dashboard** on the resulting page. Title your dashboard and click **Create dashboard**.

Once the dashboard is created, click the **Add graph** button. Give your graph a title (such as "Request durations") and click the add metric button:

![Add a graph form](https://blog.appsignal.com/images/blog/2024-04/add-graph.png)

Find the `external_api_request` metric option and configure the graph as shown below:

![Add a metric](https://blog.appsignal.com/images/blog/2024-04/add-metric.png)

Click the **Back to overview** button, and change the value of the **Label of value in Legend** field to:

```text
%name% %field% %tag%
```

![Label value in Legend](https://blog.appsignal.com/images/blog/2024-04/label-legend.png)

Once you're done, click the **Create graph** button on the bottom right. You will now see a line graph plotting the duration of each API request made by your program.

![AppSignal Performance Dashboard](https://blog.appsignal.com/images/blog/2024-04/api-request.png)

You can hover on the line graph to view the mean, 90th, and 95th percentile for each request.

![Hovering over the graph](https://blog.appsignal.com/images/blog/2024-04/hover-graph.png)

Any spikes or unusual patterns in the chart can be investigated to pinpoint the cause of performance issues.

[See the AppSignal documentation for more information on metrics and dashboards](https://docs.appsignal.com/metrics/dashboards.html).

## Getting Alerted to Performance Issues

When you've established the normal behavior of an API, you can set up alerts to detect when anomalies occur. AppSignal simplifies this process, allowing you to quickly configure alerts for unusual performance patterns.

Return to your dashboard and find the **Anomaly Detection > Triggers** section in the lower left. Click the **Add a trigger** button on the resulting page:

![Set a trigger on any metric](https://blog.appsignal.com/images/blog/2024-04/appsignal-triggers.png)

In the **New trigger** form, click **Custom metrics** at the bottom of the sidebar. Here, configure the trigger based on your specific requirements. Ensure you select `external_api_request` as the **Metric name** and choose an appropriate **Comparison operator** that suits the alert condition you want to monitor.

![Trigger form](https://blog.appsignal.com/images/blog/2024-04/trigger-form.png)

To receive email alerts, ensure that the **Email notifications** option is checked. This will notify you via email whenever the set conditions are met, helping you stay informed about any performance issues in real time. You can also add other alerting integrations like Slack, PagerDuty, Discord, etc., in the application settings.

Once you've set up the trigger conditions and notification settings, click **Save trigger**. This action will activate the trigger, and AppSignal will start monitoring the specified metric according to the conditions you've set.

![Email alert example](https://blog.appsignal.com/images/blog/2024-04/email-alert.png)

And that's it!

## Wrapping Up

With this setup in place, you can now track your third-party integrations reliably and get alerted if any performance issues arise.

I hope this has given you an idea of how to use the Node.js performance APIs in tandem with AppSignal to track, monitor, and ultimately improve your Node application's performance.

Cheers, and happy coding!

**P.S. If you liked this post, [subscribe to our JavaScript Sorcery list](https://blog.appsignal.com/javascript-sorcery) for a monthly deep dive into more magical JavaScript tips and tricks.**

**P.P.S. If you need an APM for your Node.js app, go and [check out the AppSignal APM for Node.js](https://www.appsignal.com/nodejs).**