Skip to content

fix(logging): use Symbol.for for externalTransports to survive jiti module isolation#12475

Closed
Yida-Dev wants to merge 1 commit intoopenclaw:mainfrom
Yida-Dev:fix/plugin-log-transport-module-isolation
Closed

fix(logging): use Symbol.for for externalTransports to survive jiti module isolation#12475
Yida-Dev wants to merge 1 commit intoopenclaw:mainfrom
Yida-Dev:fix/plugin-log-transport-module-isolation

Conversation

@Yida-Dev
Copy link
Copy Markdown
Contributor

@Yida-Dev Yida-Dev commented Feb 9, 2026

Summary

Fixes #12473

Plugins loaded via jiti get their own module cache, so the module-level externalTransports Set in logger.ts becomes a separate instance in the plugin context. Any transport registered by a plugin via registerLogTransport() is silently lost when buildLogger() iterates the main process's Set.

Root cause

Main process:   import logger.ts  ->  externalTransports = new Set() (A)
Plugin (jiti):  import logger.ts  ->  externalTransports = new Set() (B)

Plugin calls registerLogTransport(fn)  ->  adds to Set B
buildLogger() iterates Set A           ->  fn is missing

Fix

Store the Set on globalThis[Symbol.for("openclaw.logging.externalTransports")] so all module instances -- regardless of how they were loaded -- share the same collection.

This matches the pattern already established in:

  • src/infra/warning-filter.ts (Symbol.for("openclaw.warning-filter"))
  • src/plugins/runtime.ts (Symbol.for("openclaw.pluginRegistryState"))

Files changed

File Change
src/logging/logger.ts Replace new Set() with globalThis[Symbol.for()] getter
src/logging/logger-transport.test.ts Verify shared Set and register/unregister lifecycle

Test plan

  • New test: externalTransports is stored on globalThis via Symbol.for
  • New test: registerLogTransport adds to and removes from the shared global Set
  • All 27 existing logging + diagnostics-otel tests pass

Greptile Overview

Greptile Summary

This PR changes src/logging/logger.ts to store the externalTransports Set on globalThis[Symbol.for("openclaw.logging.externalTransports")] so transports registered from jiti-loaded plugin module instances are visible to the main process logger builder.

It also adds src/logging/logger-transport.test.ts to validate that the Set is shared via Symbol.for and that registerLogTransport() mutates the shared Set (including unregister cleanup).

Confidence Score: 4/5

  • Mostly safe to merge, but the newly added tests are order-dependent and can fail in isolation/sharded runs.
  • The production change in logger.ts is small and follows established patterns (Symbol.for + globalThis). The only clear issues found are in the new test file, which assumes logger.js has already been executed and dereferences the global Set before ensuring initialization.
  • src/logging/logger-transport.test.ts

…odule isolation

Plugins loaded via jiti get their own module cache, so the module-level
`externalTransports` Set in logger.ts is a different instance in the
plugin context than in the main process.  Any transport registered by a
plugin is silently lost when buildLogger() iterates the main Set.

Store the Set on `globalThis[Symbol.for("openclaw.logging.externalTransports")]`
so all module instances share the same collection, matching the pattern
already used by warning-filter.ts and plugins/runtime.ts.

Closes openclaw#12473

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 file reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +7 to +12
it("externalTransports is stored on globalThis via Symbol.for", () => {
// Importing logger.ts should have populated the global key.
const g = globalThis as typeof globalThis & {
[key: symbol]: Set<LogTransport> | undefined;
};
expect(g[EXTERNAL_TRANSPORTS_KEY]).toBeInstanceOf(Set);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Order-dependent test setup

This test expects globalThis[Symbol.for(...)] to be populated by importing logger.ts, but this file only imports LogTransport as a type (erased at runtime), so ./logger.js may never run. That makes the assertion fail when this test runs in isolation or in a fresh worker. Consider doing a real await import("./logger.js") (or importing a runtime symbol) before reading g[EXTERNAL_TRANSPORTS_KEY] so initialization is deterministic.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/logging/logger-transport.test.ts
Line: 7:12

Comment:
**Order-dependent test setup**

This test expects `globalThis[Symbol.for(...)]` to be populated by importing `logger.ts`, but this file only imports `LogTransport` as a type (erased at runtime), so `./logger.js` may never run. That makes the assertion fail when this test runs in isolation or in a fresh worker. Consider doing a real `await import("./logger.js")` (or importing a runtime symbol) before reading `g[EXTERNAL_TRANSPORTS_KEY]` so initialization is deterministic.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +18 to +22
const g = globalThis as typeof globalThis & {
[key: symbol]: Set<LogTransport> | undefined;
};
const set = g[EXTERNAL_TRANSPORTS_KEY]!;
const sizeBefore = set.size;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dereference before initialization

In this test, const set = g[EXTERNAL_TRANSPORTS_KEY]!; happens before importing ./logger.js, so set can be undefined and the non-null assertion will crash. Import ./logger.js first (or otherwise ensure initialization) before reading the global key, so the test doesn’t depend on previous tests/order.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/logging/logger-transport.test.ts
Line: 18:22

Comment:
**Dereference before initialization**

In this test, `const set = g[EXTERNAL_TRANSPORTS_KEY]!;` happens *before* importing `./logger.js`, so `set` can be `undefined` and the non-null assertion will crash. Import `./logger.js` first (or otherwise ensure initialization) before reading the global key, so the test doesn’t depend on previous tests/order.

How can I resolve this? If you propose a fix, please make it concise.

@openclaw-barnacle
Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added the stale Marked as stale due to inactivity label Feb 21, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the stale Marked as stale due to inactivity label Feb 23, 2026
@steipete
Copy link
Copy Markdown
Contributor

Closing as AI-assisted stale-fix triage.

Linked issue #12473 ("[Bug]: Plugin log transports lost due to module isolation in logging system") is currently CLOSED and was closed on 2026-02-13T15:11:52Z with state reason NOT_PLANNED.
Given that issue state, this fix PR is no longer needed in the active queue and is being closed as stale.

If the underlying bug is still reproducible on current main, please reopen this PR (or open a new focused fix PR) and reference both #12473 and #12475 for fast re-triage.

@steipete
Copy link
Copy Markdown
Contributor

Closed after AI-assisted stale-fix triage (closed issue duplicate/stale fix).

@steipete steipete closed this Feb 24, 2026
sreis added a commit to triclambert/openclaw that referenced this pull request Feb 26, 2026
The externalTransports Set in logger.ts is module-scoped, so plugin
bundles that receive a separate copy of the module have their own
isolated registry. registerLogTransport() from the OTel plugin may
register on a different Set than the one the gateway logger reads.

Fix: use globalThis.__openclawLogTransportState singleton, matching the
pattern PR openclaw#12897 established for the diagnostic event bus.

Related openclaw#12475
sreis added a commit to triclambert/openclaw that referenced this pull request Feb 26, 2026
The externalTransports Set in logger.ts is module-scoped, so plugin
bundles that receive a separate copy of the module have their own
isolated registry. registerLogTransport() from the OTel plugin may
register on a different Set than the one the gateway logger reads.

Fix: use globalThis.__openclawLogTransportState singleton, matching the
pattern PR openclaw#12897 established for the diagnostic event bus.

Related openclaw#12475
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Plugin log transports lost due to module isolation in logging system

2 participants