diff --git a/packages/opentelemetry-instrumentation-fetch/README.md b/packages/opentelemetry-instrumentation-fetch/README.md index 6c04e78606..2e139bd5eb 100644 --- a/packages/opentelemetry-instrumentation-fetch/README.md +++ b/packages/opentelemetry-instrumentation-fetch/README.md @@ -61,6 +61,14 @@ fetch('http://localhost:8090/fetch.js'); See [examples/tracer-web/fetch](https://github.com/open-telemetry/opentelemetry-js/tree/main/examples/tracer-web) for a short example. +### Fetch Instrumentation options + +Fetch instrumentation plugin has few options available to choose from. You can set the following: + +| Options | Type | Description | +| ------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | ------------------------------------- | +| [`applyCustomAttributesOnSpan`](https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-instrumentation-fetch/src/fetch.ts#L47) | `HttpCustomAttributeFunction` | Function for adding custom attributes | + ## Useful links - For more information on OpenTelemetry, visit: diff --git a/packages/opentelemetry-instrumentation-fetch/src/fetch.ts b/packages/opentelemetry-instrumentation-fetch/src/fetch.ts index 93259c8002..8d28649794 100644 --- a/packages/opentelemetry-instrumentation-fetch/src/fetch.ts +++ b/packages/opentelemetry-instrumentation-fetch/src/fetch.ts @@ -19,6 +19,7 @@ import { isWrapped, InstrumentationBase, InstrumentationConfig, + safeExecuteInTheMiddle, } from '@opentelemetry/instrumentation'; import * as core from '@opentelemetry/core'; import * as web from '@opentelemetry/web'; @@ -43,6 +44,14 @@ const getUrlNormalizingAnchor = () => { return a; }; +export interface FetchCustomAttributeFunction { + ( + span: api.Span, + request: Request | RequestInit, + result: Response | FetchError + ): void; +} + /** * FetchPlugin Config */ @@ -61,6 +70,8 @@ export interface FetchInstrumentationConfig extends InstrumentationConfig { * also not be traced. */ ignoreUrls?: Array; + /** Function for adding custom attributes on the span */ + applyCustomAttributesOnSpan?: FetchCustomAttributeFunction; } /** @@ -311,6 +322,7 @@ export class FetchInstrumentation extends InstrumentationBase< response: Response ) { try { + plugin._applyAttributesAfterFetch(span, options, response); if (response.status >= 200 && response.status < 400) { plugin._endSpan(span, spanData, response); } else { @@ -331,6 +343,7 @@ export class FetchInstrumentation extends InstrumentationBase< error: FetchError ) { try { + plugin._applyAttributesAfterFetch(span, options, error); plugin._endSpan(span, spanData, { status: error.status || 0, statusText: error.message, @@ -360,6 +373,28 @@ export class FetchInstrumentation extends InstrumentationBase< }; } + private _applyAttributesAfterFetch( + span: api.Span, + request: Request | RequestInit, + result: Response | FetchError + ) { + const applyCustomAttributesOnSpan = this._getConfig() + .applyCustomAttributesOnSpan; + if (applyCustomAttributesOnSpan) { + safeExecuteInTheMiddle( + () => applyCustomAttributesOnSpan(span, request, result), + error => { + if (!error) { + return; + } + + api.diag.error('applyCustomAttributesOnSpan', error); + }, + true + ); + } + } + /** * Prepares a span data - needed later for matching appropriate network * resources diff --git a/packages/opentelemetry-instrumentation-fetch/test/fetch.test.ts b/packages/opentelemetry-instrumentation-fetch/test/fetch.test.ts index f0f2065ef4..a9568f71cc 100644 --- a/packages/opentelemetry-instrumentation-fetch/test/fetch.test.ts +++ b/packages/opentelemetry-instrumentation-fetch/test/fetch.test.ts @@ -35,7 +35,11 @@ import { } from '@opentelemetry/web'; import * as assert from 'assert'; import * as sinon from 'sinon'; -import { FetchInstrumentation, FetchInstrumentationConfig } from '../src'; +import { + FetchInstrumentation, + FetchInstrumentationConfig, + FetchCustomAttributeFunction, +} from '../src'; import { AttributeNames } from '../src/enums/AttributeNames'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; @@ -57,6 +61,7 @@ const getData = (url: string, method?: string) => }, }); +const CUSTOM_ATTRIBUTE_KEY = 'span kind'; const defaultResource = { connectEnd: 15, connectStart: 13, @@ -576,6 +581,73 @@ describe('fetch', () => { }); }); + describe('applyCustomAttributesOnSpan option', () => { + const noop = () => {}; + const prepare = ( + url: string, + applyCustomAttributesOnSpan: FetchCustomAttributeFunction, + cb: VoidFunction = noop + ) => { + const propagateTraceHeaderCorsUrls = [url]; + + prepareData(cb, url, { + propagateTraceHeaderCorsUrls, + applyCustomAttributesOnSpan, + }); + }; + + afterEach(() => { + clearData(); + }); + + it('applies attributes when the request is succesful', done => { + prepare( + url, + span => { + span.setAttribute(CUSTOM_ATTRIBUTE_KEY, 'custom value'); + }, + () => { + const span: tracing.ReadableSpan = exportSpy.args[1][0][0]; + const attributes = span.attributes; + + assert.ok(attributes[CUSTOM_ATTRIBUTE_KEY] === 'custom value'); + done(); + } + ); + }); + + it('applies custom attributes when the request fails', done => { + prepare( + badUrl, + span => { + span.setAttribute(CUSTOM_ATTRIBUTE_KEY, 'custom value'); + }, + () => { + const span: tracing.ReadableSpan = exportSpy.args[1][0][0]; + const attributes = span.attributes; + + assert.ok(attributes[CUSTOM_ATTRIBUTE_KEY] === 'custom value'); + done(); + } + ); + }); + + it('has request and response objects in callback arguments', done => { + const applyCustomAttributes: FetchCustomAttributeFunction = ( + span, + request, + response + ) => { + assert.ok(request.method === 'GET'); + assert.ok(response.status === 200); + + done(); + }; + + prepare(url, applyCustomAttributes); + }); + }); + describe('when url is ignored', () => { beforeEach(done => { const propagateTraceHeaderCorsUrls = url;