Skip to content

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

@fangxiu-wf

Description

@fangxiu-wf

Summary

The logging system uses module-level state (loggingState object and externalTransports Set) that can be duplicated when plugins are loaded via jiti, causing registered log transports from plugins to be invisible to the main process.

This is architecturally identical to the issue fixed in #5190 for diagnostic events.

Steps to reproduce

  1. Create a plugin that registers a custom log transport:
// In plugin loaded via jiti
import { registerLogTransport } from "openclaw/plugin-sdk";

registerLogTransport((logObj) => {
  console.log("Custom transport:", logObj);
});
  1. Configure the plugin in plugins.entries and enable it
  2. Trigger logging from the main gateway process
  3. Observe that the custom transport is never called

Expected behavior

Plugin-registered log transports should receive log records emitted by the main process, similar to how diagnostic event listeners work after the #5190 fix.

Actual behavior

When jiti loads the plugin, it creates a separate module cache. The plugin adds its transport to the plugin module's externalTransports Set, but the main process iterates over its own (empty) externalTransports Set. The two module instances never share state.

Affected code locations:

  • src/logging/state.ts (line 1-17): Module-level loggingState object
  • src/logging/logger.ts (line 40): Module-level externalTransports Set
  • src/logging/logger.ts (line 195-204): registerLogTransport() function
  • src/logging/logger.ts (line 111-113): Transport iteration in buildLogger()

Environment

  • Clawdbot version: 2026.2.1
  • OS: macOS / Linux
  • Install method: pnpm / npm

Logs or screenshots

Debug evidence (similar to #5190):

// In src/logging/logger.ts
export function registerLogTransport(transport: LogTransport): () => void {
  console.error('[DEBUG] Adding transport. Current count:', externalTransports.size);
  externalTransports.add(transport);
  console.error('[DEBUG] After add:', externalTransports.size);
  // ...
}

function buildLogger(settings: ResolvedSettings): TsLogger<LogObj> {
  console.error('[DEBUG] Building logger, transports:', externalTransports.size);
  // ...
}

Expected output:

  • Plugin calls registerLogTransport(): count 0 → 1 ✓
  • Gateway calls buildLogger(): transports = 1 ✓

Actual output:

  • Plugin calls registerLogTransport(): count 0 → 1 ✓
  • Gateway calls buildLogger(): transports = 0 ✗

Proposed solution

Use Symbol.for("openclaw.logging.state") as the global registry key (matching the approach in #5190):

// src/logging/state.ts
const GLOBAL_KEY = Symbol.for("openclaw.logging.state");

type LoggingGlobalState = {
  cachedLogger: unknown;
  cachedSettings: unknown;
  // ... other fields
  externalTransports: Set<LogTransport>;
};

function getGlobalLoggingState(): LoggingGlobalState {
  const g = globalThis as typeof globalThis & { [GLOBAL_KEY]?: LoggingGlobalState };
  if (!g[GLOBAL_KEY]) {
    g[GLOBAL_KEY] = {
      // ... initialize all fields
      externalTransports: new Set(),
    };
  }
  return g[GLOBAL_KEY];
}

export const loggingState = getGlobalLoggingState();

This ensures all module instances (main process + jiti-loaded plugins) share the same state object.

Additional context

Currently, no plugins register custom log transports, so this is a latent bug rather than an active failure. However, consistency with the diagnostic events fix (#5190) is important for maintainability.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions