diff --git a/packages/opentelemetry-plugin-http/README.md b/packages/opentelemetry-plugin-http/README.md index 8daf06f844..bcdc59e230 100644 --- a/packages/opentelemetry-plugin-http/README.md +++ b/packages/opentelemetry-plugin-http/README.md @@ -55,6 +55,8 @@ Http plugin has few options available to choose from. You can set the following: | [`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. | +| `requireParentforOutgoingSpans` | Boolean | Require that is a parent span to create new span for outgoing requests. | +| `requireParentforIncomingSpans` | Boolean | Require that is a parent span to create new span for incoming requests. | ## Useful links - For more information on OpenTelemetry, visit: diff --git a/packages/opentelemetry-plugin-http/src/http.ts b/packages/opentelemetry-plugin-http/src/http.ts index 51263cc89e..c6c9aaa933 100644 --- a/packages/opentelemetry-plugin-http/src/http.ts +++ b/packages/opentelemetry-plugin-http/src/http.ts @@ -22,8 +22,14 @@ import { SpanKind, SpanOptions, Status, + SpanContext, + TraceFlags, } from '@opentelemetry/api'; -import { BasePlugin } from '@opentelemetry/core'; +import { + BasePlugin, + NoRecordingSpan, + getExtractedSpanContext, +} from '@opentelemetry/core'; import { ClientRequest, IncomingMessage, @@ -57,6 +63,12 @@ export class HttpPlugin extends BasePlugin { /** keep track on spans not ended */ private readonly _spanNotEnded: WeakSet; + private readonly _emptySpanContext: SpanContext = { + traceId: '', + spanId: '', + traceFlags: TraceFlags.NONE, + }; + constructor(readonly moduleName: string, readonly version: string) { super(`@opentelemetry/plugin-${moduleName}`, VERSION); // For now component is equal to moduleName but it can change in the future. @@ -396,7 +408,6 @@ export class HttpPlugin extends BasePlugin { const spanOptions: SpanOptions = { kind: SpanKind.CLIENT, }; - const span = plugin._startHttpSpan(operationName, spanOptions); return plugin._tracer.withSpan(span, () => { @@ -417,9 +428,26 @@ export class HttpPlugin extends BasePlugin { } private _startHttpSpan(name: string, options: SpanOptions) { - const span = this._tracer - .startSpan(name, options) - .setAttribute(AttributeNames.COMPONENT, this.component); + /* + * If a parent is required but not present, we use a `NoRecordingSpan` to still + * propagate context without recording it. + */ + const requireParent = + options.kind === SpanKind.CLIENT + ? this._config.requireParentforOutgoingSpans + : this._config.requireParentforIncomingSpans; + let span: Span; + if (requireParent === true && this._tracer.getCurrentSpan() === undefined) { + const spanContext = + getExtractedSpanContext(context.active()) ?? plugin._emptySpanContext; + // TODO: Refactor this when a solution is found in + // https://github.com/open-telemetry/opentelemetry-specification/issues/530 + span = new NoRecordingSpan(spanContext); + } else { + span = this._tracer + .startSpan(name, options) + .setAttribute(AttributeNames.COMPONENT, this.component); + } this._spanNotEnded.add(span); return span; } diff --git a/packages/opentelemetry-plugin-http/src/types.ts b/packages/opentelemetry-plugin-http/src/types.ts index 335e0bbc30..b7e230d37a 100644 --- a/packages/opentelemetry-plugin-http/src/types.ts +++ b/packages/opentelemetry-plugin-http/src/types.ts @@ -69,6 +69,10 @@ export interface HttpPluginConfig extends PluginConfig { applyCustomAttributesOnSpan?: HttpCustomAttributeFunction; /** The primary server name of the matched virtual host. */ serverName?: string; + /** Require parent to create span for outgoing requests */ + requireParentforOutgoingSpans?: boolean; + /** Require parent to create span for incoming requests */ + requireParentforIncomingSpans?: boolean; } export interface Err extends Error { 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 10fa530ba3..7197a7410d 100644 --- a/packages/opentelemetry-plugin-http/test/functionals/http-enable.test.ts +++ b/packages/opentelemetry-plugin-http/test/functionals/http-enable.test.ts @@ -696,5 +696,125 @@ describe('HttpPlugin', () => { req.end(); }); }); + + describe('with require parent span', () => { + beforeEach(done => { + memoryExporter.reset(); + plugin.enable(http, provider, provider.logger, {}); + server = http.createServer((request, response) => { + response.end('Test Server Response'); + }); + server.listen(serverPort, done); + }); + + afterEach(() => { + server.close(); + plugin.disable(); + }); + + it(`should not trace without parent with options enabled (both client & server)`, async () => { + plugin.disable(); + const config: HttpPluginConfig = { + requireParentforIncomingSpans: true, + requireParentforOutgoingSpans: true, + }; + plugin.enable(http, provider, provider.logger, config); + const testPath = `/test/test`; + await httpRequest.get( + `${protocol}://${hostname}:${serverPort}${testPath}` + ); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + }); + + it(`should not trace without parent with options enabled (client only)`, async () => { + plugin.disable(); + const config: HttpPluginConfig = { + requireParentforOutgoingSpans: true, + }; + plugin.enable(http, provider, provider.logger, config); + const testPath = `/test/test`; + const result = await httpRequest.get( + `${protocol}://${hostname}:${serverPort}${testPath}` + ); + assert( + result.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY] !== undefined + ); + assert( + result.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY] !== undefined + ); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual( + spans.every(span => span.kind === SpanKind.SERVER), + true + ); + }); + + it(`should not trace without parent with options enabled (server only)`, async () => { + plugin.disable(); + const config: HttpPluginConfig = { + requireParentforIncomingSpans: true, + }; + plugin.enable(http, provider, provider.logger, config); + const testPath = `/test/test`; + const result = await httpRequest.get( + `${protocol}://${hostname}:${serverPort}${testPath}` + ); + assert( + result.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY] !== undefined + ); + assert( + result.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY] !== undefined + ); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual( + spans.every(span => span.kind === SpanKind.CLIENT), + true + ); + }); + + it(`should trace with parent with both requireParent options enabled`, done => { + plugin.disable(); + const config: HttpPluginConfig = { + requireParentforIncomingSpans: true, + requireParentforOutgoingSpans: true, + }; + plugin.enable(http, provider, provider.logger, config); + const testPath = `/test/test`; + const tracer = provider.getTracer('default'); + const span = tracer.startSpan('parentSpan', { + kind: SpanKind.INTERNAL, + }); + tracer.withSpan(span, () => { + httpRequest + .get(`${protocol}://${hostname}:${serverPort}${testPath}`) + .then(result => { + span.end(); + assert( + result.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY] !== + undefined + ); + assert( + result.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY] !== + undefined + ); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 2); + assert.strictEqual( + spans.filter(span => span.kind === SpanKind.CLIENT).length, + 1 + ); + assert.strictEqual( + spans.filter(span => span.kind === SpanKind.INTERNAL).length, + 1 + ); + return done(); + }) + .catch(done); + }); + }); + }); }); });