opentelemetry-instrumentation-fetch-node
Version:
OpenTelemetry Node 18+ native fetch automatic instrumentation package
175 lines • 7.24 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.FetchInstrumentation = void 0;
/*
* Portions from https://github.com/elastic/apm-agent-nodejs
* Copyright Elasticsearch B.V. and other contributors where applicable.
* Licensed under the BSD 2-Clause License; you may not use this file except in
* compliance with the BSD 2-Clause License.
*
*/
const node_diagnostics_channel_1 = __importDefault(require("node:diagnostics_channel"));
const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
const api_1 = require("@opentelemetry/api");
function getMessage(error) {
if (error instanceof AggregateError) {
return error.errors.map((e) => e.message).join(', ');
}
return error.message;
}
// Get the content-length from undici response headers.
// `headers` is an Array of buffers: [k, v, k, v, ...].
// If the header is not present, or has an invalid value, this returns null.
function contentLengthFromResponseHeaders(headers) {
const name = 'content-length';
for (let i = 0; i < headers.length; i += 2) {
const k = headers[i];
if (k.length === name.length && k.toString().toLowerCase() === name) {
const v = Number(headers[i + 1]);
if (!Number.isNaN(Number(v))) {
return v;
}
return undefined;
}
}
return undefined;
}
// A combination of https://github.com/elastic/apm-agent-nodejs and
// https://github.com/gadget-inc/opentelemetry-instrumentations/blob/main/packages/opentelemetry-instrumentation-undici/src/index.ts
class FetchInstrumentation {
// Keep ref to avoid https://github.com/nodejs/node/issues/42170 bug and for
// unsubscribing.
channelSubs;
spanFromReq = new WeakMap();
tracer;
config;
meter;
instrumentationName = 'opentelemetry-instrumentation-node-18-fetch';
instrumentationVersion = '1.0.0';
instrumentationDescription = 'Instrumentation for Node 18 fetch via diagnostics_channel';
subscribeToChannel(diagnosticChannel, onMessage) {
const channel = node_diagnostics_channel_1.default.channel(diagnosticChannel);
channel.subscribe(onMessage);
this.channelSubs.push({
name: diagnosticChannel,
channel,
onMessage,
});
}
constructor(config) {
// Force load fetch API (since it's lazy loaded in Node 18)
fetch('').catch(() => { });
this.channelSubs = [];
this.meter = api_1.metrics.getMeter(this.instrumentationName, this.instrumentationVersion);
this.tracer = api_1.trace.getTracer(this.instrumentationName, this.instrumentationVersion);
this.config = { ...config };
}
disable() {
this.channelSubs?.forEach((sub) => sub.channel.unsubscribe(sub.onMessage));
}
enable() {
this.subscribeToChannel('undici:request:create', (args) => this.onRequest(args));
this.subscribeToChannel('undici:request:headers', (args) => this.onHeaders(args));
this.subscribeToChannel('undici:request:trailers', (args) => this.onDone(args));
this.subscribeToChannel('undici:request:error', (args) => this.onError(args));
}
setTracerProvider(tracerProvider) {
this.tracer = tracerProvider.getTracer(this.instrumentationName, this.instrumentationVersion);
}
setMeterProvider(meterProvider) {
this.meter = meterProvider.getMeter(this.instrumentationName, this.instrumentationVersion);
}
setConfig(config) {
this.config = { ...config };
}
getConfig() {
return this.config;
}
onRequest({ request }) {
// Don't instrument CONNECT - see comments at:
// https://github.com/elastic/apm-agent-nodejs/blob/c55b1d8c32b2574362fc24d81b8e173ce2f75257/lib/instrumentation/modules/undici.js#L24
if (request.method === 'CONNECT') {
return;
}
if (this.config.ignoreRequestHook && this.config.ignoreRequestHook(request) === true) {
return;
}
const span = this.tracer.startSpan(`HTTP ${request.method}`, {
kind: api_1.SpanKind.CLIENT,
attributes: {
[semantic_conventions_1.SemanticAttributes.HTTP_URL]: getAbsoluteUrl(request.origin, request.path),
[semantic_conventions_1.SemanticAttributes.HTTP_METHOD]: request.method,
[semantic_conventions_1.SemanticAttributes.HTTP_TARGET]: request.path,
'http.client': 'fetch',
},
});
const requestContext = api_1.trace.setSpan(api_1.context.active(), span);
const addedHeaders = {};
api_1.propagation.inject(requestContext, addedHeaders);
if (this.config.onRequest) {
this.config.onRequest({ request, span, additionalHeaders: addedHeaders });
}
if (Array.isArray(request.headers)) {
request.headers.push(...Object.entries(addedHeaders).flat());
}
else {
request.headers += Object.entries(addedHeaders)
.map(([k, v]) => `${k}: ${v}\r\n`)
.join('');
}
this.spanFromReq.set(request, span);
}
onHeaders({ request, response }) {
const span = this.spanFromReq.get(request);
if (span !== undefined) {
// We are currently *not* capturing response headers, even though the
// intake API does allow it, because none of the other `setHttpContext`
// uses currently do.
const cLen = contentLengthFromResponseHeaders(response.headers);
const attrs = {
[semantic_conventions_1.SemanticAttributes.HTTP_STATUS_CODE]: response.statusCode,
};
if (cLen) {
attrs[semantic_conventions_1.SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH] = cLen;
}
span.setAttributes(attrs);
span.setStatus({
code: response.statusCode >= 400 ? api_1.SpanStatusCode.ERROR : api_1.SpanStatusCode.OK,
message: String(response.statusCode),
});
}
}
onDone({ request }) {
const span = this.spanFromReq.get(request);
if (span !== undefined) {
span.end();
this.spanFromReq.delete(request);
}
}
onError({ request, error }) {
const span = this.spanFromReq.get(request);
if (span !== undefined) {
span.recordException(error);
span.setStatus({
code: api_1.SpanStatusCode.ERROR,
message: getMessage(error),
});
span.end();
}
}
}
exports.FetchInstrumentation = FetchInstrumentation;
function getAbsoluteUrl(origin, path = '/') {
const url = `${origin}`;
if (origin.endsWith('/') && path.startsWith('/')) {
return `${url}${path.slice(1)}`;
}
if (!origin.endsWith('/') && !path.startsWith('/')) {
return `${url}/${path.slice(1)}`;
}
return `${url}${path}`;
}
//# sourceMappingURL=index.js.map