diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index 673477fdbb..33e8a3d0ce 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to experimental packages in this project will be documented ### :rocket: (Enhancement) +* feat(api-logs): Add delegating no-op logger provider [#4861](https://github.com/open-telemetry/opentelemetry-js/pull/4861) @hectorhdzg + ### :bug: (Bug Fix) * fix(sampler-jaeger-remote): fixes an issue where package could emit unhandled promise rejections @Just-Sieb diff --git a/experimental/packages/api-logs/src/ProxyLogger.ts b/experimental/packages/api-logs/src/ProxyLogger.ts new file mode 100644 index 0000000000..7869c65eac --- /dev/null +++ b/experimental/packages/api-logs/src/ProxyLogger.ts @@ -0,0 +1,69 @@ +/* + * 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 { NOOP_LOGGER } from './NoopLogger'; +import { Logger } from './types/Logger'; +import { LoggerOptions } from './types/LoggerOptions'; +import { LogRecord } from './types/LogRecord'; + +export class ProxyLogger implements Logger { + // When a real implementation is provided, this will be it + private _delegate?: Logger; + + constructor( + private _provider: LoggerDelegator, + public readonly name: string, + public readonly version?: string | undefined, + public readonly options?: LoggerOptions | undefined + ) {} + + /** + * Emit a log record. This method should only be used by log appenders. + * + * @param logRecord + */ + emit(logRecord: LogRecord): void { + this._getLogger().emit(logRecord); + } + + /** + * Try to get a logger from the proxy logger provider. + * If the proxy logger provider has no delegate, return a noop logger. + */ + private _getLogger() { + if (this._delegate) { + return this._delegate; + } + const logger = this._provider.getDelegateLogger( + this.name, + this.version, + this.options + ); + if (!logger) { + return NOOP_LOGGER; + } + this._delegate = logger; + return this._delegate; + } +} + +export interface LoggerDelegator { + getDelegateLogger( + name: string, + version?: string, + options?: LoggerOptions + ): Logger | undefined; +} diff --git a/experimental/packages/api-logs/src/ProxyLoggerProvider.ts b/experimental/packages/api-logs/src/ProxyLoggerProvider.ts new file mode 100644 index 0000000000..952ae4da2a --- /dev/null +++ b/experimental/packages/api-logs/src/ProxyLoggerProvider.ts @@ -0,0 +1,55 @@ +/* + * 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 { LoggerProvider } from './types/LoggerProvider'; +import { Logger } from './types/Logger'; +import { LoggerOptions } from './types/LoggerOptions'; +import { NOOP_LOGGER_PROVIDER } from './NoopLoggerProvider'; +import { ProxyLogger } from './ProxyLogger'; + +export class ProxyLoggerProvider implements LoggerProvider { + private _delegate?: LoggerProvider; + + getLogger( + name: string, + version?: string | undefined, + options?: LoggerOptions | undefined + ): Logger { + return ( + this.getDelegateLogger(name, version, options) ?? + new ProxyLogger(this, name, version, options) + ); + } + + getDelegate(): LoggerProvider { + return this._delegate ?? NOOP_LOGGER_PROVIDER; + } + + /** + * Set the delegate logger provider + */ + setDelegate(delegate: LoggerProvider) { + this._delegate = delegate; + } + + getDelegateLogger( + name: string, + version?: string | undefined, + options?: LoggerOptions | undefined + ): Logger | undefined { + return this._delegate?.getLogger(name, version, options); + } +} diff --git a/experimental/packages/api-logs/src/api/logs.ts b/experimental/packages/api-logs/src/api/logs.ts index d6f24b5e07..b4f5ab7aa9 100644 --- a/experimental/packages/api-logs/src/api/logs.ts +++ b/experimental/packages/api-logs/src/api/logs.ts @@ -24,10 +24,13 @@ import { LoggerProvider } from '../types/LoggerProvider'; import { NOOP_LOGGER_PROVIDER } from '../NoopLoggerProvider'; import { Logger } from '../types/Logger'; import { LoggerOptions } from '../types/LoggerOptions'; +import { ProxyLoggerProvider } from '../ProxyLoggerProvider'; export class LogsAPI { private static _instance?: LogsAPI; + private _proxyLoggerProvider = new ProxyLoggerProvider(); + private constructor() {} public static getInstance(): LogsAPI { @@ -48,6 +51,7 @@ export class LogsAPI { provider, NOOP_LOGGER_PROVIDER ); + this._proxyLoggerProvider.setDelegate(provider); return provider; } @@ -60,7 +64,7 @@ export class LogsAPI { public getLoggerProvider(): LoggerProvider { return ( _global[GLOBAL_LOGS_API_KEY]?.(API_BACKWARDS_COMPATIBILITY_VERSION) ?? - NOOP_LOGGER_PROVIDER + this._proxyLoggerProvider ); } @@ -80,5 +84,6 @@ export class LogsAPI { /** Remove the global logger provider */ public disable(): void { delete _global[GLOBAL_LOGS_API_KEY]; + this._proxyLoggerProvider = new ProxyLoggerProvider(); } } diff --git a/experimental/packages/api-logs/src/index.ts b/experimental/packages/api-logs/src/index.ts index 55f31b770a..d34d8eee98 100644 --- a/experimental/packages/api-logs/src/index.ts +++ b/experimental/packages/api-logs/src/index.ts @@ -26,6 +26,8 @@ export { LoggerOptions } from './types/LoggerOptions'; export { AnyValue, AnyValueMap } from './types/AnyValue'; export { NOOP_LOGGER, NoopLogger } from './NoopLogger'; export { NOOP_LOGGER_PROVIDER, NoopLoggerProvider } from './NoopLoggerProvider'; +export { ProxyLogger } from './ProxyLogger'; +export { ProxyLoggerProvider } from './ProxyLoggerProvider'; import { LogsAPI } from './api/logs'; export const logs = LogsAPI.getInstance(); diff --git a/experimental/packages/api-logs/test/api/api.test.ts b/experimental/packages/api-logs/test/api/api.test.ts index 3548e08184..6994430420 100644 --- a/experimental/packages/api-logs/test/api/api.test.ts +++ b/experimental/packages/api-logs/test/api/api.test.ts @@ -15,7 +15,7 @@ */ import * as assert from 'assert'; -import { Logger, logs } from '../../src'; +import { Logger, ProxyLoggerProvider, logs } from '../../src'; import { NoopLogger } from '../../src/NoopLogger'; import { NoopLoggerProvider } from '../../src/NoopLoggerProvider'; @@ -23,9 +23,11 @@ describe('API', () => { const dummyLogger = new NoopLogger(); it('should expose a logger provider via getLoggerProvider', () => { - const provider = logs.getLoggerProvider(); - assert.ok(provider); - assert.strictEqual(typeof provider, 'object'); + assert.ok(logs.getLoggerProvider() instanceof ProxyLoggerProvider); + assert.ok( + (logs.getLoggerProvider() as ProxyLoggerProvider).getDelegate() instanceof + NoopLoggerProvider + ); }); describe('GlobalLoggerProvider', () => { diff --git a/experimental/packages/api-logs/test/internal/global.test.ts b/experimental/packages/api-logs/test/internal/global.test.ts index 8ac9c48cf3..da0fbdbb16 100644 --- a/experimental/packages/api-logs/test/internal/global.test.ts +++ b/experimental/packages/api-logs/test/internal/global.test.ts @@ -17,6 +17,7 @@ import * as assert from 'assert'; import { _global, GLOBAL_LOGS_API_KEY } from '../../src/internal/global-utils'; import { NoopLoggerProvider } from '../../src/NoopLoggerProvider'; +import { ProxyLoggerProvider } from '../../src'; const api1 = require('../../src') as typeof import('../../src'); @@ -66,11 +67,10 @@ describe('Global Utils', () => { assert.strictEqual(original, api1.logs.getLoggerProvider()); }); - it('should return the module NoOp implementation if the version is a mismatch', () => { - const original = api1.logs.getLoggerProvider(); - api1.logs.setGlobalLoggerProvider(new NoopLoggerProvider()); + it('should return the module no op implementation if the version is a mismatch', () => { + api1.logs.setGlobalLoggerProvider(new ProxyLoggerProvider()); const afterSet = _global[GLOBAL_LOGS_API_KEY]!(-1); - assert.strictEqual(original, afterSet); + assert.ok(afterSet instanceof NoopLoggerProvider); }); }); diff --git a/experimental/packages/api-logs/test/proxy-implementations/proxy-logger.test.ts b/experimental/packages/api-logs/test/proxy-implementations/proxy-logger.test.ts new file mode 100644 index 0000000000..85d9f593c0 --- /dev/null +++ b/experimental/packages/api-logs/test/proxy-implementations/proxy-logger.test.ts @@ -0,0 +1,117 @@ +/* + * 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 * as sinon from 'sinon'; +import { + Logger, + LoggerProvider, + ProxyLogger, + ProxyLoggerProvider, +} from '../../src'; +import { NoopLogger } from '../../src/NoopLogger'; + +describe('ProxyLogger', () => { + let provider: ProxyLoggerProvider; + const sandbox = sinon.createSandbox(); + + beforeEach(() => { + provider = new ProxyLoggerProvider(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('when no delegate is set', () => { + it('should return proxy loggers', () => { + const logger = provider.getLogger('test'); + assert.ok(logger instanceof ProxyLogger); + }); + }); + + describe('when delegate is set before getLogger', () => { + let delegate: LoggerProvider; + let getLoggerStub: sinon.SinonStub; + + beforeEach(() => { + getLoggerStub = sandbox.stub().returns(new NoopLogger()); + delegate = { + getLogger: getLoggerStub, + }; + provider.setDelegate(delegate); + }); + + it('should return loggers directly from the delegate', () => { + const logger = provider.getLogger('test', 'v0'); + + sandbox.assert.calledOnce(getLoggerStub); + assert.strictEqual(getLoggerStub.firstCall.returnValue, logger); + assert.deepStrictEqual(getLoggerStub.firstCall.args, [ + 'test', + 'v0', + undefined, + ]); + }); + + it('should return loggers directly from the delegate with schema url', () => { + const logger = provider.getLogger('test', 'v0', { + schemaUrl: 'https://opentelemetry.io/schemas/1.7.0', + }); + + sandbox.assert.calledOnce(getLoggerStub); + assert.strictEqual(getLoggerStub.firstCall.returnValue, logger); + assert.deepStrictEqual(getLoggerStub.firstCall.args, [ + 'test', + 'v0', + { schemaUrl: 'https://opentelemetry.io/schemas/1.7.0' }, + ]); + }); + }); + + describe('when delegate is set after getLogger', () => { + let logger: Logger; + let delegateProvider: LoggerProvider; + + let delegateLogger: Logger; + let emitCalled: boolean; + + beforeEach(() => { + emitCalled = false; + delegateLogger = { + emit() { + emitCalled = true; + }, + }; + + logger = provider.getLogger('test'); + + delegateProvider = { + getLogger() { + return delegateLogger; + }, + }; + provider.setDelegate(delegateProvider); + }); + + it('should emit from the delegate logger', () => { + logger.emit({ + body: 'Test', + }); + assert.ok(emitCalled); + }); + }); +}); diff --git a/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts b/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts index 1d8bc87fbd..d24fe66b0b 100644 --- a/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts +++ b/experimental/packages/opentelemetry-sdk-node/test/sdk.test.ts @@ -66,7 +66,7 @@ import { serviceInstanceIdDetectorSync, } from '@opentelemetry/resources'; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; -import { logs } from '@opentelemetry/api-logs'; +import { logs, ProxyLoggerProvider } from '@opentelemetry/api-logs'; import { SimpleLogRecordProcessor, InMemoryLogRecordExporter, @@ -90,6 +90,7 @@ describe('Node SDK', () => { let ctxManager: any; let propagator: any; let delegate: any; + let logsDelegate: any; beforeEach(() => { diag.disable(); @@ -102,6 +103,9 @@ describe('Node SDK', () => { ctxManager = context['_getContextManager'](); propagator = propagation['_getGlobalPropagator'](); delegate = (trace.getTracerProvider() as ProxyTracerProvider).getDelegate(); + logsDelegate = ( + logs.getLoggerProvider() as ProxyLoggerProvider + ).getDelegate(); }); afterEach(() => { @@ -113,6 +117,7 @@ describe('Node SDK', () => { // need to set OTEL_TRACES_EXPORTER to none since default value is otlp // which sets up an exporter and affects the context manager env.OTEL_TRACES_EXPORTER = 'none'; + env.OTEL_LOGS_EXPORTER = 'none'; const sdk = new NodeSDK({ autoDetectResources: false, }); @@ -135,6 +140,11 @@ describe('Node SDK', () => { 'tracer provider should not have changed' ); assert.ok(!(metrics.getMeterProvider() instanceof MeterProvider)); + assert.strictEqual( + (logs.getLoggerProvider() as ProxyLoggerProvider).getDelegate(), + logsDelegate, + 'logger provider should not have changed' + ); delete env.OTEL_TRACES_EXPORTER; await sdk.shutdown(); }); @@ -327,7 +337,10 @@ describe('Node SDK', () => { 'tracer provider should not have changed' ); - assert.ok(logs.getLoggerProvider() instanceof LoggerProvider); + assert.ok( + (logs.getLoggerProvider() as ProxyLoggerProvider) instanceof + LoggerProvider + ); await sdk.shutdown(); delete env.OTEL_TRACES_EXPORTER; });