diff --git a/packages/opentelemetry-core/src/context/propagation/composite.ts b/packages/opentelemetry-core/src/context/propagation/composite.ts new file mode 100644 index 0000000000..c1a2906658 --- /dev/null +++ b/packages/opentelemetry-core/src/context/propagation/composite.ts @@ -0,0 +1,78 @@ +/*! + * Copyright 2020, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Carrier, Context, HttpTextFormat, Logger } from '@opentelemetry/api'; +import { NoopLogger } from '../../common/NoopLogger'; +import { CompositePropagatorConfig } from './types'; + +/** Combines multiple propagators into a single propagator. */ +export class CompositePropagator implements HttpTextFormat { + private readonly _propagators: HttpTextFormat[]; + private readonly _logger: Logger; + + /** + * Construct a composite propagator from a list of propagators. + * + * @param [config] Configuration object for composite propagator + */ + constructor(config: CompositePropagatorConfig = {}) { + this._propagators = config.propagators ?? []; + this._logger = config.logger ?? new NoopLogger(); + } + + /** + * Run each of the configured propagators with the given context and carrier. + * Propagators are run in the order they are configured, so if multiple + * propagators write the same carrier key, the propagator later in the list + * will "win". + * + * @param context Context to inject + * @param carrier Carrier into which context will be injected + */ + inject(context: Context, carrier: Carrier) { + for (const propagator of this._propagators) { + try { + propagator.inject(context, carrier); + } catch (err) { + this._logger.warn( + `Failed to inject with ${propagator.constructor.name}. Err: ${err.message}` + ); + } + } + } + + /** + * Run each of the configured propagators with the given context and carrier. + * Propagators are run in the order they are configured, so if multiple + * propagators write the same context key, the propagator later in the list + * will "win". + * + * @param context Context to add values to + * @param carrier Carrier from which to extract context + */ + extract(context: Context, carrier: Carrier): Context { + return this._propagators.reduce((ctx, propagator) => { + try { + return propagator.extract(ctx, carrier); + } catch (err) { + this._logger.warn( + `Failed to inject with ${propagator.constructor.name}. Err: ${err.message}` + ); + } + return ctx; + }, context); + } +} diff --git a/packages/opentelemetry-core/src/context/propagation/types.ts b/packages/opentelemetry-core/src/context/propagation/types.ts new file mode 100644 index 0000000000..ff4a95a4f4 --- /dev/null +++ b/packages/opentelemetry-core/src/context/propagation/types.ts @@ -0,0 +1,30 @@ +/*! + * Copyright 2020, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { HttpTextFormat, Logger } from '@opentelemetry/api'; + +/** Configuration object for composite propagator */ +export interface CompositePropagatorConfig { + /** + * List of propagators to run. Propagators run in the + * list order. If a propagator later in the list writes the same context + * key as a propagator earlier in the list, the later on will "win". + */ + propagators?: HttpTextFormat[]; + + /** Instance of logger */ + logger?: Logger; +} diff --git a/packages/opentelemetry-core/src/index.ts b/packages/opentelemetry-core/src/index.ts index 61168c30bc..7fb711d43a 100644 --- a/packages/opentelemetry-core/src/index.ts +++ b/packages/opentelemetry-core/src/index.ts @@ -21,7 +21,9 @@ export * from './common/types'; export * from './version'; export * from './context/context'; export * from './context/propagation/B3Format'; +export * from './context/propagation/composite'; export * from './context/propagation/HttpTraceContext'; +export * from './context/propagation/types'; export * from './platform'; export * from './trace/instrumentation/BasePlugin'; export * from './trace/NoRecordingSpan'; diff --git a/packages/opentelemetry-core/test/context/composite.test.ts b/packages/opentelemetry-core/test/context/composite.test.ts new file mode 100644 index 0000000000..55b4b8ea8f --- /dev/null +++ b/packages/opentelemetry-core/test/context/composite.test.ts @@ -0,0 +1,160 @@ +/*! + * Copyright 2019, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Context } from '@opentelemetry/scope-base'; +import * as assert from 'assert'; +import { + CompositePropagator, + HttpTraceContext, + randomSpanId, + randomTraceId, +} from '../../src'; +import { + setExtractedSpanContext, + getExtractedSpanContext, +} from '../../src/context/context'; +import { + B3Format, + X_B3_SPAN_ID, + X_B3_TRACE_ID, + X_B3_SAMPLED, +} from '../../src/context/propagation/B3Format'; +import { + TRACE_PARENT_HEADER, + TRACE_STATE_HEADER, +} from '../../src/context/propagation/HttpTraceContext'; +import { TraceState } from '../../src/trace/TraceState'; +import { HttpTextFormat, SpanContext } from '@opentelemetry/api'; + +describe('Composite Propagator', () => { + let traceId: string; + let spanId: string; + + beforeEach(() => { + traceId = randomTraceId(); + spanId = randomSpanId(); + }); + + describe('inject', () => { + let carrier: { [key: string]: unknown }; + let spanContext: SpanContext; + let ctxWithSpanContext: Context; + + beforeEach(() => { + carrier = {}; + spanContext = { + spanId, + traceId, + traceFlags: 1, + traceState: new TraceState('foo=bar'), + }; + ctxWithSpanContext = setExtractedSpanContext( + Context.ROOT_CONTEXT, + spanContext + ); + }); + + it('should inject context using all configured propagators', () => { + const composite = new CompositePropagator({ + propagators: [new B3Format(), new HttpTraceContext()], + }); + composite.inject(ctxWithSpanContext, carrier); + + assert.strictEqual(carrier[X_B3_TRACE_ID], traceId); + assert.strictEqual(carrier[X_B3_SPAN_ID], spanId); + assert.strictEqual(carrier[X_B3_SAMPLED], 1); + assert.strictEqual( + carrier[TRACE_PARENT_HEADER], + `00-${traceId}-${spanId}-01` + ); + assert.strictEqual(carrier[TRACE_STATE_HEADER], 'foo=bar'); + }); + + it('should not throw', () => { + const composite = new CompositePropagator({ + propagators: [new ThrowingPropagator(), new HttpTraceContext()], + }); + composite.inject(ctxWithSpanContext, carrier); + + assert.strictEqual( + carrier[TRACE_PARENT_HEADER], + `00-${traceId}-${spanId}-01` + ); + }); + }); + + describe('extract', () => { + let carrier: { [key: string]: unknown }; + + beforeEach(() => { + carrier = { + [X_B3_TRACE_ID]: traceId, + [X_B3_SPAN_ID]: spanId, + [X_B3_SAMPLED]: 1, + [TRACE_PARENT_HEADER]: `00-${traceId}-${spanId}-01`, + [TRACE_STATE_HEADER]: 'foo=bar', + }; + }); + + it('should extract context using all configured propagators', () => { + const composite = new CompositePropagator({ + propagators: [new B3Format(), new HttpTraceContext()], + }); + const spanContext = getExtractedSpanContext( + composite.extract(Context.ROOT_CONTEXT, carrier) + ); + + if (!spanContext) { + throw new Error('no extracted context'); + } + + assert.strictEqual(spanContext.traceId, traceId); + assert.strictEqual(spanContext.spanId, spanId); + assert.strictEqual(spanContext.traceFlags, 1); + assert.strictEqual(spanContext.isRemote, true); + assert.strictEqual(spanContext.traceState!.get('foo'), 'bar'); + }); + + it('should not throw', () => { + const composite = new CompositePropagator({ + propagators: [new ThrowingPropagator(), new HttpTraceContext()], + }); + const spanContext = getExtractedSpanContext( + composite.extract(Context.ROOT_CONTEXT, carrier) + ); + + if (!spanContext) { + throw new Error('no extracted context'); + } + + assert.strictEqual(spanContext.traceId, traceId); + assert.strictEqual(spanContext.spanId, spanId); + assert.strictEqual(spanContext.traceFlags, 1); + assert.strictEqual(spanContext.isRemote, true); + assert.strictEqual(spanContext.traceState!.get('foo'), 'bar'); + }); + }); +}); + +class ThrowingPropagator implements HttpTextFormat { + inject(context: Context, carrier: unknown) { + throw new Error('this propagator throws'); + } + + extract(context: Context, carrier: unknown): Context { + throw new Error('This propagator throws'); + } +}