Skip to content

Commit 3522dec

Browse files
committed
feat: add article about nuxt otel plugin
1 parent fc57bd9 commit 3522dec

File tree

4 files changed

+230
-1
lines changed

4 files changed

+230
-1
lines changed

content/1.posts/70.nuxt-otel.md

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
---
2+
title: "How to Develop an Open Telemetry Plugin for Nuxt"
3+
lead: "Integrating Observability into Your Nuxt Application with OpenTelemetry"
4+
date: 2025-03-09
5+
image:
6+
src: /images/nuxt_otel.png
7+
badge:
8+
label: Development
9+
tags:
10+
- Nuxt
11+
- OpenTelemetry
12+
---
13+
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.
14+
15+
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.
16+
17+
## Add the OTel configuration in a Nuxt application
18+
19+
Let’s add some environment variables in the Nuxt configuration that our plugin will need:
20+
21+
* `otelExporterOtlpEndpoint` will hold the base endpoint URL for sending telemetry data (this corresponds to the [`OTEL_EXPORTER_OTLP_ENDPOINT` environment variable](https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/#otel_exporter_otlp_endpoint)).
22+
23+
* `otelExporterOtlpHeaders` will hold the list of headers to apply to all outgoing data (this corresponds to the [`OTEL_EXPORTER_OTLP_HEADERS` environment variable](https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/#otel_exporter_otlp_headers))
24+
25+
* `otelResourceAttributes` will hold the list of attributes for the resource (this corresponds to the [`OTEL_RESOURCE_ATTRIBUTES` environment variable](https://opentelemetry.io/docs/languages/sdk-configuration/general/#otel_resource_attributes))
26+
27+
* `otelServiceName` will hold the name of the resource that will be associated with the data (this corresponds to the [`OTEL_SERVICE_NAME` environment variable](https://opentelemetry.io/docs/languages/sdk-configuration/general/#otel_service_name))
28+
29+
```typescript [nuxt.config.ts]
30+
runtimeConfig: {
31+
public: {
32+
otelExporterOtlpEndpoint: '',
33+
otelExporterOtlpHeaders: '',
34+
otelResourceAttributes: '',
35+
otelServiceName: '',
36+
}
37+
}
38+
```
39+
40+
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](https://www.youtube.com/watch?v=_FYV5WfiWvs) for a better explanation). So don’t do that.
41+
42+
```typescript [nuxt.config.ts]
43+
runtimeConfig: {
44+
public: {
45+
otelExporterOtlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
46+
otelExporterOtlpHeaders: process.env.OTEL_EXPORTER_OTLP_HEADERS,
47+
otelResourceAttributes: process.env.OTEL_RESOURCE_ATTRIBUTES,
48+
otelServiceName: process.env.OTEL_SERVICE_NAME,
49+
}
50+
}
51+
```
52+
53+
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:
54+
55+
```plaintext [.env]
56+
NUXT_PUBLIC_OTEL_EXPORTER_OTLP_ENDPOINT=https://localhost:21171
57+
NUXT_PUBLIC_OTEL_RESOURCE_ATTRIBUTES=service.instance.id=acevubay
58+
NUXT_PUBLIC_OTEL_EXPORTER_OTLP_HEADERS=x-otlp-api-key=1a7be8ic1ch5b4
59+
NUXT_PUBLIC_OTEL_SERVICE_NAME=WebApp
60+
```
61+
62+
Nuxt is supposed to automatically [generate a typescript interface for the configuration](https://nuxt.com/docs/guide/going-further/runtime-config#typing-runtime-config) but as it did not seem to work on my project I provided the typing manually like that:
63+
64+
```typescript [config.d.ts]
65+
declare module 'nuxt/schema' {
66+
interface RuntimeConfig {
67+
}
68+
interface PublicRuntimeConfig {
69+
otelExporterOtlpEndpoint: string,
70+
otelExporterOtlpHeaders: string,
71+
otelResourceAttributes: string,
72+
otelServiceName: string,
73+
}
74+
}
75+
// It is always important to ensure you import/export something when augmenting a type
76+
export {}
77+
```
78+
79+
## Create the instrumentation plugin
80+
81+
We can use the `nuxt` CLI to create the new `instrumentation` plugin:
82+
83+
```bash
84+
pnpm nuxt add plugin instrumentation
85+
```
86+
87+
We can use the new [Object Syntax for plugins](https://nuxt.com/docs/guide/directory-structure/plugins#object-syntax-plugins) to implement our plugin:
88+
89+
```typescript [app/plugins/instrumentation.ts]
90+
export default defineNuxtPlugin({
91+
name: 'opentelemetry-plugin',
92+
async setup() {
93+
const config = useRuntimeConfig();
94+
const { otelExporterOtlpEndpoint: otlpUrl, otelExporterOtlpHeaders: headers, otelResourceAttributes: resourceAttributes, otelServiceName: serviceName } = config.public;
95+
if (otlpUrl && headers && resourceAttributes && serviceName) {
96+
initializeTelemetry(otlpUrl, parseDelimitedValues(headers), parseDelimitedValues(resourceAttributes), serviceName);
97+
}
98+
}
99+
})
100+
```
101+
102+
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.
103+
104+
```typescript [app/plugins/instrumentation.ts]
105+
function parseDelimitedValues(s: string): Record<string, string> {
106+
const headers = s.split(",");
107+
const result: Record<string, string> = {};
108+
109+
headers.forEach((header) => {
110+
const [key, value] = header.split("=");
111+
if (key && value) {
112+
result[key.trim()] = value.trim();
113+
}
114+
});
115+
116+
return result;
117+
}
118+
```
119+
120+
## Use the OpenTelemetry SDKs to implement the instrumentation
121+
122+
First, let’s add some OpenTelemetry npm packages to the project.
123+
124+
```bash
125+
pnpm add @opentelemetry/sdk-trace-web @opentelemetry/resources @opentelemetry/semantic-conventions
126+
```
127+
128+
::callout{icon="i-heroicons-exclamation-triangle"}
129+
Please note, as mentioned in the [OpenTelemetry SDK JavaScript documentation](https://opentelemetry.io/docs/languages/js/), that “the client instrumentation for the browser is experimental and mostly unspecified”.
130+
::
131+
132+
Second, let’s create a [`TracerProvider`](https://opentelemetry.io/docs/specs/otel/trace/api/#tracerprovider) using the `WebTracerProvider` class:
133+
134+
```typescript [app/plugins/instrumentation.ts]
135+
import {WebTracerProvider} from "@opentelemetry/sdk-trace-web";
136+
import {Resource} from "@opentelemetry/resources";
137+
import {ATTR_SERVICE_NAME} from "@opentelemetry/semantic-conventions";
138+
139+
function initializeTelemetry(otlpUrl: string, headers: Record<string, string>, ressourceAttributes: Record<string, string>, serviceName: string) {
140+
ressourceAttributes[ATTR_SERVICE_NAME] = serviceName;
141+
const provider = new WebTracerProvider({
142+
resource: new Resource(ressourceAttributes),
143+
});
144+
}
145+
```
146+
147+
We set up the `WebTracerProvider` with a [resource](https://opentelemetry.io/docs/languages/js/resources/) 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.
148+
149+
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.
150+
151+
We need to define how spans will be processed and exported. For the processor, you can [choose between using a `SimpleSpanProcessor` or a `BatchSpanProcessor`](https://opentelemetry.io/docs/languages/js/instrumentation/#picking-the-right-span-processor). 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.
152+
153+
```typescript [app/plugins/instrumentation.ts]
154+
const provider = new WebTracerProvider({
155+
resource: new Resource(ressourceAttributes),
156+
spanProcessors: [
157+
new BatchSpanProcessor(new ConsoleSpanExporter()),
158+
]
159+
});
160+
```
161+
162+
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`](https://www.npmjs.com/package/@opentelemetry/exporter-trace-otlp-proto) package.
163+
164+
```bash
165+
pnpm add @opentelemetry/exporter-trace-otlp-proto
166+
```
167+
168+
```typescript [app/plugins/instrumentation.ts]
169+
const provider = new WebTracerProvider({
170+
resource: new Resource(ressourceAttributes),
171+
spanProcessors: [
172+
new BatchSpanProcessor(new ConsoleSpanExporter()),
173+
new BatchSpanProcessor(new OTLPTraceExporter({url: `${otlpUrl}/v1/traces`, headers}))
174+
]
175+
});
176+
```
177+
178+
You can notice that to create this exporter we use the exporter endpoint URL and the headers we provided in the configuration.
179+
180+
OpenTelemetry uses a [context](https://opentelemetry.io/docs/languages/js/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`](https://www.npmjs.com/package/@opentelemetry/context-zone) package that will maintain the correct context between asynchronous operations.
181+
182+
```bash
183+
pnpm add @opentelemetry/context-zone
184+
```
185+
186+
```typescript [app/plugins/instrumentation.ts]
187+
provider.register({
188+
// Changing default contextManager to use ZoneContextManager - supports asynchronous operations - optional
189+
contextManager: new ZoneContextManager(),
190+
});
191+
```
192+
193+
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`](https://www.npmjs.com/package/@opentelemetry/auto-instrumentations-web) package that automatically captures data like documents load speed, user interactions, HTTP requests.
194+
195+
```bash
196+
pnpm add @opentelemetry/auto-instrumentations-web @opentelemetry/instrumentation
197+
```
198+
199+
```typescript [app/plugins/instrumentation.ts]
200+
registerInstrumentations({
201+
instrumentations: getWebAutoInstrumentations({
202+
"@opentelemetry/instrumentation-fetch": {
203+
propagateTraceHeaderCorsUrls: [new RegExp(`\\/api\\/*`)],
204+
}
205+
}),
206+
});
207+
```
208+
209+
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.
210+
211+
## Verify the instrumentation works correctly
212+
213+
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.
214+
215+
![Web console displaying span details exported in console.](/posts/images/70.nuxt-otel-1.png){.rounded-lg.mx-auto}
216+
217+
You can also verify that the `OTLPTraceExporter` is exporting the spans correctly by setting up a backend like [Jaeger](https://www.jaegertracing.io/), but this process is more complex.
218+
219+
## Resources & Conclusion
220+
221+
Since client instrumentation in the browser is still experimental, there aren't many resources available besides the [official documentation](https://opentelemetry.io/docs/languages/js/getting-started/browser/). 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:
222+
223+
* this [sample](https://github.com/aaronpowell/aspire-azure-dev-day-js-talk/blob/main/src/bookstore-web/src/instrumentation.ts) from [**Aaron Powell**](https://github.com/aaronpowell)
224+
225+
* this [sample](https://github.com/robrich/net-aspire/blob/main/01-full/vue-app/src/tracing.ts) from [Rob Richardson](https://github.com/robrich)
226+
227+
If you're interested in OpenTelemetry, I highly recommend the free training “[Getting Started With OpenTelemetry](https://training.linuxfoundation.org/training/getting-started-with-opentelemetry-lfs148/)” 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.
228+
229+
You can find the complete code of the plugin [here](https://github.com/TechWatching/AspnetWithNuxt/blob/f13278296bf3989af53d8560a5c4eae4862a1bea/WebApp/app/plugins/instrumentation.ts).

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"typescript": "^5.8.2",
4343
"vue-tsc": "^2.2.8"
4444
},
45-
"packageManager": "pnpm@10.5.2",
45+
"packageManager": "pnpm@10.6.1",
4646
"pnpm": {
4747
"onlyBuiltDependencies": [
4848
"@parcel/watcher",

public/images/nuxt_otel.png

465 KB
Loading
213 KB
Loading

0 commit comments

Comments
 (0)