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;