diff --git a/experimental/packages/opentelemetry-instrumentation-http/README.md b/experimental/packages/opentelemetry-instrumentation-http/README.md index 340d658b241..85579bcac15 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/README.md +++ b/experimental/packages/opentelemetry-instrumentation-http/README.md @@ -51,7 +51,9 @@ Http instrumentation has few options available to choose from. You can set the f | [`startIncomingSpanHook`](https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-instrumentation-http/src/types.ts#L97) | `StartIncomingSpanCustomAttributeFunction` | Function for adding custom attributes before a span is started in incomingRequest | | [`startOutgoingSpanHook`](https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-instrumentation-http/src/types.ts#L99) | `StartOutgoingSpanCustomAttributeFunction` | Function for adding custom attributes before a span is started in outgoingRequest | | [`ignoreIncomingPaths`](https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-instrumentation-http/src/types.ts#L87) | `IgnoreMatcher[]` | Http instrumentation will not trace all incoming requests that match paths | +| `ignoreIncomingRequestHook` | `IgnoreIncomingRequestFunction[]` | Http instrumentation will not trace all incoming requests that matched with custom function | | [`ignoreOutgoingUrls`](https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-instrumentation-http/src/types.ts#L89) | `IgnoreMatcher[]` | Http instrumentation will not trace all outgoing requests that match urls | +| `ignoreOutgoingRequestHook` | `IgnoreOutgoingRequestFunction[]` | Http instrumentation will not trace all outgoing requests that matched with custom function | | [`serverName`](https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-instrumentation-http/src/types.ts#L101) | `string` | The primary server name of the matched virtual host. | | [`requireParentforOutgoingSpans`](https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-instrumentation-http/src/types.ts#L103) | Boolean | Require that is a parent span to create new span for outgoing requests. | | [`requireParentforIncomingSpans`](https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-instrumentation-http/src/types.ts#L105) | Boolean | Require that is a parent span to create new span for incoming requests. | diff --git a/experimental/packages/opentelemetry-instrumentation-http/src/http.ts b/experimental/packages/opentelemetry-instrumentation-http/src/http.ts index e183d5e5e65..23477b5340e 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/src/http.ts +++ b/experimental/packages/opentelemetry-instrumentation-http/src/http.ts @@ -382,7 +382,12 @@ export class HttpInstrumentation extends InstrumentationBase { utils.isIgnored( pathname, instrumentation._getConfig().ignoreIncomingPaths, - (e: Error) => instrumentation._diag.error('caught ignoreIncomingPaths error: ', e) + (e: unknown) => instrumentation._diag.error('caught ignoreIncomingPaths error: ', e) + ) || + safeExecuteInTheMiddle( + () => instrumentation._getConfig().ignoreIncomingRequestHook?.(request), + () => {}, + true ) ) { return context.with(suppressTracing(context.active()), () => { @@ -534,7 +539,12 @@ export class HttpInstrumentation extends InstrumentationBase { utils.isIgnored( origin + pathname, instrumentation._getConfig().ignoreOutgoingUrls, - (e: Error) => instrumentation._diag.error('caught ignoreOutgoingUrls error: ', e) + (e: unknown) => instrumentation._diag.error('caught ignoreOutgoingUrls error: ', e) + ) || + safeExecuteInTheMiddle( + () => instrumentation._getConfig().ignoreOutgoingRequestHook?.(optionsParsed), + () => {}, + true ) ) { return original.apply(this, [optionsParsed, ...args]); diff --git a/experimental/packages/opentelemetry-instrumentation-http/src/types.ts b/experimental/packages/opentelemetry-instrumentation-http/src/types.ts index 79f4e844cd2..fdb26147fce 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/src/types.ts +++ b/experimental/packages/opentelemetry-instrumentation-http/src/types.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { +import { Span, SpanAttributes, } from '@opentelemetry/api'; @@ -63,6 +63,14 @@ export interface HttpCustomAttributeFunction { ): void; } +export interface IgnoreIncomingRequestFunction { + (request: IncomingMessage ): boolean; +} + +export interface IgnoreOutgoingRequestFunction { + (request: RequestOptions ): boolean; +} + export interface HttpRequestCustomAttributeFunction { (span: Span, request: ClientRequest | IncomingMessage): void; } @@ -85,8 +93,12 @@ export interface StartOutgoingSpanCustomAttributeFunction { export interface HttpInstrumentationConfig extends InstrumentationConfig { /** Not trace all incoming requests that match paths */ ignoreIncomingPaths?: IgnoreMatcher[]; + /** Not trace all incoming requests that matched with custom function */ + ignoreIncomingRequestHook?: IgnoreIncomingRequestFunction; /** Not trace all outgoing requests that match urls */ ignoreOutgoingUrls?: IgnoreMatcher[]; + /** Not trace all outgoing requests that matched with custom function */ + ignoreOutgoingRequestHook?: IgnoreOutgoingRequestFunction; /** Function for adding custom attributes after response is handled */ applyCustomAttributesOnSpan?: HttpCustomAttributeFunction; /** Function for adding custom attributes before request is handled */ diff --git a/experimental/packages/opentelemetry-instrumentation-http/src/utils.ts b/experimental/packages/opentelemetry-instrumentation-http/src/utils.ts index 4f8a03c7af8..e1393b242f4 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/src/utils.ts +++ b/experimental/packages/opentelemetry-instrumentation-http/src/utils.ts @@ -129,7 +129,7 @@ export const satisfiesPattern = ( export const isIgnored = ( constant: string, list?: IgnoreMatcher[], - onException?: (error: Error) => void + onException?: (error: unknown) => void ): boolean => { if (!list) { // No ignored urls - trace everything @@ -304,18 +304,18 @@ export const getRequestInfo = ( }`; } - if (hasExpectHeader(optionsParsed)) { - optionsParsed.headers = Object.assign({}, optionsParsed.headers); - } else if (!optionsParsed.headers) { - optionsParsed.headers = {}; - } + const headers = optionsParsed.headers ?? {}; + optionsParsed.headers = Object.keys(headers).reduce((normalizedHeader, key) => { + normalizedHeader[key.toLowerCase()] = headers[key]; + return normalizedHeader; + }, {} as OutgoingHttpHeaders); // some packages return method in lowercase.. // ensure upperCase for consistency const method = optionsParsed.method ? optionsParsed.method.toUpperCase() : 'GET'; - return { origin, pathname, method, optionsParsed }; + return { origin, pathname, method, optionsParsed, }; }; /** @@ -501,7 +501,7 @@ export function headerCapture(type: 'request' | 'response', headers: string[]) { return (span: Span, getHeader: (key: string) => undefined | string | string[] | number) => { for (const [capturedHeader, normalizedHeader] of normalizedHeaders) { const value = getHeader(capturedHeader); - + if (value === undefined) { continue; } diff --git a/experimental/packages/opentelemetry-instrumentation-http/test/functionals/http-enable.test.ts b/experimental/packages/opentelemetry-instrumentation-http/test/functionals/http-enable.test.ts index 72c1ca5a1f7..38c549b7e39 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/test/functionals/http-enable.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-http/test/functionals/http-enable.test.ts @@ -18,7 +18,7 @@ import { context, propagation, Span as ISpan, - SpanKind, + SpanKind, trace, SpanAttributes, } from '@opentelemetry/api'; @@ -142,11 +142,17 @@ describe('HttpInstrumentation', () => { throw new Error('bad ignoreIncomingPaths function'); }, ], + ignoreIncomingRequestHook: _request => { + throw new Error('bad ignoreIncomingRequestHook function'); + }, ignoreOutgoingUrls: [ (url: string) => { throw new Error('bad ignoreOutgoingUrls function'); }, ], + ignoreOutgoingRequestHook: _request => { + throw new Error('bad ignoreOutgoingRequestHook function'); + }, applyCustomAttributesOnSpan: () => { throw new Error(applyCustomAttributesOnSpanErrorMessage); }, @@ -167,7 +173,12 @@ describe('HttpInstrumentation', () => { it('should generate valid spans (client side and server side)', async () => { const result = await httpRequest.get( - `${protocol}://${hostname}:${serverPort}${pathname}` + `${protocol}://${hostname}:${serverPort}${pathname}`, + { + headers: { + 'user-agent': 'tester' + } + } ); const spans = memoryExporter.getFinishedSpans(); const [incomingSpan, outgoingSpan] = spans; @@ -207,11 +218,20 @@ describe('HttpInstrumentation', () => { /\/ignored\/regexp$/i, (url: string) => url.endsWith('/ignored/function'), ], + ignoreIncomingRequestHook: request => { + return request.headers['user-agent']?.match('ignored-string') != null; + }, ignoreOutgoingUrls: [ `${protocol}://${hostname}:${serverPort}/ignored/string`, /\/ignored\/regexp$/i, (url: string) => url.endsWith('/ignored/function'), ], + ignoreOutgoingRequestHook: request => { + if (request.headers?.['user-agent'] != null) { + return `${request.headers['user-agent']}`.match('ignored-string') != null; + } + return false; + }, applyCustomAttributesOnSpan: customAttributeFunction, requestHook: requestHookFunction, responseHook: responseHookFunction, @@ -447,7 +467,7 @@ describe('HttpInstrumentation', () => { }); for (const ignored of ['string', 'function', 'regexp']) { - it(`should not trace ignored requests (client and server side) with type ${ignored}`, async () => { + it(`should not trace ignored requests with paths (client and server side) with type ${ignored}`, async () => { const testPath = `/ignored/${ignored}`; await httpRequest.get( @@ -458,6 +478,31 @@ describe('HttpInstrumentation', () => { }); } + it('should not trace ignored requests with headers (client and server side)', async () => { + const testValue = 'ignored-string'; + + await Promise.all([ + httpRequest.get( + `${protocol}://${hostname}:${serverPort}`, + { + headers: { + 'user-agent': testValue + } + } + ), + httpRequest.get( + `${protocol}://${hostname}:${serverPort}`, + { + headers: { + 'uSeR-aGeNt': testValue + } + } + ) + ]); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + }); + for (const arg of ['string', {}, new Date()]) { it(`should be tracable and not throw exception in ${protocol} instrumentation when passing the following argument ${JSON.stringify( arg @@ -507,7 +552,7 @@ describe('HttpInstrumentation', () => { hostname: 'localhost', pathname: '/', forceStatus: { - code: SpanStatusCode.ERROR, + code: SpanStatusCode.ERROR, message: err.message, }, component: 'http', diff --git a/experimental/packages/opentelemetry-instrumentation-http/test/functionals/https-enable.test.ts b/experimental/packages/opentelemetry-instrumentation-http/test/functionals/https-enable.test.ts index 46bf516e136..2611642b4c0 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/test/functionals/https-enable.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-http/test/functionals/https-enable.test.ts @@ -112,11 +112,17 @@ describe('HttpsInstrumentation', () => { throw new Error('bad ignoreIncomingPaths function'); }, ], + ignoreIncomingRequestHook: _request => { + throw new Error('bad ignoreIncomingRequestHook function'); + }, ignoreOutgoingUrls: [ (url: string) => { throw new Error('bad ignoreOutgoingUrls function'); }, ], + ignoreOutgoingRequestHook: _request => { + throw new Error('bad ignoreOutgoingRequestHook function'); + }, applyCustomAttributesOnSpan: () => { throw new Error(applyCustomAttributesOnSpanErrorMessage); }, @@ -142,7 +148,12 @@ describe('HttpsInstrumentation', () => { it('should generate valid spans (client side and server side)', async () => { const result = await httpsRequest.get( - `${protocol}://${hostname}:${serverPort}${pathname}` + `${protocol}://${hostname}:${serverPort}${pathname}`, + { + headers: { + 'user-agent': 'tester' + } + } ); const spans = memoryExporter.getFinishedSpans(); const [incomingSpan, outgoingSpan] = spans; @@ -181,11 +192,20 @@ describe('HttpsInstrumentation', () => { /\/ignored\/regexp$/i, (url: string) => url.endsWith('/ignored/function'), ], + ignoreIncomingRequestHook: request => { + return request.headers['user-agent']?.match('ignored-string') != null; + }, ignoreOutgoingUrls: [ `${protocol}://${hostname}:${serverPort}/ignored/string`, /\/ignored\/regexp$/i, (url: string) => url.endsWith('/ignored/function'), ], + ignoreOutgoingRequestHook: request => { + if (request.headers?.['user-agent'] != null) { + return `${request.headers['user-agent']}`.match('ignored-string') != null; + } + return false; + }, applyCustomAttributesOnSpan: customAttributeFunction, serverName, }); @@ -412,7 +432,7 @@ describe('HttpsInstrumentation', () => { }); for (const ignored of ['string', 'function', 'regexp']) { - it(`should not trace ignored requests (client and server side) with type ${ignored}`, async () => { + it(`should not trace ignored requests with paths (client and server side) with type ${ignored}`, async () => { const testPath = `/ignored/${ignored}`; await httpsRequest.get( @@ -423,6 +443,31 @@ describe('HttpsInstrumentation', () => { }); } + it('should not trace ignored requests with headers (client and server side)', async () => { + const testValue = 'ignored-string'; + + await Promise.all([ + httpsRequest.get( + `${protocol}://${hostname}:${serverPort}`, + { + headers: { + 'user-agent': testValue + } + } + ), + httpsRequest.get( + `${protocol}://${hostname}:${serverPort}`, + { + headers: { + 'uSeR-aGeNt': testValue + } + } + ) + ]); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + }); + for (const arg of ['string', {}, new Date()]) { it(`should be tracable and not throw exception in ${protocol} instrumentation when passing the following argument ${JSON.stringify( arg diff --git a/experimental/packages/opentelemetry-instrumentation-http/test/functionals/utils.test.ts b/experimental/packages/opentelemetry-instrumentation-http/test/functionals/utils.test.ts index 7b8a2e8d1ef..35e68f4211c 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/test/functionals/utils.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-http/test/functionals/utils.test.ts @@ -170,7 +170,7 @@ describe('Utility', () => { }); it('should not re-throw when function throws an exception', () => { - const onException = (e: Error) => { + const onException = (e: unknown) => { // Do nothing }; for (const callback of [undefined, onException]) { @@ -480,7 +480,7 @@ describe('Utility', () => { assert.strictEqual(attributes[SemanticAttributes.HTTP_ROUTE], undefined) }); }); - + describe('headers to span attributes capture', () => { let span: Span;