diff --git a/api/README.md b/api/README.md index 2e8d75cd41..46ca4fd212 100644 --- a/api/README.md +++ b/api/README.md @@ -91,6 +91,10 @@ const meterProvider = new MeterProvider({ api.metrics.setGlobalMeterProvider(meterProvider); ``` +## Version Compatibility + +Because the npm installer and node module resolution algorithm could potentially allow two or more copies of any given package to exist within the same `node_modules` structure, the OpenTelemetry API takes advantage of a variable on the `global` object to store the global API. When an API method in the API package is called, it checks if this `global` API exists and proxies calls to it if and only if it is a compatible API version. This means if a package has a dependency on an OpenTelemetry API version which is not compatible with the API used by the end user, the package will receive a no-op implementation of the API. + ## Advanced Use ### API Registration Options diff --git a/api/src/api/context.ts b/api/src/api/context.ts index 3c5f7f0ea7..b127917990 100644 --- a/api/src/api/context.ts +++ b/api/src/api/context.ts @@ -15,17 +15,24 @@ */ import { + Context, ContextManager, NoopContextManager, - Context, } from '@opentelemetry/context-base'; +import { + API_BACKWARDS_COMPATIBILITY_VERSION, + GLOBAL_CONTEXT_MANAGER_API_KEY, + makeGetter, + _global, +} from './global-utils'; + +const NOOP_CONTEXT_MANAGER = new NoopContextManager(); /** * Singleton object which represents the entry point to the OpenTelemetry Context API */ export class ContextAPI { private static _instance?: ContextAPI; - private _contextManager: ContextManager = new NoopContextManager(); /** Empty private constructor prevents end users from constructing a new instance of the API */ private constructor() {} @@ -45,7 +52,17 @@ export class ContextAPI { public setGlobalContextManager( contextManager: ContextManager ): ContextManager { - this._contextManager = contextManager; + if (_global[GLOBAL_CONTEXT_MANAGER_API_KEY]) { + // global context manager has already been set + return this._getContextManager(); + } + + _global[GLOBAL_CONTEXT_MANAGER_API_KEY] = makeGetter( + API_BACKWARDS_COMPATIBILITY_VERSION, + contextManager, + NOOP_CONTEXT_MANAGER + ); + return contextManager; } @@ -53,7 +70,7 @@ export class ContextAPI { * Get the currently active context */ public active(): Context { - return this._contextManager.active(); + return this._getContextManager().active(); } /** @@ -66,7 +83,7 @@ export class ContextAPI { context: Context, fn: T ): ReturnType { - return this._contextManager.with(context, fn); + return this._getContextManager().with(context, fn); } /** @@ -76,6 +93,20 @@ export class ContextAPI { * @param context context to bind to the event emitter or function. Defaults to the currently active context */ public bind(target: T, context: Context = this.active()): T { - return this._contextManager.bind(target, context); + return this._getContextManager().bind(target, context); + } + + private _getContextManager(): ContextManager { + return ( + _global[GLOBAL_CONTEXT_MANAGER_API_KEY]?.( + API_BACKWARDS_COMPATIBILITY_VERSION + ) ?? NOOP_CONTEXT_MANAGER + ); + } + + /** Disable and remove the global context manager */ + public disable() { + this._getContextManager().disable(); + delete _global[GLOBAL_CONTEXT_MANAGER_API_KEY]; } } diff --git a/api/src/api/global-utils.ts b/api/src/api/global-utils.ts new file mode 100644 index 0000000000..3114d1b6d0 --- /dev/null +++ b/api/src/api/global-utils.ts @@ -0,0 +1,67 @@ +/*! + * 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 { ContextManager } from '@opentelemetry/context-base'; +import { HttpTextPropagator } from '../context/propagation/HttpTextPropagator'; +import { MeterProvider } from '../metrics/MeterProvider'; +import { TracerProvider } from '../trace/tracer_provider'; + +export const GLOBAL_CONTEXT_MANAGER_API_KEY = Symbol.for( + 'io.opentelemetry.js.api.context' +); +export const GLOBAL_METRICS_API_KEY = Symbol.for( + 'io.opentelemetry.js.api.metrics' +); +export const GLOBAL_PROPAGATION_API_KEY = Symbol.for( + 'io.opentelemetry.js.api.propagation' +); +export const GLOBAL_TRACE_API_KEY = Symbol.for('io.opentelemetry.js.api.trace'); + +type Get = (version: number) => T; +type MyGlobals = Partial<{ + [GLOBAL_CONTEXT_MANAGER_API_KEY]: Get; + [GLOBAL_METRICS_API_KEY]: Get; + [GLOBAL_PROPAGATION_API_KEY]: Get; + [GLOBAL_TRACE_API_KEY]: Get; +}>; + +export const _global = global as typeof global & MyGlobals; + +/** + * Make a function which accepts a version integer and returns the instance of an API if the version + * is compatible, or a fallback version (usually NOOP) if it is not. + * + * @param requiredVersion Backwards compatibility version which is required to return the instance + * @param instance Instance which should be returned if the required version is compatible + * @param fallback Fallback instance, usually NOOP, which will be returned if the required version is not compatible + */ +export function makeGetter( + requiredVersion: number, + instance: T, + fallback: T +): Get { + return (version: number): T => + version === requiredVersion ? instance : fallback; +} + +/** + * A number which should be incremented each time a backwards incompatible + * change is made to the API. This number is used when an API package + * attempts to access the global API to ensure it is getting a compatible + * version. If the global API is not compatible with the API package + * attempting to get it, a NOOP API implementation will be returned. + */ +export const API_BACKWARDS_COMPATIBILITY_VERSION = 0; diff --git a/api/src/api/metrics.ts b/api/src/api/metrics.ts index 118c20db99..dedf58be54 100644 --- a/api/src/api/metrics.ts +++ b/api/src/api/metrics.ts @@ -17,13 +17,18 @@ import { Meter } from '../metrics/Meter'; import { MeterProvider } from '../metrics/MeterProvider'; import { NOOP_METER_PROVIDER } from '../metrics/NoopMeterProvider'; +import { + API_BACKWARDS_COMPATIBILITY_VERSION, + GLOBAL_METRICS_API_KEY, + makeGetter, + _global, +} from './global-utils'; /** * Singleton object which represents the entry point to the OpenTelemetry Metrics API */ export class MetricsAPI { private static _instance?: MetricsAPI; - private _meterProvider: MeterProvider = NOOP_METER_PROVIDER; /** Empty private constructor prevents end users from constructing a new instance of the API */ private constructor() {} @@ -41,7 +46,17 @@ export class MetricsAPI { * Set the current global meter. Returns the initialized global meter provider. */ public setGlobalMeterProvider(provider: MeterProvider): MeterProvider { - this._meterProvider = provider; + if (_global[GLOBAL_METRICS_API_KEY]) { + // global meter provider has already been set + return this.getMeterProvider(); + } + + _global[GLOBAL_METRICS_API_KEY] = makeGetter( + API_BACKWARDS_COMPATIBILITY_VERSION, + provider, + NOOP_METER_PROVIDER + ); + return provider; } @@ -49,7 +64,10 @@ export class MetricsAPI { * Returns the global meter provider. */ public getMeterProvider(): MeterProvider { - return this._meterProvider; + return ( + _global[GLOBAL_METRICS_API_KEY]?.(API_BACKWARDS_COMPATIBILITY_VERSION) ?? + NOOP_METER_PROVIDER + ); } /** @@ -58,4 +76,9 @@ export class MetricsAPI { public getMeter(name: string, version?: string): Meter { return this.getMeterProvider().getMeter(name, version); } + + /** Remove the global meter provider */ + public disable() { + delete _global[GLOBAL_METRICS_API_KEY]; + } } diff --git a/api/src/api/propagation.ts b/api/src/api/propagation.ts index f26c3c6f80..a8af524244 100644 --- a/api/src/api/propagation.ts +++ b/api/src/api/propagation.ts @@ -20,6 +20,12 @@ import { HttpTextPropagator } from '../context/propagation/HttpTextPropagator'; import { NOOP_HTTP_TEXT_PROPAGATOR } from '../context/propagation/NoopHttpTextPropagator'; import { defaultSetter, SetterFunction } from '../context/propagation/setter'; import { ContextAPI } from './context'; +import { + API_BACKWARDS_COMPATIBILITY_VERSION, + GLOBAL_PROPAGATION_API_KEY, + makeGetter, + _global, +} from './global-utils'; const contextApi = ContextAPI.getInstance(); @@ -28,7 +34,6 @@ const contextApi = ContextAPI.getInstance(); */ export class PropagationAPI { private static _instance?: PropagationAPI; - private _propagator: HttpTextPropagator = NOOP_HTTP_TEXT_PROPAGATOR; /** Empty private constructor prevents end users from constructing a new instance of the API */ private constructor() {} @@ -48,7 +53,17 @@ export class PropagationAPI { public setGlobalPropagator( propagator: HttpTextPropagator ): HttpTextPropagator { - this._propagator = propagator; + if (_global[GLOBAL_PROPAGATION_API_KEY]) { + // global propagator has already been set + return this._getGlobalPropagator(); + } + + _global[GLOBAL_PROPAGATION_API_KEY] = makeGetter( + API_BACKWARDS_COMPATIBILITY_VERSION, + propagator, + NOOP_HTTP_TEXT_PROPAGATOR + ); + return propagator; } @@ -64,7 +79,7 @@ export class PropagationAPI { setter: SetterFunction = defaultSetter, context = contextApi.active() ): void { - return this._propagator.inject(context, carrier, setter); + return this._getGlobalPropagator().inject(context, carrier, setter); } /** @@ -79,6 +94,19 @@ export class PropagationAPI { getter: GetterFunction = defaultGetter, context = contextApi.active() ): Context { - return this._propagator.extract(context, carrier, getter); + return this._getGlobalPropagator().extract(context, carrier, getter); + } + + /** Remove the global propagator */ + public disable() { + delete _global[GLOBAL_PROPAGATION_API_KEY]; + } + + private _getGlobalPropagator(): HttpTextPropagator { + return ( + _global[GLOBAL_PROPAGATION_API_KEY]?.( + API_BACKWARDS_COMPATIBILITY_VERSION + ) ?? NOOP_HTTP_TEXT_PROPAGATOR + ); } } diff --git a/api/src/api/trace.ts b/api/src/api/trace.ts index 4af88e70e6..bae7353e08 100644 --- a/api/src/api/trace.ts +++ b/api/src/api/trace.ts @@ -15,15 +15,20 @@ */ import { NOOP_TRACER_PROVIDER } from '../trace/NoopTracerProvider'; -import { TracerProvider } from '../trace/tracer_provider'; import { Tracer } from '../trace/tracer'; +import { TracerProvider } from '../trace/tracer_provider'; +import { + API_BACKWARDS_COMPATIBILITY_VERSION, + GLOBAL_TRACE_API_KEY, + makeGetter, + _global, +} from './global-utils'; /** * Singleton object which represents the entry point to the OpenTelemetry Tracing API */ export class TraceAPI { private static _instance?: TraceAPI; - private _tracerProvider: TracerProvider = NOOP_TRACER_PROVIDER; /** Empty private constructor prevents end users from constructing a new instance of the API */ private constructor() {} @@ -41,15 +46,28 @@ export class TraceAPI { * Set the current global tracer. Returns the initialized global tracer provider */ public setGlobalTracerProvider(provider: TracerProvider): TracerProvider { - this._tracerProvider = provider; - return provider; + if (_global[GLOBAL_TRACE_API_KEY]) { + // global tracer provider has already been set + return this.getTracerProvider(); + } + + _global[GLOBAL_TRACE_API_KEY] = makeGetter( + API_BACKWARDS_COMPATIBILITY_VERSION, + provider, + NOOP_TRACER_PROVIDER + ); + + return this.getTracerProvider(); } /** * Returns the global tracer provider. */ public getTracerProvider(): TracerProvider { - return this._tracerProvider; + return ( + _global[GLOBAL_TRACE_API_KEY]?.(API_BACKWARDS_COMPATIBILITY_VERSION) ?? + NOOP_TRACER_PROVIDER + ); } /** @@ -58,4 +76,9 @@ export class TraceAPI { public getTracer(name: string, version?: string): Tracer { return this.getTracerProvider().getTracer(name, version); } + + /** Remove the global tracer provider */ + public disable() { + delete _global[GLOBAL_TRACE_API_KEY]; + } } diff --git a/api/test/api/api.test.ts b/api/test/api/api.test.ts index e9994038cc..d02453d252 100644 --- a/api/test/api/api.test.ts +++ b/api/test/api/api.test.ts @@ -22,6 +22,10 @@ import api, { NoopTracer, SpanOptions, Span, + context, + trace, + propagation, + metrics, } from '../../src'; describe('API', () => { @@ -41,8 +45,11 @@ describe('API', () => { }; const dummySpan = new NoopSpan(spanContext); - afterEach(() => { - api.trace.setGlobalTracerProvider(new NoopTracerProvider()); + beforeEach(() => { + context.disable(); + trace.disable(); + propagation.disable(); + metrics.disable(); }); it('should not crash', () => { diff --git a/api/test/api/global.test.ts b/api/test/api/global.test.ts new file mode 100644 index 0000000000..5ff4e25a2b --- /dev/null +++ b/api/test/api/global.test.ts @@ -0,0 +1,81 @@ +/*! + * 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 * as assert from 'assert'; +import { NoopContextManager } from '@opentelemetry/context-base'; +import { + _global, + GLOBAL_CONTEXT_MANAGER_API_KEY, +} from '../../src/api/global-utils'; + +const api1 = require('../../src') as typeof import('../../src'); + +// clear cache and load a second instance of the api +for (const key of Object.keys(require.cache)) { + delete require.cache[key]; +} +const api2 = require('../../src') as typeof import('../../src'); + +describe('Global Utils', () => { + // prove they are separate instances + assert.notEqual(api1, api2); + // that return separate noop instances to start + assert.notStrictEqual( + api1.context['_getContextManager'](), + api2.context['_getContextManager']() + ); + + beforeEach(() => { + api1.context.disable(); + api1.propagation.disable(); + api1.trace.disable(); + api1.metrics.disable(); + }); + + it('should change the global context manager', () => { + const original = api1.context['_getContextManager'](); + const newContextManager = new NoopContextManager(); + api1.context.setGlobalContextManager(newContextManager); + assert.notStrictEqual(api1.context['_getContextManager'](), original); + assert.strictEqual(api1.context['_getContextManager'](), newContextManager); + }); + + it('should load an instance from one which was set in the other', () => { + api1.context.setGlobalContextManager(new NoopContextManager()); + assert.strictEqual( + api1.context['_getContextManager'](), + api2.context['_getContextManager']() + ); + }); + + it('should disable both if one is disabled', () => { + const original = api1.context['_getContextManager'](); + + api1.context.setGlobalContextManager(new NoopContextManager()); + + assert.notStrictEqual(original, api1.context['_getContextManager']()); + api2.context.disable(); + assert.strictEqual(original, api1.context['_getContextManager']()); + }); + + it('should return the module NoOp implementation if the version is a mismatch', () => { + const original = api1.context['_getContextManager'](); + api1.context.setGlobalContextManager(new NoopContextManager()); + const afterSet = _global[GLOBAL_CONTEXT_MANAGER_API_KEY]!(-1); + + assert.strictEqual(original, afterSet); + }); +});