
How to Develop an Open Telemetry Plugin for Nuxt
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:
otelExporterOtlpEndpoint
will hold the base endpoint URL for sending telemetry data (this corresponds to theOTEL_EXPORTER_OTLP_ENDPOINT
environment variable).otelExporterOtlpHeaders
will hold the list of headers to apply to all outgoing data (this corresponds to theOTEL_EXPORTER_OTLP_HEADERS
environment variable)otelResourceAttributes
will hold the list of attributes for the resource (this corresponds to theOTEL_RESOURCE_ATTRIBUTES
environment variable)otelServiceName
will hold the name of the resource that will be associated with the data (this corresponds to theOTEL_SERVICE_NAME
environment variable)
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.
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:
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:
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:
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.
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
Second, let’s create a TracerProvider
using the WebTracerProvider
class:
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.
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
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
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
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.
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:
- this sample from Aaron Powell
- this sample from Rob Richardson
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.