Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use global API instances #943

Merged
merged 8 commits into from
Apr 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]) {
dyladan marked this conversation as resolved.
Show resolved Hide resolved
// 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>(
dyladan marked this conversation as resolved.
Show resolved Hide resolved
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