diff --git a/packages/opentelemetry-core/src/context/propagation/composite.ts b/packages/opentelemetry-core/src/context/propagation/composite.ts index a5a8216e7..507b407f2 100644 --- a/packages/opentelemetry-core/src/context/propagation/composite.ts +++ b/packages/opentelemetry-core/src/context/propagation/composite.ts @@ -41,7 +41,7 @@ export class CompositePropagator implements TextMapPropagator { this._propagators // older propagators may not have fields function, null check to be sure .map(p => (typeof p.fields === 'function' ? p.fields() : [])) - .reduce((x, y) => x.concat(y)) + .reduce((x, y) => x.concat(y), []) ) ); } diff --git a/packages/opentelemetry-core/src/utils/environment.ts b/packages/opentelemetry-core/src/utils/environment.ts index c353a3867..e5e3ef084 100644 --- a/packages/opentelemetry-core/src/utils/environment.ts +++ b/packages/opentelemetry-core/src/utils/environment.ts @@ -43,7 +43,10 @@ function isEnvVarANumber(key: unknown): key is keyof ENVIRONMENT_NUMBERS { ); } -const ENVIRONMENT_LISTS_KEYS = ['OTEL_NO_PATCH_MODULES'] as const; +const ENVIRONMENT_LISTS_KEYS = [ + 'OTEL_NO_PATCH_MODULES', + 'OTEL_PROPAGATORS', +] as const; type ENVIRONMENT_LISTS = { [K in typeof ENVIRONMENT_LISTS_KEYS[number]]?: string[]; @@ -83,21 +86,22 @@ export const DEFAULT_ENVIRONMENT: Required = { HOSTNAME: '', KUBERNETES_SERVICE_HOST: '', NAMESPACE: '', + OTEL_BSP_EXPORT_TIMEOUT: 30000, + OTEL_BSP_MAX_EXPORT_BATCH_SIZE: 512, + OTEL_BSP_MAX_QUEUE_SIZE: 2048, + OTEL_BSP_SCHEDULE_DELAY: 5000, OTEL_EXPORTER_JAEGER_AGENT_HOST: '', OTEL_EXPORTER_JAEGER_ENDPOINT: '', OTEL_EXPORTER_JAEGER_PASSWORD: '', OTEL_EXPORTER_JAEGER_USER: '', OTEL_LOG_LEVEL: DiagLogLevel.INFO, OTEL_NO_PATCH_MODULES: [], + OTEL_PROPAGATORS: ['tracecontext', 'baggage'], OTEL_RESOURCE_ATTRIBUTES: '', OTEL_SAMPLING_PROBABILITY: 1, OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: 1000, OTEL_SPAN_EVENT_COUNT_LIMIT: 1000, OTEL_SPAN_LINK_COUNT_LIMIT: 1000, - OTEL_BSP_EXPORT_TIMEOUT: 30000, - OTEL_BSP_MAX_EXPORT_BATCH_SIZE: 512, - OTEL_BSP_MAX_QUEUE_SIZE: 2048, - OTEL_BSP_SCHEDULE_DELAY: 5000, }; /** diff --git a/packages/opentelemetry-node/package.json b/packages/opentelemetry-node/package.json index 785fddc71..99c422744 100644 --- a/packages/opentelemetry-node/package.json +++ b/packages/opentelemetry-node/package.json @@ -66,6 +66,8 @@ "@opentelemetry/context-async-hooks": "^0.18.2", "@opentelemetry/core": "^0.18.2", "@opentelemetry/tracing": "^0.18.2", + "@opentelemetry/propagator-b3": "^0.18.0", + "@opentelemetry/propagator-jaeger": "^0.18.0", "semver": "^7.1.3" } } diff --git a/packages/opentelemetry-node/src/NodeTracerProvider.ts b/packages/opentelemetry-node/src/NodeTracerProvider.ts index 19fb8976f..dd256689a 100644 --- a/packages/opentelemetry-node/src/NodeTracerProvider.ts +++ b/packages/opentelemetry-node/src/NodeTracerProvider.ts @@ -13,17 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +import { TextMapPropagator } from '@opentelemetry/api'; import { AsyncHooksContextManager, AsyncLocalStorageContextManager, } from '@opentelemetry/context-async-hooks'; +import { B3Propagator, B3InjectEncoding } from '@opentelemetry/propagator-b3'; import { BasicTracerProvider, + PROPAGATOR_FACTORY, SDKRegistrationConfig, } from '@opentelemetry/tracing'; import * as semver from 'semver'; import { NodeTracerConfig } from './config'; +import { JaegerHttpTracePropagator } from '@opentelemetry/propagator-jaeger'; /** * Register this TracerProvider for use with the OpenTelemetry API. @@ -33,6 +36,22 @@ import { NodeTracerConfig } from './config'; * @param config Configuration object for SDK registration */ export class NodeTracerProvider extends BasicTracerProvider { + protected static readonly _registeredPropagators = new Map< + string, + PROPAGATOR_FACTORY + >([ + [ + 'b3', + () => + new B3Propagator({ injectEncoding: B3InjectEncoding.SINGLE_HEADER }), + ], + [ + 'b3multi', + () => new B3Propagator({ injectEncoding: B3InjectEncoding.MULTI_HEADER }), + ], + ['jaeger', () => new JaegerHttpTracePropagator()], + ]); + constructor(config: NodeTracerConfig = {}) { super(config); } @@ -48,4 +67,11 @@ export class NodeTracerProvider extends BasicTracerProvider { super.register(config); } + + protected _getPropagator(name: string): TextMapPropagator | undefined { + return ( + super._getPropagator(name) || + NodeTracerProvider._registeredPropagators.get(name)?.() + ); + } } diff --git a/packages/opentelemetry-node/test/NodeTracerProvider.test.ts b/packages/opentelemetry-node/test/NodeTracerProvider.test.ts index 110ef6973..81fe63477 100644 --- a/packages/opentelemetry-node/test/NodeTracerProvider.test.ts +++ b/packages/opentelemetry-node/test/NodeTracerProvider.test.ts @@ -20,6 +20,7 @@ import { setSpan, setSpanContext, getSpan, + propagation, } from '@opentelemetry/api'; import { AlwaysOnSampler, AlwaysOffSampler } from '@opentelemetry/core'; import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; @@ -211,4 +212,37 @@ describe('NodeTracerProvider', () => { return patchedFn(); }); }); + + describe('.register()', () => { + let originalPropagators: string | number | undefined | string[]; + beforeEach(() => { + originalPropagators = process.env.OTEL_PROPAGATORS; + }); + + afterEach(() => { + // otherwise we may assign 'undefined' (a string) + if (originalPropagators !== undefined) { + (process.env as any).OTEL_PROPAGATORS = originalPropagators; + } else { + delete (process.env as any).OTEL_PROPAGATORS; + } + }); + + it('should allow propagators as per the specification', () => { + (process.env as any).OTEL_PROPAGATORS = 'b3,b3multi,jaeger'; + + const provider = new NodeTracerProvider(); + provider.register(); + + assert.deepStrictEqual(propagation.fields(), [ + 'b3', + 'x-b3-traceid', + 'x-b3-spanid', + 'x-b3-flags', + 'x-b3-sampled', + 'x-b3-parentspanid', + 'uber-trace-id', + ]); + }); + }); }); diff --git a/packages/opentelemetry-node/tsconfig.json b/packages/opentelemetry-node/tsconfig.json index 38e281f4b..353ee432d 100644 --- a/packages/opentelemetry-node/tsconfig.json +++ b/packages/opentelemetry-node/tsconfig.json @@ -15,6 +15,12 @@ { "path": "../opentelemetry-core" }, + { + "path": "../opentelemetry-propagator-b3" + }, + { + "path": "../opentelemetry-propagator-jaeger" + }, { "path": "../opentelemetry-resources" }, diff --git a/packages/opentelemetry-tracing/src/BasicTracerProvider.ts b/packages/opentelemetry-tracing/src/BasicTracerProvider.ts index 5eee7bea2..b5db06027 100644 --- a/packages/opentelemetry-tracing/src/BasicTracerProvider.ts +++ b/packages/opentelemetry-tracing/src/BasicTracerProvider.ts @@ -19,11 +19,14 @@ import { trace, context, propagation, + TextMapPropagator, + diag, } from '@opentelemetry/api'; import { CompositePropagator, - HttpBaggage, HttpTraceContext, + HttpBaggage, + getEnv, } from '@opentelemetry/core'; import { Resource } from '@opentelemetry/resources'; import { SpanProcessor, Tracer } from '.'; @@ -32,10 +35,21 @@ import { MultiSpanProcessor } from './MultiSpanProcessor'; import { NoopSpanProcessor } from './NoopSpanProcessor'; import { SDKRegistrationConfig, TracerConfig } from './types'; import merge = require('lodash.merge'); + +export type PROPAGATOR_FACTORY = () => TextMapPropagator; + /** * This class represents a basic tracer provider which platform libraries can extend */ export class BasicTracerProvider implements TracerProvider { + protected static readonly _registeredPropagators = new Map< + string, + PROPAGATOR_FACTORY + >([ + ['tracecontext', () => new HttpTraceContext()], + ['baggage', () => new HttpBaggage()], + ]); + private readonly _config: TracerConfig; private readonly _registeredSpanProcessors: SpanProcessor[] = []; private readonly _tracers: Map = new Map(); @@ -86,9 +100,7 @@ export class BasicTracerProvider implements TracerProvider { register(config: SDKRegistrationConfig = {}) { trace.setGlobalTracerProvider(this); if (config.propagator === undefined) { - config.propagator = new CompositePropagator({ - propagators: [new HttpBaggage(), new HttpTraceContext()], - }); + config.propagator = this._buildPropagatorFromEnv(); } if (config.contextManager) { @@ -103,4 +115,43 @@ export class BasicTracerProvider implements TracerProvider { shutdown() { return this.activeSpanProcessor.shutdown(); } + + protected _getPropagator(name: string): TextMapPropagator | undefined { + return BasicTracerProvider._registeredPropagators.get(name)?.(); + } + + protected _buildPropagatorFromEnv(): TextMapPropagator | undefined { + // per spec, propagators from env must be deduplicated + const uniquePropagatorNames = [...new Set(getEnv().OTEL_PROPAGATORS)]; + + const propagators = uniquePropagatorNames.map(name => { + const propagator = this._getPropagator(name); + if (!propagator) { + diag.warn( + `Propagator "${name}" requested through environment variable is unavailable.` + ); + } + + return propagator; + }); + const validPropagators = propagators.reduce( + (list, item) => { + if (item) { + list.push(item); + } + return list; + }, + [] + ); + + if (validPropagators.length === 0) { + return; + } else if (uniquePropagatorNames.length === 1) { + return validPropagators[0]; + } else { + return new CompositePropagator({ + propagators: validPropagators, + }); + } + } } diff --git a/packages/opentelemetry-tracing/test/BasicTracerProvider.test.ts b/packages/opentelemetry-tracing/test/BasicTracerProvider.test.ts index bfef16e09..981d7edb4 100644 --- a/packages/opentelemetry-tracing/test/BasicTracerProvider.test.ts +++ b/packages/opentelemetry-tracing/test/BasicTracerProvider.test.ts @@ -22,7 +22,14 @@ import { setSpan, setSpanContext, getSpan, + TextMapPropagator, + TextMapSetter, + Context, + TextMapGetter, + propagation, + diag, } from '@opentelemetry/api'; +import { CompositePropagator } from '@opentelemetry/core'; import { AlwaysOnSampler, AlwaysOffSampler, @@ -115,6 +122,99 @@ describe('BasicTracerProvider', () => { }); }); + describe('.register()', () => { + const envSource = (typeof window !== 'undefined' + ? window + : process.env) as any; + + describe('propagator', () => { + class DummyPropagator implements TextMapPropagator { + inject( + context: Context, + carrier: any, + setter: TextMapSetter + ): void { + throw new Error('Method not implemented.'); + } + extract( + context: Context, + carrier: any, + getter: TextMapGetter + ): Context { + throw new Error('Method not implemented.'); + } + fields(): string[] { + throw new Error('Method not implemented.'); + } + } + + let setGlobalPropagatorStub: sinon.SinonSpy< + [TextMapPropagator], + TextMapPropagator + >; + let originalPropagators: string | number | undefined | string[]; + beforeEach(() => { + setGlobalPropagatorStub = sinon.spy(propagation, 'setGlobalPropagator'); + originalPropagators = envSource.OTEL_PROPAGATORS; + }); + + afterEach(() => { + setGlobalPropagatorStub.restore(); + + // otherwise we may assign 'undefined' (a string) + if (originalPropagators !== undefined) { + envSource.OTEL_PROPAGATORS = originalPropagators; + } else { + delete envSource.OTEL_PROPAGATORS; + } + }); + + it('should be set to a given value if it it provided', () => { + const provider = new BasicTracerProvider(); + provider.register({ + propagator: new DummyPropagator(), + }); + assert.ok( + setGlobalPropagatorStub.calledOnceWithExactly( + sinon.match.instanceOf(DummyPropagator) + ) + ); + }); + + it('should be composite if 2 or more propagators provided in an environment variable', () => { + const provider = new BasicTracerProvider(); + provider.register(); + + assert.ok( + setGlobalPropagatorStub.calledOnceWithExactly( + sinon.match.instanceOf(CompositePropagator) + ) + ); + assert.deepStrictEqual(setGlobalPropagatorStub.args[0][0].fields(), [ + 'traceparent', + 'tracestate', + 'baggage', + ]); + }); + + it('warns if there is no propagator registered with a given name', () => { + const warnStub = sinon.spy(diag, 'warn'); + + envSource.OTEL_PROPAGATORS = 'missing-propagator'; + const provider = new BasicTracerProvider({}); + provider.register(); + + assert.ok( + warnStub.calledOnceWithExactly( + 'Propagator "missing-propagator" requested through environment variable is unavailable.' + ) + ); + + warnStub.restore(); + }); + }); + }); + describe('.startSpan()', () => { it('should start a span with name only', () => { const tracer = new BasicTracerProvider().getTracer('default');