OpenTelemetry Node.js Fetch Instrumentation
This package, `@gasbuddy/opentelemetry-instrumentation-fetch-node`, provides automatic OpenTelemetry instrumentation specifically for Node.js 18+ native `fetch` API calls. Unlike generic HTTP instrumentation, it is designed to work with the `undici`-based native `fetch` implementation in newer Node.js versions. The current stable version is 1.2.3, released in July 2024, with a consistent release cadence addressing bug fixes and features. A key differentiator is its use of Node.js's diagnostics channel for tracing and a unique workaround involving a 'phony fetch' to an unparseable URL, ensuring the instrumentation is active even with Node's lazy-loading behavior of the `fetch` API. It allows for advanced customization of spans and headers through an `onRequest` event. This is crucial for applications leveraging native `fetch` in modern Node environments that need comprehensive observability.
Common errors
-
TypeError: fetch is not a function
cause The application is running on a Node.js version older than 18.0.0, where the global native `fetch` API is not available.fixUpgrade your Node.js runtime to version 18.0.0 or higher to enable native `fetch` functionality. -
OpenTelemetry traces for native fetch calls are not appearing or seem incomplete.
cause The `NodeFetchInstrumentation` or the overall OpenTelemetry SDK was not correctly initialized and registered before `fetch` calls were made, preventing proper instrumentation.fixEnsure `new NodeFetchInstrumentation()` is passed to `registerInstrumentations()` and that the `NodeSDK` is explicitly started *before* any native `fetch` requests are executed in your application's lifecycle. -
TypeScript compiler error: Property 'headers' does not exist on type 'Request' or 'Response' in `onRequest` callback.
cause The type definitions being used for `Request` or `Response` within the `onRequest` callback might not align with the `undici`-based native `fetch` types or you're attempting to access headers improperly.fixReview the `Request` and `Response` interfaces provided by the DOM and Node's `undici` typings for correct property access. Use standard methods like `request.headers.get('header-name')` or `request.headers.raw()` for robust header manipulation.
Warnings
- gotcha This instrumentation specifically targets Node.js 18.0.0 or higher due to its reliance on native `fetch` (which uses `undici` internally) and Node's diagnostics channel features. It will not function on older Node.js versions.
- gotcha Node.js's native `fetch` is lazily loaded. This instrumentation performs an internal 'phony fetch' to an unparseable URL at initialization to ensure the diagnostics channel is registered and no `fetch` events are missed.
- gotcha Unlike generic HTTP instrumentations (e.g., `@opentelemetry/instrumentation-http`), this package is exclusively for Node.js native `fetch`. If your application uses both native `fetch` and Node's traditional `http`/`https` modules, you will need to register both instrumentations to cover all traffic.
- gotcha Since `undici` (Node's internal fetch implementation) and consequently this instrumentation (from v1.2.0) support array-based header values, your `onRequest` or `onResponse` callbacks should be prepared to handle headers as potentially being string arrays, not just single strings.
Install
-
npm install opentelemetry-instrumentation-fetch-node -
yarn add opentelemetry-instrumentation-fetch-node -
pnpm add opentelemetry-instrumentation-fetch-node
Imports
- NodeFetchInstrumentation
const { NodeFetchInstrumentation } = require('@gasbuddy/opentelemetry-instrumentation-fetch-node');import { NodeFetchInstrumentation } from '@gasbuddy/opentelemetry-instrumentation-fetch-node'; - NodeFetchInstrumentationConfig
import type { NodeFetchInstrumentationConfig } from '@gasbuddy/opentelemetry-instrumentation-fetch-node'; - registerInstrumentations
import { registerInstrumentations } from '@opentelemetry/instrumentation';
Quickstart
import { NodeSDK } from '@opentelemetry/sdk-node';
import { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-node';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { NodeFetchInstrumentation } from '@gasbuddy/opentelemetry-instrumentation-fetch-node';
const sdk = new NodeSDK({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'my-fetch-service',
}),
traceExporter: new ConsoleSpanExporter(),
});
// Initialize and register the fetch instrumentation
registerInstrumentations([
new NodeFetchInstrumentation({
propagateContext: true, // Propagate trace context in outgoing headers
ignoreMethods: ['OPTIONS'], // Do not instrument OPTIONS requests
onRequest: (span, request) => {
// Add custom attributes to the span before the request is made
span.setAttribute('http.request.method', request.method);
span.setAttribute('http.request.headers.host', request.headers.get('host') ?? '');
// Example: Add a custom header to the outgoing request
// request.headers.set('x-custom-trace-id', span.spanContext().traceId);
},
applyCustomAttributesOnSpan: (span, request, response) => {
// Add custom attributes to the span after the response is received
if (response) {
span.setAttribute('http.response.status_code', response.status);
span.setAttribute('http.response.status_text', response.statusText);
}
}
}),
]);
sdk.start();
async function makeFetchRequest() {
try {
console.log('Making a fetch request...');
const response = await fetch('https://httpbin.org/get', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
});
const data = await response.json();
console.log('Fetch request completed (URL):', data.url);
} catch (error) {
console.error('Fetch request failed:', error);
}
}
// Execute the request and then shut down the SDK gracefully
makeFetchRequest().finally(() => {
console.log('Shutting down OpenTelemetry SDK...');
// A small delay to ensure all spans are processed before shutdown
setTimeout(() => sdk.shutdown().then(() => console.log('SDK shutdown complete.')), 500);
});