From d56344056484ac658ecd019512f502e7147c78bf Mon Sep 17 00:00:00 2001 From: Charles Cheng Date: Thu, 18 Jun 2026 11:31:41 +0800 Subject: [PATCH 1/3] fix(sdk-logs): stop Logger.emit() doing work after LoggerProvider shutdown emit() previously built a LogRecord, reported logger metrics and called processor onEmit even after the owning LoggerProvider had shut down. Track shutdown state on LoggerProviderSharedState, set it in LoggerProvider shutdown, and short-circuit both emit() and enabled() so existing Loggers do no work once the provider is shut down (matching the OpenTelemetry Java SDK). --- experimental/packages/sdk-logs/src/Logger.ts | 12 ++++++ .../packages/sdk-logs/src/LoggerProvider.ts | 3 ++ .../src/internal/LoggerProviderSharedState.ts | 5 +++ .../sdk-logs/test/common/Logger.test.ts | 43 +++++++++++++++++++ 4 files changed, 63 insertions(+) diff --git a/experimental/packages/sdk-logs/src/Logger.ts b/experimental/packages/sdk-logs/src/Logger.ts index 985d5a2edd9..bca024acdbb 100644 --- a/experimental/packages/sdk-logs/src/Logger.ts +++ b/experimental/packages/sdk-logs/src/Logger.ts @@ -38,6 +38,12 @@ export class Logger implements ILogger { } public emit(logRecord: LogRecord): void { + // Once the owning LoggerProvider has been shut down, emit() must not do any + // work (record construction, metrics reporting, processor onEmit). This + // mirrors the shutdown guard in the OpenTelemetry Java SDK. + if (this._sharedState.hasShutdown) { + return; + } const currentContext = logRecord.context || context.active(); if (!this.enabled(logRecord)) { return; @@ -74,6 +80,12 @@ export class Logger implements ILogger { severityNumber?: SeverityNumber; eventName?: string; }): boolean { + // After the owning LoggerProvider has been shut down, emit() is a no-op, so + // report not-enabled to let callers skip building log records altogether. + if (this._sharedState.hasShutdown) { + return false; + } + const loggerConfig = this._loggerConfig; if (loggerConfig.disabled) { diff --git a/experimental/packages/sdk-logs/src/LoggerProvider.ts b/experimental/packages/sdk-logs/src/LoggerProvider.ts index 969bfaa779e..6d7c7937e39 100644 --- a/experimental/packages/sdk-logs/src/LoggerProvider.ts +++ b/experimental/packages/sdk-logs/src/LoggerProvider.ts @@ -121,6 +121,9 @@ export class LoggerProvider implements ILoggerProvider { } private _shutdown(): Promise { + // Mark the shared state as shut down so any Logger.emit() calls (including + // ones that race with the in-flight processor shutdown) become no-ops. + this._sharedState.hasShutdown = true; return this._sharedState.activeProcessor.shutdown(); } } diff --git a/experimental/packages/sdk-logs/src/internal/LoggerProviderSharedState.ts b/experimental/packages/sdk-logs/src/internal/LoggerProviderSharedState.ts index bd7c13037a0..e1f510971ed 100644 --- a/experimental/packages/sdk-logs/src/internal/LoggerProviderSharedState.ts +++ b/experimental/packages/sdk-logs/src/internal/LoggerProviderSharedState.ts @@ -42,6 +42,11 @@ export class LoggerProviderSharedState { readonly logRecordLimits: Required; readonly processors: LogRecordProcessor[]; readonly loggerMetrics: LoggerMetrics; + /** + * Whether the owning LoggerProvider has been shut down. Once `true`, Loggers + * sharing this state must not do any work on `emit()`. + */ + hasShutdown = false; private _loggerConfigurator: LoggerConfigurator; private _loggerConfigs: Map> = new Map(); diff --git a/experimental/packages/sdk-logs/test/common/Logger.test.ts b/experimental/packages/sdk-logs/test/common/Logger.test.ts index 4c6443bed6d..c3f74f38547 100644 --- a/experimental/packages/sdk-logs/test/common/Logger.test.ts +++ b/experimental/packages/sdk-logs/test/common/Logger.test.ts @@ -97,6 +97,49 @@ describe('Logger', () => { assert.ok(callSpy.called); }); + it('should not do any work when emitting after the LoggerProvider has shut down', async () => { + const { logger, loggerProvider, logProcessor } = + setupLoggerProvider('simple'); + const onEmitSpy = sinon.spy(logProcessor, 'onEmit'); + const makeReadonlySpy = sinon.spy( + LogRecordImpl.prototype, + '_makeReadonly' + ); + + try { + assert.strictEqual( + logger.enabled(), + true, + 'sanity: logger should be enabled before shutdown' + ); + + await loggerProvider.shutdown(); + + assert.strictEqual( + logger.enabled(), + false, + 'enabled() should report false after the provider has shut down' + ); + + logger.emit({ + body: 'log body emitted after shutdown', + }); + + assert.strictEqual( + onEmitSpy.called, + false, + 'processor.onEmit should not be called after the provider has shut down' + ); + assert.strictEqual( + makeReadonlySpy.called, + false, + 'no LogRecord should be constructed after the provider has shut down' + ); + } finally { + makeReadonlySpy.restore(); + } + }); + it('should make log record instance readonly after emit it', () => { const { logger } = setupLoggerProvider('simple'); const makeOnlySpy = sinon.spy(LogRecordImpl.prototype, '_makeReadonly'); From ed015f92631e9d27de3c70436fe9377e28154510 Mon Sep 17 00:00:00 2001 From: Charles Cheng Date: Thu, 18 Jun 2026 11:32:47 +0800 Subject: [PATCH 2/3] docs(changelog): add entry for sdk-logs post-shutdown emit fix (#6826) --- experimental/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index 7639a7934cf..f22a1f50b12 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -17,6 +17,7 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2 ### :bug: Bug Fixes +* fix(sdk-logs): stop `Logger.emit()` doing work (record construction, metrics, processor `onEmit`) after the `LoggerProvider` has shut down [#6826](https://github.com/open-telemetry/opentelemetry-js/pull/6826) @anneheartrecord * fix(sdk-logs): default BatchLogRecordProcessor `scheduleDelayMillis` is 1000 [#6796](https://github.com/open-telemetry/opentelemetry-js/pull/6796) @trentm * fix(configuration): percent-decode keys and values in `resource.attributes_list` per spec [#6787](https://github.com/open-telemetry/opentelemetry-js/pull/6787) @MikeGoldsmith * fix(configuration): default `log_level` to `info` in env-based config initialization for consistency with file-based config [#6788](https://github.com/open-telemetry/opentelemetry-js/pull/6788) @MikeGoldsmith From d818341dd0d1c0f0e3e4cbaa91be3eda8a9ddc56 Mon Sep 17 00:00:00 2001 From: Charles Cheng Date: Fri, 19 Jun 2026 17:55:40 +0800 Subject: [PATCH 3/3] =?UTF-8?q?refactor(sdk-logs):=20address=20review=20ni?= =?UTF-8?q?ts=20=E2=80=94=20remove=20redundant=20shutdown=20guard=20and=20?= =?UTF-8?q?trim=20verbose=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove early `hasShutdown` short-circuit from `emit()`; the subsequent `enabled()` call already gates on shutdown, so the guard was redundant - Drop two-line explanatory comments before guards that speak for themselves in Logger.ts, LoggerProvider.ts, and LoggerProviderSharedState.ts Suggested by trentm and seconded by pichlermarc in code review. --- experimental/packages/sdk-logs/src/Logger.ts | 8 -------- experimental/packages/sdk-logs/src/LoggerProvider.ts | 2 -- .../sdk-logs/src/internal/LoggerProviderSharedState.ts | 4 ---- 3 files changed, 14 deletions(-) diff --git a/experimental/packages/sdk-logs/src/Logger.ts b/experimental/packages/sdk-logs/src/Logger.ts index bca024acdbb..b8019d0ab85 100644 --- a/experimental/packages/sdk-logs/src/Logger.ts +++ b/experimental/packages/sdk-logs/src/Logger.ts @@ -38,12 +38,6 @@ export class Logger implements ILogger { } public emit(logRecord: LogRecord): void { - // Once the owning LoggerProvider has been shut down, emit() must not do any - // work (record construction, metrics reporting, processor onEmit). This - // mirrors the shutdown guard in the OpenTelemetry Java SDK. - if (this._sharedState.hasShutdown) { - return; - } const currentContext = logRecord.context || context.active(); if (!this.enabled(logRecord)) { return; @@ -80,8 +74,6 @@ export class Logger implements ILogger { severityNumber?: SeverityNumber; eventName?: string; }): boolean { - // After the owning LoggerProvider has been shut down, emit() is a no-op, so - // report not-enabled to let callers skip building log records altogether. if (this._sharedState.hasShutdown) { return false; } diff --git a/experimental/packages/sdk-logs/src/LoggerProvider.ts b/experimental/packages/sdk-logs/src/LoggerProvider.ts index 6d7c7937e39..545b6e05b6e 100644 --- a/experimental/packages/sdk-logs/src/LoggerProvider.ts +++ b/experimental/packages/sdk-logs/src/LoggerProvider.ts @@ -121,8 +121,6 @@ export class LoggerProvider implements ILoggerProvider { } private _shutdown(): Promise { - // Mark the shared state as shut down so any Logger.emit() calls (including - // ones that race with the in-flight processor shutdown) become no-ops. this._sharedState.hasShutdown = true; return this._sharedState.activeProcessor.shutdown(); } diff --git a/experimental/packages/sdk-logs/src/internal/LoggerProviderSharedState.ts b/experimental/packages/sdk-logs/src/internal/LoggerProviderSharedState.ts index e1f510971ed..4920810b33a 100644 --- a/experimental/packages/sdk-logs/src/internal/LoggerProviderSharedState.ts +++ b/experimental/packages/sdk-logs/src/internal/LoggerProviderSharedState.ts @@ -42,10 +42,6 @@ export class LoggerProviderSharedState { readonly logRecordLimits: Required; readonly processors: LogRecordProcessor[]; readonly loggerMetrics: LoggerMetrics; - /** - * Whether the owning LoggerProvider has been shut down. Once `true`, Loggers - * sharing this state must not do any work on `emit()`. - */ hasShutdown = false; private _loggerConfigurator: LoggerConfigurator; private _loggerConfigs: Map> = new Map();