diff --git a/.eslintrc.js b/.eslintrc.js index 507822e8..b2767212 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -44,6 +44,7 @@ module.exports = { "files": ["test/**/*.ts"], "rules": { "no-empty": "off", + "@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/ban-ts-ignore": "off", "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-explicit-any": "off", diff --git a/src/api/context.ts b/src/api/context.ts index f5c10a93..d6ea2445 100644 --- a/src/api/context.ts +++ b/src/api/context.ts @@ -17,12 +17,12 @@ import { NoopContextManager } from '../context/NoopContextManager'; import { Context, ContextManager } from '../context/types'; import { - API_BACKWARDS_COMPATIBILITY_VERSION, - GLOBAL_CONTEXT_MANAGER_API_KEY, - makeGetter, - _global, -} from './global-utils'; + getGlobal, + registerGlobal, + unregisterGlobal, +} from '../internal/global-utils'; +const API_NAME = 'context'; const NOOP_CONTEXT_MANAGER = new NoopContextManager(); /** @@ -49,17 +49,7 @@ export class ContextAPI { public setGlobalContextManager( contextManager: 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 - ); - + registerGlobal(API_NAME, contextManager); return contextManager; } @@ -98,16 +88,12 @@ export class ContextAPI { } private _getContextManager(): ContextManager { - return ( - _global[GLOBAL_CONTEXT_MANAGER_API_KEY]?.( - API_BACKWARDS_COMPATIBILITY_VERSION - ) ?? NOOP_CONTEXT_MANAGER - ); + return getGlobal(API_NAME) || NOOP_CONTEXT_MANAGER; } /** Disable and remove the global context manager */ public disable() { this._getContextManager().disable(); - delete _global[GLOBAL_CONTEXT_MANAGER_API_KEY]; + unregisterGlobal(API_NAME); } } diff --git a/src/api/diag.ts b/src/api/diag.ts index 58a5cdd1..7b8ed7b7 100644 --- a/src/api/diag.ts +++ b/src/api/diag.ts @@ -19,51 +19,29 @@ import { DiagLogFunction, createNoopDiagLogger, diagLoggerFunctions, + FilteredDiagLogger, } from '../diag/logger'; import { DiagLogLevel, createLogLevelDiagLogger } from '../diag/logLevel'; import { - API_BACKWARDS_COMPATIBILITY_VERSION, - GLOBAL_DIAG_LOGGER_API_KEY, - makeGetter, - _global, -} from './global-utils'; - -/** Internal simple Noop Diag API that returns a noop logger and does not allow any changes */ -function noopDiagApi(): DiagAPI { - const noopApi = createNoopDiagLogger() as DiagAPI; - - noopApi.getLogger = () => noopApi; - noopApi.setLogger = noopApi.getLogger; - noopApi.setLogLevel = () => {}; - - return noopApi; -} + getGlobal, + registerGlobal, + unregisterGlobal, +} from '../internal/global-utils'; /** * Singleton object which represents the entry point to the OpenTelemetry internal * diagnostic API */ export class DiagAPI implements DiagLogger { + private static _instance?: DiagAPI; + /** Get the singleton instance of the DiagAPI API */ public static instance(): DiagAPI { - let theInst = null; - if (_global[GLOBAL_DIAG_LOGGER_API_KEY]) { - // Looks like a previous instance was set, so try and fetch it - theInst = _global[GLOBAL_DIAG_LOGGER_API_KEY]?.( - API_BACKWARDS_COMPATIBILITY_VERSION - ) as DiagAPI; - } - - if (!theInst) { - theInst = new DiagAPI(); - _global[GLOBAL_DIAG_LOGGER_API_KEY] = makeGetter( - API_BACKWARDS_COMPATIBILITY_VERSION, - theInst, - noopDiagApi() - ); + if (!this._instance) { + this._instance = new DiagAPI(); } - return theInst; + return this._instance; } /** @@ -71,14 +49,12 @@ export class DiagAPI implements DiagLogger { * @private */ private constructor() { - let _logLevel: DiagLogLevel = DiagLogLevel.INFO; - let _filteredLogger: DiagLogger | null; - let _logger: DiagLogger = createNoopDiagLogger(); + const _noopLogger = createNoopDiagLogger(); function _logProxy(funcName: keyof DiagLogger): DiagLogFunction { return function () { const orgArguments = arguments as unknown; - const theLogger = _filteredLogger || _logger; + const theLogger = self.getLogger(); const theFunc = theLogger[funcName]; if (typeof theFunc === 'function') { return theFunc.apply( @@ -94,29 +70,23 @@ export class DiagAPI implements DiagLogger { // DiagAPI specific functions - self.getLogger = (): DiagLogger => { - // Return itself if no existing logger is defined (defaults effectively to a Noop) - return _logger; + self.getLogger = (): FilteredDiagLogger => { + return getGlobal('diag') || _noopLogger; }; - self.setLogger = (logger?: DiagLogger): DiagLogger => { - const prevLogger = _logger; - if (!logger || logger !== self) { - // Simple special case to avoid any possible infinite recursion on the logging functions - _logger = logger || createNoopDiagLogger(); - _filteredLogger = createLogLevelDiagLogger(_logLevel, _logger); - } - - return prevLogger; + self.setLogger = ( + logger: DiagLogger, + logLevel: DiagLogLevel = DiagLogLevel.INFO + ) => { + // This is required to prevent an endless loop in the case where the diag + // is used as a child of itself accidentally. + logger = logger === self ? self.getLogger().getChild() : logger; + logger = logger ?? _noopLogger; + registerGlobal('diag', createLogLevelDiagLogger(logLevel, logger), true); }; - self.setLogLevel = (maxLogLevel: DiagLogLevel) => { - if (maxLogLevel !== _logLevel) { - _logLevel = maxLogLevel; - if (_logger) { - _filteredLogger = createLogLevelDiagLogger(maxLogLevel, _logger); - } - } + self.disable = () => { + unregisterGlobal('diag'); }; for (let i = 0; i < diagLoggerFunctions.length; i++) { @@ -129,17 +99,17 @@ export class DiagAPI implements DiagLogger { * Return the currently configured logger instance, if no logger has been configured * it will return itself so any log level filtering will still be applied in this case. */ - public getLogger!: () => DiagLogger; + public getLogger!: () => FilteredDiagLogger; /** - * Set the DiagLogger instance - * @param logger - [Optional] The DiagLogger instance to set as the default logger, if not provided it will set it back as a noop + * Set the global DiagLogger and DiagLogLevel. + * If a global diag logger is already set, this will override it. + * + * @param logger - [Optional] The DiagLogger instance to set as the default logger. + * @param logLevel - [Optional] The DiagLogLevel used to filter logs sent to the logger. If not provided it will default to INFO. * @returns The previously registered DiagLogger */ - public setLogger!: (logger?: DiagLogger) => DiagLogger; - - /** Set the default maximum diagnostic logging level */ - public setLogLevel!: (maxLogLevel: DiagLogLevel) => void; + public setLogger!: (logger: DiagLogger, logLevel?: DiagLogLevel) => void; // DiagLogger implementation public verbose!: DiagLogFunction; @@ -147,4 +117,9 @@ export class DiagAPI implements DiagLogger { public info!: DiagLogFunction; public warn!: DiagLogFunction; public error!: DiagLogFunction; + + /** + * Unregister the global logger and return to Noop + */ + public disable!: () => void; } diff --git a/src/api/global-utils.ts b/src/api/global-utils.ts deleted file mode 100644 index 5c86e6ed..00000000 --- a/src/api/global-utils.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright The 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 { DiagAPI } from '../api/diag'; -import { ContextManager } from '../context/types'; -import { _globalThis } from '../platform'; -import { TextMapPropagator } from '../propagation/TextMapPropagator'; -import { TracerProvider } from '../trace/tracer_provider'; - -export const GLOBAL_CONTEXT_MANAGER_API_KEY = Symbol.for( - 'io.opentelemetry.js.api.context' -); - -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'); - -export const GLOBAL_DIAG_LOGGER_API_KEY = Symbol.for( - 'io.opentelemetry.js.api.diag' -); - -type Get = (version: number) => T; -type OtelGlobal = Partial<{ - [GLOBAL_CONTEXT_MANAGER_API_KEY]: Get; - [GLOBAL_PROPAGATION_API_KEY]: Get; - [GLOBAL_TRACE_API_KEY]: Get; - [GLOBAL_DIAG_LOGGER_API_KEY]: Get; -}>; - -export const _global = _globalThis as OtelGlobal; - -/** - * 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 = 5; diff --git a/src/api/propagation.ts b/src/api/propagation.ts index dd214413..ee6394d2 100644 --- a/src/api/propagation.ts +++ b/src/api/propagation.ts @@ -24,11 +24,12 @@ import { TextMapSetter, } from '../propagation/TextMapPropagator'; import { - API_BACKWARDS_COMPATIBILITY_VERSION, - GLOBAL_PROPAGATION_API_KEY, - makeGetter, - _global, -} from './global-utils'; + getGlobal, + registerGlobal, + unregisterGlobal, +} from '../internal/global-utils'; + +const API_NAME = 'propagation'; /** * Singleton object which represents the entry point to the OpenTelemetry Propagation API @@ -52,17 +53,7 @@ export class PropagationAPI { * Set the current propagator. Returns the initialized propagator */ public setGlobalPropagator(propagator: TextMapPropagator): TextMapPropagator { - 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_TEXT_MAP_PROPAGATOR - ); - + registerGlobal(API_NAME, propagator); return propagator; } @@ -105,14 +96,10 @@ export class PropagationAPI { /** Remove the global propagator */ public disable() { - delete _global[GLOBAL_PROPAGATION_API_KEY]; + unregisterGlobal(API_NAME); } private _getGlobalPropagator(): TextMapPropagator { - return ( - _global[GLOBAL_PROPAGATION_API_KEY]?.( - API_BACKWARDS_COMPATIBILITY_VERSION - ) ?? NOOP_TEXT_MAP_PROPAGATOR - ); + return getGlobal(API_NAME) || NOOP_TEXT_MAP_PROPAGATOR; } } diff --git a/src/api/trace.ts b/src/api/trace.ts index f0c20055..f9beba0c 100644 --- a/src/api/trace.ts +++ b/src/api/trace.ts @@ -14,17 +14,17 @@ * limitations under the License. */ -import { NOOP_TRACER_PROVIDER } from '../trace/NoopTracerProvider'; import { ProxyTracerProvider } from '../trace/ProxyTracerProvider'; import { Tracer } from '../trace/tracer'; import { TracerProvider } from '../trace/tracer_provider'; import { isSpanContextValid } from '../trace/spancontext-utils'; import { - API_BACKWARDS_COMPATIBILITY_VERSION, - GLOBAL_TRACE_API_KEY, - makeGetter, - _global, -} from './global-utils'; + getGlobal, + registerGlobal, + unregisterGlobal, +} from '../internal/global-utils'; + +const API_NAME = 'trace'; /** * Singleton object which represents the entry point to the OpenTelemetry Tracing API @@ -50,30 +50,16 @@ export class TraceAPI { * Set the current global tracer. Returns the initialized global tracer provider */ public setGlobalTracerProvider(provider: TracerProvider): TracerProvider { - if (_global[GLOBAL_TRACE_API_KEY]) { - // global tracer provider has already been set - return this.getTracerProvider(); - } - this._proxyTracerProvider.setDelegate(provider); - - _global[GLOBAL_TRACE_API_KEY] = makeGetter( - API_BACKWARDS_COMPATIBILITY_VERSION, - this._proxyTracerProvider, - NOOP_TRACER_PROVIDER - ); - - return this.getTracerProvider(); + registerGlobal(API_NAME, this._proxyTracerProvider); + return this._proxyTracerProvider; } /** * Returns the global tracer provider. */ public getTracerProvider(): TracerProvider { - return ( - _global[GLOBAL_TRACE_API_KEY]?.(API_BACKWARDS_COMPATIBILITY_VERSION) ?? - this._proxyTracerProvider - ); + return getGlobal(API_NAME) || this._proxyTracerProvider; } /** @@ -85,7 +71,7 @@ export class TraceAPI { /** Remove the global tracer provider */ public disable() { - delete _global[GLOBAL_TRACE_API_KEY]; + unregisterGlobal(API_NAME); this._proxyTracerProvider = new ProxyTracerProvider(); } diff --git a/src/diag/logLevel.ts b/src/diag/logLevel.ts index 362bb135..f3699e27 100644 --- a/src/diag/logLevel.ts +++ b/src/diag/logLevel.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { DiagAPI } from '../api/diag'; -import { DiagLogger, DiagLogFunction, createNoopDiagLogger } from './logger'; +import { diag } from '..'; +import { DiagLogger, DiagLogFunction, FilteredDiagLogger } from './logger'; /** * Defines the available internal logging levels for the diagnostic logger, the numeric values @@ -81,19 +81,15 @@ const levelMap: { n: keyof DiagLogger; l: DiagLogLevel }[] = [ export function createLogLevelDiagLogger( maxLevel: DiagLogLevel, logger?: DiagLogger | null -): DiagLogger { +): FilteredDiagLogger { if (maxLevel < DiagLogLevel.NONE) { maxLevel = DiagLogLevel.NONE; } else if (maxLevel > DiagLogLevel.ALL) { maxLevel = DiagLogLevel.ALL; } - if (maxLevel === DiagLogLevel.NONE) { - return createNoopDiagLogger(); - } - if (!logger) { - logger = DiagAPI.instance(); + logger = diag.getLogger().getChild(); } function _filterFunc( @@ -116,11 +112,13 @@ export function createLogLevelDiagLogger( return function () {}; } - const newLogger = {} as DiagLogger; + const newLogger = {} as FilteredDiagLogger; for (let i = 0; i < levelMap.length; i++) { const name = levelMap[i].n; newLogger[name] = _filterFunc(logger, name, levelMap[i].l); } + newLogger.getChild = () => logger!; + return newLogger; } diff --git a/src/diag/logger.ts b/src/diag/logger.ts index f718076b..4a6bfc4a 100644 --- a/src/diag/logger.ts +++ b/src/diag/logger.ts @@ -59,6 +59,18 @@ export interface DiagLogger { verbose: DiagLogFunction; } +/** + * A filtered diag logger has the same functions as a diag logger, but calls the + * child logging methods or not depending on some filtering scheme; for example + * log level, namespace, or throttling. + */ +export interface FilteredDiagLogger extends DiagLogger { + /** + * Get the child logger of the filtered diag logger. + */ + getChild(): DiagLogger; +} + // DiagLogger implementation export const diagLoggerFunctions: Array = [ 'verbose', @@ -75,12 +87,14 @@ function noopLogFunction() {} * @implements {@link DiagLogger} * @returns {DiagLogger} */ -export function createNoopDiagLogger(): DiagLogger { - const diagLogger = {} as DiagLogger; +export function createNoopDiagLogger(): FilteredDiagLogger { + const diagLogger = {} as FilteredDiagLogger; for (let i = 0; i < diagLoggerFunctions.length; i++) { diagLogger[diagLoggerFunctions[i]] = noopLogFunction; } + diagLogger.getChild = () => diagLogger; + return diagLogger; } diff --git a/src/internal/global-utils.ts b/src/internal/global-utils.ts new file mode 100644 index 00000000..9d1b55e5 --- /dev/null +++ b/src/internal/global-utils.ts @@ -0,0 +1,92 @@ +/* + * Copyright The 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 { diag } from '..'; +import { ContextManager } from '../context/types'; +import { FilteredDiagLogger } from '../diag/logger'; +import { _globalThis } from '../platform'; +import { TextMapPropagator } from '../propagation/TextMapPropagator'; +import type { TracerProvider } from '../trace/tracer_provider'; +import { VERSION } from '../version'; +import { isCompatible } from './semver'; + +const GLOBAL_OPENTELEMETRY_API_KEY = Symbol.for('io.opentelemetry.js.api'); + +const _global = _globalThis as OTelGlobal; + +export function registerGlobal( + type: Type, + instance: OTelGlobalAPI[Type], + allowOverride = false +): void { + _global[GLOBAL_OPENTELEMETRY_API_KEY] = _global[ + GLOBAL_OPENTELEMETRY_API_KEY + ] ?? { + version: VERSION, + }; + + const api = _global[GLOBAL_OPENTELEMETRY_API_KEY]!; + if (!allowOverride && api[type]) { + // already registered an API of this type + const err = new Error( + `@opentelemetry/api: Attempted duplicate registration of API: ${type}` + ); + diag.error(err.stack || err.message); + return; + } + + if (api.version != VERSION) { + // All registered APIs must be of the same version exactly + const err = new Error( + '@opentelemetry/api: All API registration versions must match' + ); + diag.error(err.stack || err.message); + return; + } + + api[type] = instance; +} + +export function getGlobal( + type: Type +): OTelGlobalAPI[Type] | undefined { + const version = _global[GLOBAL_OPENTELEMETRY_API_KEY]?.version; + if (!version || !isCompatible(version)) { + return; + } + return _global[GLOBAL_OPENTELEMETRY_API_KEY]?.[type]; +} + +export function unregisterGlobal(type: keyof OTelGlobalAPI) { + const api = _global[GLOBAL_OPENTELEMETRY_API_KEY]; + + if (api) { + delete api[type]; + } +} + +type OTelGlobal = { + [GLOBAL_OPENTELEMETRY_API_KEY]?: OTelGlobalAPI; +}; + +type OTelGlobalAPI = { + version: string; + + diag?: FilteredDiagLogger; + trace?: TracerProvider; + context?: ContextManager; + propagation?: TextMapPropagator; +}; diff --git a/src/internal/semver.ts b/src/internal/semver.ts new file mode 100644 index 00000000..fba640d4 --- /dev/null +++ b/src/internal/semver.ts @@ -0,0 +1,113 @@ +/* + * Copyright The 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 { VERSION } from '../version'; + +const re = /^(\d+)\.(\d+)\.(\d+)(?:-(.*))?$/; + +/** + * Create a function to test an API version to see if it is compatible with the provided ownVersion. + * + * The returned function has the following semantics: + * - Exact match is always compatible + * - Major versions must always match + * - The minor version of the API module requesting access to the global API must be greater or equal to the minor version of this API + * - Patch and build tag differences are not considered at this time + * + * @param ownVersion version which should be checked against + */ +export function _makeCompatibilityCheck( + ownVersion: string +): (version: string) => boolean { + const acceptedVersions = new Set([ownVersion]); + const rejectedVersions = new Set(); + + const myVersionMatch = ownVersion.match(re); + if (!myVersionMatch) { + throw new Error('Cannot parse own version'); + } + + const ownVersionParsed = { + major: +myVersionMatch[1], + minor: +myVersionMatch[2], + patch: +myVersionMatch[3], + }; + + return function isCompatible(version: string): boolean { + if (acceptedVersions.has(version)) { + return true; + } + + if (rejectedVersions.has(version)) { + return false; + } + + const m = version.match(re); + if (!m) { + // cannot parse other version + rejectedVersions.add(version); + return false; + } + + const otherVersionParsed = { + major: +m[1], + minor: +m[2], + patch: +m[3], + }; + + // major versions must match + if (ownVersionParsed.major != otherVersionParsed.major) { + rejectedVersions.add(version); + return false; + } + + // if major version is 0, minor is treated like major and patch is treated like minor + if (ownVersionParsed.major === 0) { + if (ownVersionParsed.minor != otherVersionParsed.minor) { + rejectedVersions.add(version); + return false; + } + + if (ownVersionParsed.patch < otherVersionParsed.patch) { + rejectedVersions.add(version); + return false; + } + + acceptedVersions.add(version); + return true; + } + + if (ownVersionParsed.minor < otherVersionParsed.minor) { + rejectedVersions.add(version); + return false; + } + + acceptedVersions.add(version); + return true; + }; +} + +/** + * Test an API version to see if it is compatible with this API. + * + * - Exact match is always compatible + * - Major versions must always match + * - The minor version of the API module requesting access to the global API must be greater or equal to the minor version of this API + * - Patch and build tag differences are not considered at this time + * + * @param version version of the API requesting an instance of the global API + */ +export const isCompatible = _makeCompatibilityCheck(VERSION); diff --git a/test/api/api.test.ts b/test/api/api.test.ts index 50b905be..30abf78e 100644 --- a/test/api/api.test.ts +++ b/test/api/api.test.ts @@ -35,7 +35,6 @@ import api, { diagLoggerFunctions, } from '../../src'; import { DiagAPI } from '../../src/api/diag'; -import { _global } from '../../src/api/global-utils'; import { NoopSpan } from '../../src/trace/NoopSpan'; describe('API', () => { @@ -191,6 +190,7 @@ describe('API', () => { diagLoggerFunctions.forEach(fName => { it(`no argument logger ${fName} message doesn't throw`, () => { + //@ts-expect-error logger argument is required diag.setLogger(); assert.doesNotThrow(() => { diag[fName](`${fName} message`); diff --git a/test/diag/logLevel.test.ts b/test/diag/logLevel.test.ts index 3b7fa548..627f339f 100644 --- a/test/diag/logLevel.test.ts +++ b/test/diag/logLevel.test.ts @@ -50,8 +50,7 @@ describe('LogLevelFilter DiagLogger', () => { beforeEach(() => { // Set no logger so that sinon doesn't complain about TypeError: Attempted to wrap xxxx which is already wrapped - diag.setLogger(); - diag.setLogLevel(DiagLogLevel.INFO); + diag.disable(); // mock dummyLogger = {} as DiagLogger; @@ -164,8 +163,7 @@ describe('LogLevelFilter DiagLogger', () => { }); it('should use default logger for undefined and log', () => { - diag.setLogger(dummyLogger); - diag.setLogLevel(DiagLogLevel.ALL); + diag.setLogger(dummyLogger, DiagLogLevel.ALL); const testLogger = createLogLevelDiagLogger(map.level, undefined); testLogger[fName](`${fName} called %s`, 'param1'); diagLoggerFunctions.forEach(lName => { @@ -181,8 +179,7 @@ describe('LogLevelFilter DiagLogger', () => { }); it('should use default logger for null and log', () => { - diag.setLogger(dummyLogger); - diag.setLogLevel(DiagLogLevel.ALL); + diag.setLogger(dummyLogger, DiagLogLevel.ALL); const testLogger = createLogLevelDiagLogger(map.level, null); testLogger[fName](`${fName} called %s`, 'param1'); diagLoggerFunctions.forEach(lName => { @@ -199,17 +196,35 @@ describe('LogLevelFilter DiagLogger', () => { levelMap.forEach(masterLevelMap => { describe(`when diag logger is set to ${masterLevelMap.message}`, () => { - it('diag setLogLevel is not ignored and using default logger', () => { - diag.setLogger(dummyLogger); - diag.setLogLevel(masterLevelMap.level); + it('diag.setLogger level is not ignored and using default logger', () => { + diag.setLogger(dummyLogger, masterLevelMap.level); const testLogger = createLogLevelDiagLogger(map.level); testLogger[fName](`${fName} called %s`, 'param1'); diagLoggerFunctions.forEach(lName => { if ( fName === lName && - map.ignoreFuncs.indexOf(lName) === -1 && - masterLevelMap.ignoreFuncs.indexOf(lName) === -1 + map.ignoreFuncs.indexOf(lName) === -1 + ) { + assert.deepStrictEqual(calledArgs[lName], [ + `${fName} called %s`, + 'param1', + ]); + } else { + assert.strictEqual(calledArgs[lName], null); + } + }); + }); + + it('diag.setLogger level is ignored and using diag as the logger', () => { + diag.setLogger(dummyLogger, masterLevelMap.level); + diag.setLogger(diag, map.level); + + diag[fName](`${fName} called %s`, 'param1'); + diagLoggerFunctions.forEach(lName => { + if ( + fName === lName && + map.ignoreFuncs.indexOf(lName) === -1 ) { assert.deepStrictEqual(calledArgs[lName], [ `${fName} called %s`, @@ -221,13 +236,12 @@ describe('LogLevelFilter DiagLogger', () => { }); }); - it('diag setLogLevel is ignored when using a specific logger', () => { - diag.setLogger(dummyLogger); - diag.setLogLevel(masterLevelMap.level); + it('diag.setLogger level is ignored when using a specific logger', () => { + diag.setLogger(dummyLogger, masterLevelMap.level); const testLogger = createLogLevelDiagLogger( map.level, - diag.getLogger() + diag.getLogger().getChild() ); testLogger[fName](`${fName} called %s`, 'param1'); diagLoggerFunctions.forEach(lName => { @@ -247,9 +261,8 @@ describe('LogLevelFilter DiagLogger', () => { }); }); - it('diag setLogLevel and logger should log', () => { - diag.setLogger(dummyLogger); - diag.setLogLevel(map.level); + it('diag.setLogger level and logger should log', () => { + diag.setLogger(dummyLogger, map.level); diag[fName](`${fName} called %s`, 'param1'); diagLoggerFunctions.forEach(lName => { if (fName === lName && map.ignoreFuncs.indexOf(lName) === -1) { diff --git a/test/diag/logger.test.ts b/test/diag/logger.test.ts index a957445b..70258bbc 100644 --- a/test/diag/logger.test.ts +++ b/test/diag/logger.test.ts @@ -48,6 +48,7 @@ describe('DiagLogger functions', () => { diagLoggerFunctions.forEach(fName => { calledArgs[fName] = null; }); + diag.disable(); }); describe('constructor', () => { @@ -68,8 +69,7 @@ describe('DiagLogger functions', () => { }); it(`diag should log with ${fName} message`, () => { - diag.setLogger(dummyLogger); - diag.setLogLevel(DiagLogLevel.ALL); + diag.setLogger(dummyLogger, DiagLogLevel.ALL); diag[fName](`${fName} called %s`, 'param1'); diagLoggerFunctions.forEach(lName => { if (fName === lName) { diff --git a/test/api/global.test.ts b/test/internal/global.test.ts similarity index 58% rename from test/api/global.test.ts rename to test/internal/global.test.ts index 44edfeff..6186cee1 100644 --- a/test/api/global.test.ts +++ b/test/internal/global.test.ts @@ -15,11 +15,10 @@ */ import * as assert from 'assert'; -import { - GLOBAL_CONTEXT_MANAGER_API_KEY, - _global, -} from '../../src/api/global-utils'; +import { getGlobal } from '../../src/internal/global-utils'; +import { _globalThis } from '../../src/platform'; import { NoopContextManager } from '../../src/context/NoopContextManager'; +import sinon = require('sinon'); const api1 = require('../../src') as typeof import('../../src'); @@ -42,6 +41,9 @@ describe('Global Utils', () => { api1.context.disable(); api1.propagation.disable(); api1.trace.disable(); + api1.diag.disable(); + // @ts-ignore we are modifying internals for testing purposes here + delete _globalThis[Symbol.for('io.opentelemetry.js.api')]; }); it('should change the global context manager', () => { @@ -72,9 +74,61 @@ describe('Global Utils', () => { it('should return the module NoOp implementation if the version is a mismatch', () => { const original = api1.context['_getContextManager'](); + const newContextManager = new NoopContextManager(); + api1.context.setGlobalContextManager(newContextManager); + + assert.strictEqual(api1.context['_getContextManager'](), newContextManager); + + const globalInstance = getGlobal('context'); + assert.ok(globalInstance); + // @ts-ignore we are modifying internals for testing purposes here + _globalThis[Symbol.for('io.opentelemetry.js.api')].version = '0.0.1'; + + assert.strictEqual(api1.context['_getContextManager'](), original); + }); + + it('should log an error if there is a duplicate registration', () => { + const error = sinon.stub(); + api1.diag.setLogger({ + verbose: () => {}, + debug: () => {}, + info: () => {}, + warn: () => {}, + error, + }); + + api1.context.setGlobalContextManager(new NoopContextManager()); api1.context.setGlobalContextManager(new NoopContextManager()); - const afterSet = _global[GLOBAL_CONTEXT_MANAGER_API_KEY]!(-1); - assert.strictEqual(original, afterSet); + sinon.assert.calledOnce(error); + assert.strictEqual(error.firstCall.args.length, 1); + assert.ok( + error.firstCall.args[0].startsWith( + 'Error: @opentelemetry/api: Attempted duplicate registration of API: context' + ) + ); + }); + + it('should allow duplicate registration of the diag logger', () => { + const error1 = sinon.stub(); + const error2 = sinon.stub(); + api1.diag.setLogger({ + verbose: () => {}, + debug: () => {}, + info: () => {}, + warn: () => {}, + error: error1, + }); + + api1.diag.setLogger({ + verbose: () => {}, + debug: () => {}, + info: () => {}, + warn: () => {}, + error: error2, + }); + + sinon.assert.notCalled(error1); + sinon.assert.notCalled(error2); }); }); diff --git a/test/internal/semver.test.ts b/test/internal/semver.test.ts new file mode 100644 index 00000000..6020723b --- /dev/null +++ b/test/internal/semver.test.ts @@ -0,0 +1,101 @@ +/* + * Copyright The 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 { + isCompatible, + _makeCompatibilityCheck, +} from '../../src/internal/semver'; +import { VERSION } from '../../src/version'; + +describe('Version Compatibility', () => { + it('should be compatible if versions are equal', () => { + assert.ok(isCompatible(VERSION)); + }); + + describe('throws if own version cannot be parsed', () => { + assert.throws(() => { + _makeCompatibilityCheck('this is not semver'); + }); + }); + + describe('incompatible if other version cannot be parsed', () => { + const check = _makeCompatibilityCheck('0.1.2'); + assert.ok(!check('this is not semver')); + }); + + describe('>= 1.x', () => { + it('should be compatible if major and minor versions are equal', () => { + const check = _makeCompatibilityCheck('1.2.3'); + assert.ok(check('1.2.2')); + assert.ok(check('1.2.2-alpha')); + assert.ok(check('1.2.4')); + assert.ok(check('1.2.4-alpha')); + }); + + it('should be compatible if major versions are equal and minor version is lesser', () => { + const check = _makeCompatibilityCheck('1.2.3'); + assert.ok(check('1.1.2')); + assert.ok(check('1.1.2-alpha')); + assert.ok(check('1.1.4')); + assert.ok(check('1.1.4-alpha')); + }); + + it('should be incompatible if major versions do not match', () => { + const check = _makeCompatibilityCheck('3.3.3'); + assert.ok(!check('0.3.3')); + assert.ok(!check('0.3.3')); + }); + + it('should be incompatible if major versions match but other minor version is greater than our minor version', () => { + const check = _makeCompatibilityCheck('1.2.3'); + assert.ok(!check('1.3.3-alpha')); + assert.ok(!check('1.3.3')); + }); + }); + + describe('0.x', () => { + it('should be compatible if minor and patch versions are equal', () => { + const check = _makeCompatibilityCheck('0.1.2'); + assert.ok(check('0.1.2')); + assert.ok(check('0.1.2-alpha')); + }); + + it('should be compatible if minor versions are equal and patch version is lesser', () => { + const check = _makeCompatibilityCheck('0.1.2'); + assert.ok(check('0.1.1')); + assert.ok(check('0.1.1-alpha')); + }); + + it('should be incompatible if minor versions do not match', () => { + const check = _makeCompatibilityCheck('0.3.3'); + assert.ok(!check('0.2.3')); + assert.ok(!check('0.4.3')); + }); + + it('should be incompatible if minor versions do not match', () => { + const check = _makeCompatibilityCheck('0.3.3'); + assert.ok(!check('0.2.3')); + assert.ok(!check('0.4.3')); + }); + + it('should be incompatible if minor versions match but other patch version is greater than our patch version', () => { + const check = _makeCompatibilityCheck('0.3.3'); + assert.ok(!check('0.3.4-alpha')); + assert.ok(!check('0.3.4')); + }); + }); +});