Skip to main content

Logging

Plugins have two logging surfaces:
  • api.logger (PluginLogger) — the plugin-scoped logger injected into register(). Use this for all logging inside your plugin’s register function and tool/hook handlers.
  • createSubsystemLogger — creates a named logger for standalone modules, background workers, or code that runs outside the plugin registration context.
Both are imported from openclaw/plugin-sdk. Neither is console.log — bare console.log bypasses the gateway’s log routing and should never be used in production code.

api.logger (inside plugins)

api.logger is a PluginLogger tied to your plugin’s id. It is the correct logger for all plugin handler code:
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";

export default function register(api: OpenClawPluginApi): void {
  const { logger } = api;

  api.registerTool({
    name: "my_tool",
    description: "...",
    parameters: Type.Object({ query: Type.String() }),
    async execute(_id, params) {
      logger.info("tool called", { query: params.query });
      try {
        const result = await doWork(params.query);
        logger.debug("tool succeeded", { result });
        return { content: [{ type: "text", text: result }] };
      } catch (err) {
        logger.error("tool failed", { error: err });
        throw err;
      }
    },
  });
}
PluginLogger methods: info, warn, error, debug.
api.logger.debug may not be a no-op even in production — it respects the gateway’s configured log level. Only call debug for information that would be too noisy at info. Never debug-log secrets or full payloads.

createSubsystemLogger (standalone modules)

For code that lives outside a plugin (a shared utility, a background worker, a standalone script) use createSubsystemLogger:
import { createSubsystemLogger } from "openclaw/plugin-sdk";

const log = createSubsystemLogger("my-plugin/worker");

export async function runWorker(): Promise<void> {
  log.info("worker started");

  try {
    await doWork();
    log.info("worker finished");
  } catch (err) {
    log.error("worker failed", { error: err });
  }
}
The subsystem name appears in log output as a prefix (e.g. [my-plugin/worker]). Keep names short and slash-separated to match the convention used by core subsystems.

Available methods

SubsystemLogger has more methods than PluginLogger:
MethodLevelNotes
log.trace(msg, meta?)TRACEVery high-frequency events; stripped in most deployments
log.debug(msg, meta?)DEBUGVerbose diagnostic info
log.info(msg, meta?)INFONormal operational events
log.warn(msg, meta?)WARNUnexpected but recoverable
log.error(msg, meta?)ERRORActionable failures
log.fatal(msg, meta?)FATALGateway-critical failures
log.raw(msg)Bypasses formatting; writes the string as-is to the console
log.child(name)Returns a child logger with a suffixed name

Child loggers

Use child() to add context to a logger without changing the subsystem name:
const log = createSubsystemLogger("my-plugin/cache");

function processItem(id: string) {
  const itemLog = log.child(id);   // subsystem: "my-plugin/cache/abc123"
  itemLog.info("processing");
  // ...
}
Child loggers inherit the parent’s log level and output targets.

Checking log level before expensive work

isEnabled(level, target?) lets you skip expensive serialization when a level is suppressed:
if (log.isEnabled("debug")) {
  log.debug("full payload", { payload: expensiveSerialize(data) });
}
target values: "any" (default), "console", "file" — checks whether the level is active for a specific output target.

Which logger to use

ContextUse
Inside register(), tool execute(), or a hook handlerapi.logger
A standalone module imported by the plugincreateSubsystemLogger("my-plugin/<module>")
A background worker or service that runs between requestscreateSubsystemLogger("my-plugin/<service>")

What not to do

// ❌ Bypasses gateway log routing — never do this in production code
console.log("tool called", params);

// ❌ Logs inside execute() via api — api is the registration-time api, not in scope
export default function register(api: OpenClawPluginApi): void {
  api.registerTool({
    async execute(_id, params) {
      api.logger.info("...");   // ❌ api is captured correctly here via closure, this is fine...
      // BUT never re-import or re-create the logger per-call:
      const log = createSubsystemLogger("my-plugin");  // ❌ creates a new logger instance per call
    },
  });
}

// ✅ Capture once at registration time
export default function register(api: OpenClawPluginApi): void {
  const { logger } = api;   // ✅ captured once

  api.registerTool({
    async execute(_id, params) {
      logger.info("...");   // ✅ uses the captured logger
    },
  });
}

Async safety

logger.info() and other log methods are synchronous — they do not block the event loop. Do not await them.