Skip to content

Commit

Permalink
Use global API instances (#943)
Browse files Browse the repository at this point in the history
  • Loading branch information
dyladan authored Apr 29, 2020
1 parent 69f1913 commit e4e71b8
Show file tree
Hide file tree
Showing 21 changed files with 397 additions and 82 deletions.
4 changes: 4 additions & 0 deletions packages/opentelemetry-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
43 changes: 37 additions & 6 deletions packages/opentelemetry-api/src/api/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {}
Expand All @@ -45,15 +52,25 @@ 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;
}

/**
* Get the currently active context
*/
public active(): Context {
return this._contextManager.active();
return this._getContextManager().active();
}

/**
Expand All @@ -66,7 +83,7 @@ export class ContextAPI {
context: Context,
fn: T
): ReturnType<T> {
return this._contextManager.with(context, fn);
return this._getContextManager().with(context, fn);
}

/**
Expand All @@ -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<T>(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];
}
}
67 changes: 67 additions & 0 deletions packages/opentelemetry-api/src/api/global-utils.ts
Original file line number Diff line number Diff line change
@@ -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<T> = (version: number) => T;
type MyGlobals = Partial<{
[GLOBAL_CONTEXT_MANAGER_API_KEY]: Get<ContextManager>;
[GLOBAL_METRICS_API_KEY]: Get<MeterProvider>;
[GLOBAL_PROPAGATION_API_KEY]: Get<HttpTextPropagator>;
[GLOBAL_TRACE_API_KEY]: Get<TracerProvider>;
}>;

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<T>(
requiredVersion: number,
instance: T,
fallback: T
): Get<T> {
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;
29 changes: 26 additions & 3 deletions packages/opentelemetry-api/src/api/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {}
Expand All @@ -41,15 +46,28 @@ 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;
}

/**
* Returns the global meter provider.
*/
public getMeterProvider(): MeterProvider {
return this._meterProvider;
return (
_global[GLOBAL_METRICS_API_KEY]?.(API_BACKWARDS_COMPATIBILITY_VERSION) ??
NOOP_METER_PROVIDER
);
}

/**
Expand All @@ -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];
}
}
36 changes: 32 additions & 4 deletions packages/opentelemetry-api/src/api/propagation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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() {}
Expand All @@ -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;
}

Expand All @@ -64,7 +79,7 @@ export class PropagationAPI {
setter: SetterFunction<Carrier> = defaultSetter,
context = contextApi.active()
): void {
return this._propagator.inject(context, carrier, setter);
return this._getGlobalPropagator().inject(context, carrier, setter);
}

/**
Expand All @@ -79,6 +94,19 @@ export class PropagationAPI {
getter: GetterFunction<Carrier> = 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
);
}
}
33 changes: 28 additions & 5 deletions packages/opentelemetry-api/src/api/trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {}
Expand All @@ -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
);
}

/**
Expand All @@ -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];
}
}
11 changes: 9 additions & 2 deletions packages/opentelemetry-api/test/api/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ import api, {
NoopTracer,
SpanOptions,
Span,
context,
trace,
propagation,
metrics,
} from '../../src';

describe('API', () => {
Expand All @@ -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', () => {
Expand Down
Loading

0 comments on commit e4e71b8

Please sign in to comment.