Development·

How to Develop an Open Telemetry Plugin for Nuxt

Integrating Observability into Your Nuxt Application with OpenTelemetry

When developing an application, it’s important to collect data for observability and monitoring purposes. The OpenTelemetry (OTel) is an open source observability framework that will help you collect this telemetry in a standardized way, while being completely vendor and tool agnostic.

Currently, there is no built-in OpenTelemetry integration in Nuxt but we can easily create a plugin for that, and that’s what we will do in this article. Telemetry data can be traces, metrics or logs but for the purpose of this article we will only focus on traces.

Add the OTel configuration in a Nuxt application

Let’s add some environment variables in the Nuxt configuration that our plugin will need:

nuxt.config.ts
  runtimeConfig: {
    public: {
      otelExporterOtlpEndpoint: '',
      otelExporterOtlpHeaders: '',
      otelResourceAttributes: '',
      otelServiceName: '',
    }
  }

We could directly set up the configuration using the standard OTel environment variables, as shown below. However, these would be evaluated at build time as default values. This means they would be included in the package and could not be changed at runtime if you want to modify them based on the environment (check this video for a better explanation). So don’t do that.

nuxt.config.ts
  runtimeConfig: {
    public: {
      otelExporterOtlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
      otelExporterOtlpHeaders: process.env.OTEL_EXPORTER_OTLP_HEADERS,
      otelResourceAttributes: process.env.OTEL_RESOURCE_ATTRIBUTES,
      otelServiceName: process.env.OTEL_SERVICE_NAME,
    }
  }

Instead, it’s better to override these values at runtime using the corresponding environment variables prefixed by NUXT_PUBLIC. Let’s define them in our .env file for instance:

.env
NUXT_PUBLIC_OTEL_EXPORTER_OTLP_ENDPOINT=https://localhost:21171
NUXT_PUBLIC_OTEL_RESOURCE_ATTRIBUTES=service.instance.id=acevubay
NUXT_PUBLIC_OTEL_EXPORTER_OTLP_HEADERS=x-otlp-api-key=1a7be8ic1ch5b4
NUXT_PUBLIC_OTEL_SERVICE_NAME=WebApp

Nuxt is supposed to automatically generate a typescript interface for the configuration but as it did not seem to work on my project I provided the typing manually like that:

config.d.ts
declare module 'nuxt/schema' {
  interface RuntimeConfig {
  }
  interface PublicRuntimeConfig {
    otelExporterOtlpEndpoint: string,
    otelExporterOtlpHeaders: string,
    otelResourceAttributes: string,
    otelServiceName: string,
  }
}
// It is always important to ensure you import/export something when augmenting a type
export {}

Create the instrumentation plugin

We can use the nuxt CLI to create the new instrumentation plugin:

pnpm nuxt add plugin instrumentation

We can use the new Object Syntax for plugins to implement our plugin:

app/plugins/instrumentation.ts
export default defineNuxtPlugin({
  name: 'opentelemetry-plugin',
  async setup() {
    const config = useRuntimeConfig();
    const { otelExporterOtlpEndpoint: otlpUrl, otelExporterOtlpHeaders: headers, otelResourceAttributes: resourceAttributes, otelServiceName: serviceName } = config.public;
    if (otlpUrl && headers && resourceAttributes && serviceName) {
      initializeTelemetry(otlpUrl, parseDelimitedValues(headers), parseDelimitedValues(resourceAttributes), serviceName);
    }
  }
})

Here, we are simply retrieving the configuration we defined and call an initializeTelemetry method. The headers and attributes are strings containing key-value pairs separated by commas so we use the following function to parse them into records, making them easier to use.

app/plugins/instrumentation.ts
function parseDelimitedValues(s: string): Record<string, string> {
  const headers = s.split(",");
  const result: Record<string, string> = {};

  headers.forEach((header) => {
    const [key, value] = header.split("=");
    if (key && value) {
      result[key.trim()] = value.trim();
    }
  });

  return result;
}

Use the OpenTelemetry SDKs to implement the instrumentation

First, let’s add some OpenTelemetry npm packages to the project.

pnpm add @opentelemetry/sdk-trace-web @opentelemetry/resources @opentelemetry/semantic-conventions
Please note, as mentioned in the OpenTelemetry SDK JavaScript documentation, that “the client instrumentation for the browser is experimental and mostly unspecified”.

Second, let’s create a TracerProvider using the WebTracerProvider class:

