From 6b2a9b731fdb0b66dd6ab816c1fc533318117a81 Mon Sep 17 00:00:00 2001 From: Amir Blum Date: Wed, 13 May 2020 19:26:14 +0300 Subject: [PATCH] feat(plugin-http): add plugin hooks before processing req and res (#963) --- packages/opentelemetry-plugin-http/README.md | 2 + .../opentelemetry-plugin-http/src/http.ts | 35 +++++++++++++ .../opentelemetry-plugin-http/src/types.ts | 14 ++++- .../test/functionals/http-enable.test.ts | 51 +++++++++++++++++++ 4 files changed, 101 insertions(+), 1 deletion(-) diff --git a/packages/opentelemetry-plugin-http/README.md b/packages/opentelemetry-plugin-http/README.md index bcdc59e230e..247b50996a4 100644 --- a/packages/opentelemetry-plugin-http/README.md +++ b/packages/opentelemetry-plugin-http/README.md @@ -52,6 +52,8 @@ Http 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/master/packages/opentelemetry-plugin-http/src/types.ts#L52) | `HttpCustomAttributeFunction` | Function for adding custom attributes | +| [`requestHook`](https://github.com/open-telemetry/opentelemetry-js/blob/master/packages/opentelemetry-plugin-http/src/types.ts#L60) | `HttpRequestCustomAttributeFunction` | Function for adding custom attributes before request is handled | +| [`responseHook`](https://github.com/open-telemetry/opentelemetry-js/blob/master/packages/opentelemetry-plugin-http/src/types.ts#L67) | `HttpResponseCustomAttributeFunction` | Function for adding custom attributes before response is handled | | [`ignoreIncomingPaths`](https://github.com/open-telemetry/opentelemetry-js/blob/master/packages/opentelemetry-plugin-http/src/types.ts#L28) | `IgnoreMatcher[]` | Http plugin will not trace all incoming requests that match paths | | [`ignoreOutgoingUrls`](https://github.com/open-telemetry/opentelemetry-js/blob/master/packages/opentelemetry-plugin-http/src/types.ts#L28) | `IgnoreMatcher[]` | Http plugin will not trace all outgoing requests that match urls | | [`serverName`](https://github.com/open-telemetry/opentelemetry-js/blob/master/packages/opentelemetry-plugin-http/src/types.ts#L28) | `string` | The primary server name of the matched virtual host. | diff --git a/packages/opentelemetry-plugin-http/src/http.ts b/packages/opentelemetry-plugin-http/src/http.ts index b145b4a49eb..3697ba492ce 100644 --- a/packages/opentelemetry-plugin-http/src/http.ts +++ b/packages/opentelemetry-plugin-http/src/http.ts @@ -203,6 +203,9 @@ export class HttpPlugin extends BasePlugin { hostname, }); span.setAttributes(attributes); + if (this._config.requestHook) { + this._callRequestHook(span, request); + } request.on( 'response', @@ -212,6 +215,9 @@ export class HttpPlugin extends BasePlugin { { hostname } ); span.setAttributes(attributes); + if (this._config.responseHook) { + this._callResponseHook(span, response); + } this._tracer.bind(response); this._logger.debug('outgoingRequest on response()'); @@ -316,6 +322,13 @@ export class HttpPlugin extends BasePlugin { context.bind(request); context.bind(response); + if (plugin._config.requestHook) { + plugin._callRequestHook(span, request); + } + if (plugin._config.responseHook) { + plugin._callResponseHook(span, response); + } + // Wraps end (inspired by: // https://github.com/GoogleCloudPlatform/cloud-trace-nodejs/blob/master/src/plugins/plugin-connect.ts#L75) const originalEnd = response.end; @@ -465,6 +478,28 @@ export class HttpPlugin extends BasePlugin { this._spanNotEnded.delete(span); } + private _callResponseHook( + span: Span, + response: IncomingMessage | ServerResponse + ) { + this._safeExecute( + span, + () => this._config.responseHook!(span, response), + false + ); + } + + private _callRequestHook( + span: Span, + request: ClientRequest | IncomingMessage + ) { + this._safeExecute( + span, + () => this._config.requestHook!(span, request), + false + ); + } + private _safeExecute< T extends (...args: unknown[]) => ReturnType, K extends boolean diff --git a/packages/opentelemetry-plugin-http/src/types.ts b/packages/opentelemetry-plugin-http/src/types.ts index b7e230d37a1..da55e0fff89 100644 --- a/packages/opentelemetry-plugin-http/src/types.ts +++ b/packages/opentelemetry-plugin-http/src/types.ts @@ -57,6 +57,14 @@ export interface HttpCustomAttributeFunction { ): void; } +export interface HttpRequestCustomAttributeFunction { + (span: Span, request: ClientRequest | IncomingMessage): void; +} + +export interface HttpResponseCustomAttributeFunction { + (span: Span, response: IncomingMessage | ServerResponse): void; +} + /** * Options available for the HTTP Plugin (see [documentation](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-http#http-plugin-options)) */ @@ -65,8 +73,12 @@ export interface HttpPluginConfig extends PluginConfig { ignoreIncomingPaths?: IgnoreMatcher[]; /** Not trace all outgoing requests that match urls */ ignoreOutgoingUrls?: IgnoreMatcher[]; - /** Function for adding custom attributes */ + /** Function for adding custom attributes after response is handled */ applyCustomAttributesOnSpan?: HttpCustomAttributeFunction; + /** Function for adding custom attributes before request is handled */ + requestHook?: HttpRequestCustomAttributeFunction; + /** Function for adding custom attributes before response is handled */ + responseHook?: HttpResponseCustomAttributeFunction; /** The primary server name of the matched virtual host. */ serverName?: string; /** Require parent to create span for outgoing requests */ diff --git a/packages/opentelemetry-plugin-http/test/functionals/http-enable.test.ts b/packages/opentelemetry-plugin-http/test/functionals/http-enable.test.ts index 1fec14144f6..51400d239cf 100644 --- a/packages/opentelemetry-plugin-http/test/functionals/http-enable.test.ts +++ b/packages/opentelemetry-plugin-http/test/functionals/http-enable.test.ts @@ -40,6 +40,7 @@ import { DummyPropagation } from '../utils/DummyPropagation'; import { httpRequest } from '../utils/httpRequest'; import { ContextManager } from '@opentelemetry/context-base'; import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; +import { ClientRequest, IncomingMessage, ServerResponse } from 'http'; const applyCustomAttributesOnSpanErrorMessage = 'bad applyCustomAttributesOnSpan function'; @@ -76,6 +77,20 @@ export const customAttributeFunction = (span: ISpan): void => { span.setAttribute('span kind', SpanKind.CLIENT); }; +export const requestHookFunction = ( + span: ISpan, + request: ClientRequest | IncomingMessage +): void => { + span.setAttribute('custom request hook attribute', 'request'); +}; + +export const responseHookFunction = ( + span: ISpan, + response: IncomingMessage | ServerResponse +): void => { + span.setAttribute('custom response hook attribute', 'response'); +}; + describe('HttpPlugin', () => { let contextManager: ContextManager; @@ -207,6 +222,8 @@ describe('HttpPlugin', () => { (url: string) => url.endsWith(`/ignored/function`), ], applyCustomAttributesOnSpan: customAttributeFunction, + requestHook: requestHookFunction, + responseHook: responseHookFunction, serverName, }; plugin.enable(http, provider, provider.logger, config); @@ -703,6 +720,40 @@ describe('HttpPlugin', () => { }); req.end(); }); + + it('custom attributes should show up on client and server spans', async () => { + await httpRequest.get( + `${protocol}://${hostname}:${serverPort}${pathname}` + ); + const spans = memoryExporter.getFinishedSpans(); + const [incomingSpan, outgoingSpan] = spans; + + assert.strictEqual( + incomingSpan.attributes['custom request hook attribute'], + 'request' + ); + assert.strictEqual( + incomingSpan.attributes['custom response hook attribute'], + 'response' + ); + assert.strictEqual( + incomingSpan.attributes['span kind'], + SpanKind.CLIENT + ); + + assert.strictEqual( + outgoingSpan.attributes['custom request hook attribute'], + 'request' + ); + assert.strictEqual( + outgoingSpan.attributes['custom response hook attribute'], + 'response' + ); + assert.strictEqual( + outgoingSpan.attributes['span kind'], + SpanKind.CLIENT + ); + }); }); describe('with require parent span', () => {