OpenTelemetry setup in NextJS with AppRouter and Azure Monitor Application Insights

OpenTelemetry setup in NextJS with AppRouter and Azure Monitor Application Insights

Updated on 15 June 2024 here

OpenTelemetry is an open-source observability framework for generating, collecting, and exporting telemetry data (metrics, logs, and traces) for analysis to understand your software's performance and behavior. You can analyze them using Prometheus, Jaeger, and other observability tools. It's a CNCF-incubated project and is the second most contributed project in the CNCF landscape, second only to Kubernetes.

The main advantage of using OpenTelemetry is that it provides a vendor-neutral way to collect telemetry data. This means you can switch your observability platform without having to change your application code.

In this post, I will guide you through the process of setting up OpenTelemetry in a NextJS application with AppRouter and exporting the telemetry data to Azure Monitor Application Insights.

Setting up OpenTelemetry in NextJS with AppRouter

It is easiest to set up using the @vercel/otel library because it supports exporting to 3rd parties and results in the least amount of code. This will also work with self-hosted setups in Docker (e.g. Azure Container Apps), Azure App Service, PM2, etc.

Read the official docs at https://vercel.com/docs/observability/otel-overview.

Installing packages

As per the official docs, you need to install @vercel/otel and @opentelemetry/api. To support Azure Monitor Application Insights, you also need to install @azure/monitor-opentelemetry-exporter.

Notes:

  • The @azure/monitor-opentelemetry-exporter is in beta
  • The @vercel/otel package relies on older versions of @opentelemetry/api, so you may need to control the versions by overriding or installing the relied-upon version.

In my case, I run the following command (versions may change later):

pnpm install @vercel/otel @opentelemetry/api@1.7.0 @azure/monitor-opentelemetry-exporter@next --save-exact

Instrumentation file

To set up OpenTelemetry in NextJS, you need to create an instrumentation.ts file at the root of your project. Here's how your instrumentation.ts file should look like:

import { AzureMonitorTraceExporter } from '@azure/monitor-opentelemetry-exporter';
import { registerOTel } from '@vercel/otel';
 
export async function register() {
  registerOTel({
    serviceName: 'your-project-name',
    traceExporter: new AzureMonitorTraceExporter({
      connectionString: 'InstrumentationKey=<...>;IngestionEndpoint=https://...;LiveEndpoint=https://...',
      // you can read from ENV if you prefer to
      // connectionString: process.env.APP_INSIGHTS_CONNECTION_STRING,
    }),
  });
}

In this file, we're calling registerOTel which handles the setup of OpenTelemetry for NodeJS and specific elements for NextJS. Setting the traceExporter becomes the next important thing.

Remember to set the correct value for connectionString as copied from the Azure Portal.

Enabling instrumentation

As of the time of this writing, instrumentation using the instrumentation.ts file is experimental (see docs). You can enable it in your next.config.ts by setting experimental.instrumentationHook = true

/** @type {import('next').NextConfig} */
const config = {
  experimental: {
    instrumentationHook: true,
  },
  // other config
};
 
export default config;

Nuances of the setup

At this point, you will start to get errors on the build. The first one will read Module not found: Can't resolve 'os' then subsequently be followed by fs, child_process, and path modules. These are Webpack warnings and my suspicion is they will disappear when @azure/monitor-opentelemetry-exporter is out of beta or when we start using turbopack in place of Webpack.

Sentry guides us on how to solve this at https://sentry.io/answers/module-not-found-nextjs/#how-to-resolve, but we need to fix this on the server side because that is where the telemetry is collected. I changed my next.config.js file to have the following Webpack config.

/** @type {import('next').NextConfig} */
const config = {
  experimental: {
    instrumentationHook: true,
  },
  webpack: (config, { isServer }) => {
    if (isServer) {
      // required for @azure/monitor-opentelemetry-exporter to work
      config.resolve.fallback ??= {};
      config.resolve.fallback.os = false;
      config.resolve.fallback.fs = false;
      config.resolve.fallback.child_process = false;
      config.resolve.fallback.path = false;
    }
    return config;
  },
  // other config
};
 
export default config;

Update 15 June 2024

Making webpack changes is not necessary, instead selectively exporter.

Your next.config.js file:

/** @type {import('next').NextConfig} */
const config = {
  experimental: {
    instrumentationHook: true,
  },
  // other config
};
 
export default config;

Your instrumentation.ts file:

import type { SpanExporter } from '@opentelemetry/sdk-trace-base';
import { registerOTel } from '@vercel/otel';
 
export async function register() {
  let traceExporter: SpanExporter | undefined;
 
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    const { AzureMonitorTraceExporter } = await import('@azure/monitor-opentelemetry-exporter');
    traceExporter = new AzureMonitorTraceExporter({
      connectionString: 'InstrumentationKey=<...>;IngestionEndpoint=https://...;LiveEndpoint=https://...',
      // you can read from ENV if you prefer to
      // connectionString: process.env.APP_INSIGHTS_CONNECTION_STRING,
    });
  }
 
  registerOTel({ serviceName: 'your-project-name', traceExporter });
}

Update 23 Aug 2024

While this works, there may be issues when you set it up on your end. When debugging locally it takes about 1-3 minutes to show up on Azure portal. To you .env or .env.* files add OTEL_LOG_LEVEL=debug, restart the app, open a page a couple of times (generates traces), then you will see logs like:

  β–² Next.js 14.2.5 (turbo)
  - Local:        http://localhost:3000
  - Environments: .env
  - Experiments (use with caution):
    Β· instrumentationHook
 
 βœ“ Starting...
@opentelemetry/api: Registered a global for diag v1.9.0.
@vercel/otel: Configure propagator: tracecontext
@vercel/otel: Configure propagator: baggage
@vercel/otel: Configure sampler:  parentbased_always_on
@opentelemetry/api: Registered a global for trace v1.9.0.
@opentelemetry/api: Registered a global for context v1.9.0.
@opentelemetry/api: Registered a global for propagation v1.9.0.
@vercel/otel: Configure instrumentations: fetch undefined
@vercel/otel: started <serviceName> nodejs
found resource. {
  _attributes: {},
  asyncAttributesPending: false,
  _syncAttributes: {},
  _asyncAttributesPromise: undefined
}
Exporting 1 envelope(s)
 βœ“ Ready in 1484ms
Exporting 1 span(s). Converting to envelopes...
Exporting 2 envelope(s)
 β—‹ Compiling / ...
 βœ“ Compiled / in 3s
 GET / 200 in 3291ms
 βœ“ Compiled /favicon.ico in 173ms
 GET /icon.png?icon.499d3844.png 200 in 298ms
 GET /favicon.ico?favicon.196e4467.ico 200 in 310ms
Exporting 16 span(s). Converting to envelopes...
Exporting 17 envelope(s)
Searching for filesystem persisted files
Searching for filesystem persisted files

Otherwise, there should be a meaningful error message

Conclusion

With this setup, your NextJS application with AppRouter is now ready to export telemetry data to Azure Monitor Application Insights. Run your app and navigate across several pages for telemetry to be sent to Azure. Remember that it can take a few minutes for the data to show up on Azure Portal, usually two minutes.

If you like this work, feel free to sponsor my work here.