app/plugins/instrumentation.ts
import {WebTracerProvider} from "@opentelemetry/sdk-trace-web";
import {Resource} from "@opentelemetry/resources";
import {ATTR_SERVICE_NAME} from "@opentelemetry/semantic-conventions";

function initializeTelemetry(otlpUrl: string, headers: Record<string, string>, ressourceAttributes: Record<string, string>, serviceName: string) {
  ressourceAttributes[ATTR_SERVICE_NAME] = serviceName;
  const provider = new WebTracerProvider({
    resource: new Resource(ressourceAttributes),
  });
}

We set up the WebTracerProvider with a resource that included some attributes, and we added the service name to these attributes. This helps provide context about the entity that will produce the telemetry data.

The telemetry data produced are traces of operations in a distributed system. In our case, this includes the entire workflow from user interactions on the web application to the final result, covering API calls, potential database interactions, and more. Traces consist of spans that represent the different steps of a trace.

We need to define how spans will be processed and exported. For the processor, you can choose between using a SimpleSpanProcessor or a BatchSpanProcessor. In a local development environment, the SimpleSpanProcessor is beneficial because it processes and exports spans immediately as they are created. However, in a production environment, it is advisable to use the BatchSpanProcessor to batch spans before exporting them, which is more efficient. For the exporter we can use the ConsoleSpanExporter to display the spans in the web console.

app/plugins/instrumentation.ts
  const provider = new WebTracerProvider({
    resource: new Resource(ressourceAttributes),
    spanProcessors: [
      new BatchSpanProcessor(new ConsoleSpanExporter()),
    ]
  });

Since we want to send the traces to an observability backend (like Jaeger or Honeycomb, for example), we will also create an OTLPTraceExporter from the @opentelemetry/exporter-trace-otlp-proto package.

pnpm add @opentelemetry/exporter-trace-otlp-proto
app/plugins/instrumentation.ts
  const provider = new WebTracerProvider({
    resource: new Resource(ressourceAttributes),
    spanProcessors: [
      new BatchSpanProcessor(new ConsoleSpanExporter()),
      new BatchSpanProcessor(new OTLPTraceExporter({url: `${otlpUrl}/v1/traces`, headers}))
    ]
  });

You can notice that to create this exporter we use the exporter endpoint URL and the headers we provided in the configuration.

OpenTelemetry uses a context to store and propagate telemetry data to the different components that will create spans. For web applications, the documentation suggests to use a specific context manager ZoneContextManager from the @opentelemetry/context-zone package that will maintain the correct context between asynchronous operations.

pnpm add @opentelemetry/context-zone
app/plugins/instrumentation.ts
provider.register({
  // Changing default contextManager to use ZoneContextManager - supports asynchronous operations - optional
  contextManager: new ZoneContextManager(),
});

The last step is to specify what we want to instrumente. For that we can use the getWebAutoInstrumentations method from the @opentelemetry/auto-instrumentations-web package that automatically captures data like documents load speed, user interactions, HTTP requests.

pnpm add @opentelemetry/auto-instrumentations-web @opentelemetry/instrumentation
app/plugins/instrumentation.ts
  registerInstrumentations({
    instrumentations: getWebAutoInstrumentations({
      "@opentelemetry/instrumentation-fetch": {
        propagateTraceHeaderCorsUrls: [new RegExp(`\\/api\\/*`)],
      }
    }),
  });

As you can see above, we can customize how the web auto-instrumentation behaves by specifying certain configurations. This can be useful if you want to disable some instrumentations.

Verify the instrumentation works correctly

To make sure the plugin is set up correctly and functioning well, you can start the application and check the web console. . Since we configured the ConsoleSpanExporter, you will be able to see all the spans that are collected.

Web console displaying span details exported in console.

You can also verify that the OTLPTraceExporter is exporting the spans correctly by setting up a backend like Jaeger, but this process is more complex.

Resources & Conclusion

Since client instrumentation in the browser is still experimental, there aren't many resources available besides the official documentation. You can probably find examples from observability vendors, but they often focus on their products and don't always use the OpenTelemetry SDKs directly. Fortunately, I found some examples that were very helpful in writing the instrumentation code:

If you're interested in OpenTelemetry, I highly recommend the free training “Getting Started With OpenTelemetry” from the Linux Foundation. It was very helpful for me to understand the OTel concepts and experiment with SDKs in the labs, even if it was for other programing languages.

You can find the complete code of the plugin here.


The opinions expressed herein are my own and do not represent those of my employer or any other third-party views in any way.

Copyright © 2025 Alexandre Nédélec. All rights reserved.