diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index daabc424723..ccdb005444c 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -8,6 +8,8 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2 ### :boom: Breaking Changes +* feat(api-logs, sdk-logs)!: add `enabled` method to Logger interface and implementi it in logs API and SDK [#6371](https://github.com/open-telemetry/opentelemetry-js/pull/6371) @david-luna + ### :rocket: Features ### :bug: Bug Fixes diff --git a/experimental/packages/api-logs/src/NoopLogger.ts b/experimental/packages/api-logs/src/NoopLogger.ts index 600f2d4bfba..8645f4938f6 100644 --- a/experimental/packages/api-logs/src/NoopLogger.ts +++ b/experimental/packages/api-logs/src/NoopLogger.ts @@ -8,6 +8,9 @@ import type { LogRecord } from './types/LogRecord'; export class NoopLogger implements Logger { emit(_logRecord: LogRecord): void {} + enabled(): boolean { + return false; + } } export const NOOP_LOGGER = new NoopLogger(); diff --git a/experimental/packages/api-logs/src/ProxyLogger.ts b/experimental/packages/api-logs/src/ProxyLogger.ts index b0814dffacb..a162a17d0ae 100644 --- a/experimental/packages/api-logs/src/ProxyLogger.ts +++ b/experimental/packages/api-logs/src/ProxyLogger.ts @@ -3,10 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ +import type { Context } from '@opentelemetry/api'; import { NOOP_LOGGER } from './NoopLogger'; import type { Logger } from './types/Logger'; import type { LoggerOptions } from './types/LoggerOptions'; import type { LogRecord } from './types/LogRecord'; +import type { SeverityNumber } from './types/LogRecord'; export class ProxyLogger implements Logger { // When a real implementation is provided, this will be it @@ -37,6 +39,14 @@ export class ProxyLogger implements Logger { this._getLogger().emit(logRecord); } + enabled(options?: { + context?: Context; + severityNumber?: SeverityNumber; + eventName?: string; + }): boolean { + return this._getLogger().enabled(options); + } + /** * Try to get a logger from the proxy logger provider. * If the proxy logger provider has no delegate, return a noop logger. diff --git a/experimental/packages/api-logs/src/types/Logger.ts b/experimental/packages/api-logs/src/types/Logger.ts index 3da70201381..9cceb65534d 100644 --- a/experimental/packages/api-logs/src/types/Logger.ts +++ b/experimental/packages/api-logs/src/types/Logger.ts @@ -3,7 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ +import type { Context } from '@opentelemetry/api'; import type { LogRecord } from './LogRecord'; +import type { SeverityNumber } from './LogRecord'; export interface Logger { /** @@ -12,4 +14,14 @@ export interface Logger { * @param logRecord */ emit(logRecord: LogRecord): void; + + /** + * Will a log record with the given details get emitted? + * This can be used to avoid expensive calculation of log record data. + */ + enabled(options?: { + context?: Context; + severityNumber?: SeverityNumber; + eventName?: string; + }): boolean; } diff --git a/experimental/packages/api-logs/test/noop-implementations/noop-logger.test.ts b/experimental/packages/api-logs/test/noop-implementations/noop-logger.test.ts index 64a30d840de..acb415172d9 100644 --- a/experimental/packages/api-logs/test/noop-implementations/noop-logger.test.ts +++ b/experimental/packages/api-logs/test/noop-implementations/noop-logger.test.ts @@ -21,4 +21,9 @@ describe('NoopLogger', () => { body: 'log body', }); }); + + it('calling enabled should return false', () => { + const logger = new NoopLoggerProvider().getLogger('test-noop'); + assert.ok(!logger.enabled()); + }); }); 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 index e74de3a5c9d..8628baf3c0b 100644 --- a/experimental/packages/api-logs/test/proxy-implementations/proxy-logger.test.ts +++ b/experimental/packages/api-logs/test/proxy-implementations/proxy-logger.test.ts @@ -74,6 +74,7 @@ describe('ProxyLogger', () => { let delegateLogger: Logger; let emitCalled: boolean; + let enabledCalled: boolean; beforeEach(() => { emitCalled = false; @@ -81,6 +82,10 @@ describe('ProxyLogger', () => { emit() { emitCalled = true; }, + enabled() { + enabledCalled = true; + return true; + }, }; logger = provider.getLogger('test'); @@ -99,5 +104,10 @@ describe('ProxyLogger', () => { }); assert.ok(emitCalled); }); + + it('should call enabled from the delegate logger', () => { + logger.enabled(); + assert.ok(enabledCalled); + }); }); }); diff --git a/experimental/packages/sdk-logs/src/LogRecordProcessor.ts b/experimental/packages/sdk-logs/src/LogRecordProcessor.ts index 11baf578be1..24d2c113049 100644 --- a/experimental/packages/sdk-logs/src/LogRecordProcessor.ts +++ b/experimental/packages/sdk-logs/src/LogRecordProcessor.ts @@ -4,8 +4,9 @@ */ import type { Context } from '@opentelemetry/api'; - +import type { InstrumentationScope } from '@opentelemetry/core'; import type { SdkLogRecord } from './export/SdkLogRecord'; +import type { SeverityNumber } from '@opentelemetry/api-logs'; export interface LogRecordProcessor { /** @@ -25,4 +26,16 @@ export interface LogRecordProcessor { * opportunity for processor to do any cleanup required. */ shutdown(): Promise; + + /** + * Tells if the logger is enabled for the given context, severity number and event + * name if provided. + * @param options + */ + enabled?(options: { + context: Context; + instrumentationScope: InstrumentationScope; + severityNumber?: SeverityNumber; + eventName?: string; + }): boolean; } diff --git a/experimental/packages/sdk-logs/src/Logger.ts b/experimental/packages/sdk-logs/src/Logger.ts index ab5abb0e898..17f7e5079b3 100644 --- a/experimental/packages/sdk-logs/src/Logger.ts +++ b/experimental/packages/sdk-logs/src/Logger.ts @@ -6,6 +6,7 @@ import type * as logsAPI from '@opentelemetry/api-logs'; import { SeverityNumber } from '@opentelemetry/api-logs'; import type { InstrumentationScope } from '@opentelemetry/core'; +import type { Context } from '@opentelemetry/api'; import { context, trace, @@ -97,4 +98,53 @@ export class Logger implements logsAPI.Logger { */ logRecordInstance._makeReadonly(); } + + public enabled(options?: { + context?: Context; + severityNumber?: SeverityNumber; + eventName?: string; + }): boolean { + const loggerConfig = this._loggerConfig; + + if (loggerConfig.disabled) { + return false; + } + + // Severity number given and lower than the min configured + const severityNumber = options?.severityNumber; + if ( + typeof severityNumber === 'number' && + severityNumber !== SeverityNumber.UNSPECIFIED && + severityNumber < loggerConfig.minimumSeverity + ) { + return false; + } + + const currentContext = options?.context || context.active(); + // Trace based: the context (given or the active) has a unsampled Span + if (loggerConfig.traceBased) { + const spanContext = trace.getSpanContext(currentContext); + if (spanContext && isSpanContextValid(spanContext)) { + const isSampled = + (spanContext.traceFlags & TraceFlags.SAMPLED) === TraceFlags.SAMPLED; + if (!isSampled) { + return false; + } + } + } + + // Lastly check if there is any enabled processor + const enabledOpts = { + context: currentContext, + instrumentationScope: this.instrumentationScope, + severityNumber: options?.severityNumber, + eventName: options?.eventName, + }; + for (const processor of this._sharedState.processors) { + if (!processor.enabled || processor.enabled(enabledOpts)) { + return true; + } + } + return false; + } } diff --git a/experimental/packages/sdk-logs/src/MultiLogRecordProcessor.ts b/experimental/packages/sdk-logs/src/MultiLogRecordProcessor.ts index 2ebb584adb1..93b1824dd25 100644 --- a/experimental/packages/sdk-logs/src/MultiLogRecordProcessor.ts +++ b/experimental/packages/sdk-logs/src/MultiLogRecordProcessor.ts @@ -3,10 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ +import type { InstrumentationScope } from '@opentelemetry/core'; import { callWithTimeout } from '@opentelemetry/core'; import type { Context } from '@opentelemetry/api'; import type { LogRecordProcessor } from './LogRecordProcessor'; import type { SdkLogRecord } from './export/SdkLogRecord'; +import type { SeverityNumber } from '@opentelemetry/api-logs'; /** * Implementation of the {@link LogRecordProcessor} that simply forwards all @@ -41,4 +43,18 @@ export class MultiLogRecordProcessor implements LogRecordProcessor { public async shutdown(): Promise { await Promise.all(this.processors.map(processor => processor.shutdown())); } + + public enabled(options: { + context: Context; + instrumentationScope: InstrumentationScope; + severityNumber?: SeverityNumber; + eventName?: string; + }): boolean { + for (const processor of this.processors) { + if (!processor.enabled || processor.enabled(options)) { + return true; + } + } + return false; + } } diff --git a/experimental/packages/sdk-logs/src/export/NoopLogRecordProcessor.ts b/experimental/packages/sdk-logs/src/export/NoopLogRecordProcessor.ts index 4e014e77161..0770454871a 100644 --- a/experimental/packages/sdk-logs/src/export/NoopLogRecordProcessor.ts +++ b/experimental/packages/sdk-logs/src/export/NoopLogRecordProcessor.ts @@ -3,18 +3,29 @@ * SPDX-License-Identifier: Apache-2.0 */ +import type { InstrumentationScope } from '@opentelemetry/core'; import type { Context } from '@opentelemetry/api'; import type { LogRecordProcessor } from '../LogRecordProcessor'; import type { ReadableLogRecord } from './ReadableLogRecord'; +import type { SeverityNumber } from '@opentelemetry/api-logs'; export class NoopLogRecordProcessor implements LogRecordProcessor { - forceFlush(): Promise { + public forceFlush(): Promise { return Promise.resolve(); } - onEmit(_logRecord: ReadableLogRecord, _context: Context): void {} + public onEmit(_logRecord: ReadableLogRecord, _context: Context): void {} - shutdown(): Promise { + public shutdown(): Promise { return Promise.resolve(); } + + public enabled(_options: { + context: Context; + instrumentationScope: InstrumentationScope; + severityNumber?: SeverityNumber; + eventName?: string; + }): boolean { + return false; + } } diff --git a/experimental/packages/sdk-logs/test/common/Logger.test.ts b/experimental/packages/sdk-logs/test/common/Logger.test.ts index c247014ee43..03b239651a1 100644 --- a/experimental/packages/sdk-logs/test/common/Logger.test.ts +++ b/experimental/packages/sdk-logs/test/common/Logger.test.ts @@ -463,4 +463,171 @@ describe('Logger', () => { }); }); }); + + describe('enabled', () => { + describe('with default configuration and disabled log processors', () => { + const { logger } = setup(); + + it('should return "false" when called with no options', () => { + assert.ok(!logger.enabled()); + }); + + it('should return "false" when called with a severity number', () => { + assert.ok(!logger.enabled({ severityNumber: SeverityNumber.WARN })); + }); + + it('should return "false" when called with a context with a recording span', () => { + const spanContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.NONE, + }; + const activeContext = trace.setSpanContext(ROOT_CONTEXT, spanContext); + assert.ok(!logger.enabled({ context: activeContext })); + }); + }); + + describe('with default configuration and enabled log processors', () => { + const exporter = new InMemoryLogRecordExporter(); + const loggerProvider = new LoggerProvider({ + processors: [new SimpleLogRecordProcessor(exporter)], + }); + const logger = loggerProvider.getLogger('test-logger'); + + it('should return "true" when called with no options', () => { + assert.ok(logger.enabled()); + }); + + it('should return "true" when called with a severity number', () => { + assert.ok(logger.enabled({ severityNumber: SeverityNumber.WARN })); + }); + + it('should return "true" when called with a context with an unsampled span', () => { + const spanContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.NONE, + }; + const activeContext = trace.setSpanContext(ROOT_CONTEXT, spanContext); + assert.ok(logger.enabled({ context: activeContext })); + }); + }); + + describe('with custom configuration and disabled log processors', () => { + const loggerProvider = new LoggerProvider({ + processors: [new NoopLogRecordProcessor()], + loggerConfigurator: createLoggerConfigurator([ + { + pattern: 'disabled-logger', + config: { disabled: true }, + }, + { + pattern: 'warn-logger', + config: { minimumSeverity: SeverityNumber.WARN }, + }, + { + pattern: 'trace-logger', + config: { traceBased: true }, + }, + ]), + }); + + it('should return "false" no matter the combinations', () => { + const disabledLogger = loggerProvider.getLogger('disabled-logger'); + assert.ok(!disabledLogger.enabled()); + + const warnLogger = loggerProvider.getLogger('warn-logger'); + assert.ok(!warnLogger.enabled({ severityNumber: SeverityNumber.INFO })); + assert.ok( + !warnLogger.enabled({ severityNumber: SeverityNumber.ERROR }) + ); + + const sampledSpanContext = { + traceId: 'e4cda95b652f4a1592b449d5929fda1b', + spanId: '9e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + }; + const traceLogger = loggerProvider.getLogger('trace-logger'); + assert.ok( + !traceLogger.enabled({ + context: trace.setSpanContext(ROOT_CONTEXT, sampledSpanContext), + }) + ); + + const customLogger = loggerProvider.getLogger('custom-logger'); + assert.ok(!customLogger.enabled()); + }); + }); + + describe('with custom configuration and enabled log processors', () => { + const exporter = new InMemoryLogRecordExporter(); + const loggerProvider = new LoggerProvider({ + processors: [new SimpleLogRecordProcessor(exporter)], + loggerConfigurator: createLoggerConfigurator([ + { + pattern: 'disabled-logger', + config: { disabled: true }, + }, + { + pattern: 'warn-logger', + config: { minimumSeverity: SeverityNumber.WARN }, + }, + { + pattern: 'trace-logger', + config: { traceBased: true }, + }, + ]), + }); + + it('should return "false" if the logger is configured to be disabled', () => { + const logger = loggerProvider.getLogger('disabled-logger'); + assert.ok(!logger.enabled()); + }); + + it('should return "true" when severity is greater or equal than the configured', () => { + const logger = loggerProvider.getLogger('warn-logger'); + assert.ok(!logger.enabled({ severityNumber: SeverityNumber.INFO })); + assert.ok(logger.enabled({ severityNumber: SeverityNumber.WARN })); + assert.ok(logger.enabled({ severityNumber: SeverityNumber.ERROR })); + }); + + it('should return "true" when severity is not passed or UNSPECIFIED', () => { + const logger = loggerProvider.getLogger('warn-logger'); + assert.ok(logger.enabled()); + assert.ok( + logger.enabled({ severityNumber: SeverityNumber.UNSPECIFIED }) + ); + }); + + it('should return "true" when trace based and context has a sampled span', () => { + const logger = loggerProvider.getLogger('trace-logger'); + const unsampledSpanContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.NONE, + }; + const sampledSpanContext = { + traceId: 'e4cda95b652f4a1592b449d5929fda1b', + spanId: '9e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + }; + + assert.ok( + !logger.enabled({ + context: trace.setSpanContext(ROOT_CONTEXT, unsampledSpanContext), + }) + ); + assert.ok( + logger.enabled({ + context: trace.setSpanContext(ROOT_CONTEXT, sampledSpanContext), + }) + ); + }); + + it('should return "true" when a log processor is enabled', () => { + const logger = loggerProvider.getLogger('my-logger'); + assert.ok(logger.enabled()); + }); + }); + }); }); diff --git a/experimental/packages/sdk-logs/test/common/MultiLogRecordProcessor.test.ts b/experimental/packages/sdk-logs/test/common/MultiLogRecordProcessor.test.ts index 43d87ef3b84..943a75bba57 100644 --- a/experimental/packages/sdk-logs/test/common/MultiLogRecordProcessor.test.ts +++ b/experimental/packages/sdk-logs/test/common/MultiLogRecordProcessor.test.ts @@ -6,6 +6,7 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; +import { ROOT_CONTEXT } from '@opentelemetry/api'; import type { LogRecordProcessor, ReadableLogRecord } from '../../src'; import { LoggerProvider, @@ -15,17 +16,24 @@ import { import { MultiLogRecordProcessor } from './../../src/MultiLogRecordProcessor'; class TestProcessor implements LogRecordProcessor { + shutdownCalled = false; logRecords: ReadableLogRecord[] = []; onEmit(logRecord: ReadableLogRecord): void { - this.logRecords.push(logRecord); + if (!this.shutdownCalled) { + this.logRecords.push(logRecord); + } } shutdown(): Promise { this.logRecords = []; + this.shutdownCalled = true; return Promise.resolve(); } forceFlush(): Promise { return Promise.resolve(); } + enabled(): boolean { + return !this.shutdownCalled; + } } const setup = (processors: LogRecordProcessor[] = []) => { @@ -194,4 +202,26 @@ describe('MultiLogRecordProcessor', () => { ); }); }); + + describe('enabled', () => { + const processor1 = new TestProcessor(); + const processor2 = new TestProcessor(); + const { multiProcessor } = setup([processor1, processor2]); + const context = ROOT_CONTEXT; + const instrumentationScope = { name: 'test', version: '0.0.0' }; + + it('should return "true" if all processors are enabled', async () => { + assert.ok(multiProcessor.enabled({ context, instrumentationScope })); + }); + + it('should return "true" if any of the processors is enabled', async () => { + await processor1.shutdown(); + assert.ok(multiProcessor.enabled({ context, instrumentationScope })); + }); + + it('should return "false" if all processors are not enabled', async () => { + await processor2.shutdown(); + assert.ok(!multiProcessor.enabled({ context, instrumentationScope })); + }); + }); });