Files
openclaw/src/plugins/runtime/runtime-logging.ts
T
Alex Knight 1f0c6a66a6 fix(plugins): plugin loggers drop writes after the log level is raised at runtime (#97617)
* fix(plugins): plugin loggers drop writes after the log level is raised at runtime

The plugin runtime logging facade captured a single tslog child logger per
getChildLogger() call. tslog snapshots a sublogger's min level at creation, so a
long-lived plugin logger (e.g. a channel monitor that runs for the whole gateway
session) kept dropping debug/verbose writes after the log level was raised at
runtime, even though shouldLogVerbose()/isFileLogLevelEnabled() reported the new
level. This made channels like Mattermost go dark while core subsystem loggers
(which re-resolve per emit) kept logging.

Resolve the child logger per call so it always reflects the current level, with a
cheap isFileLogLevelEnabled pre-gate (skipped for explicit overrides) to avoid
building a sublogger when the level is disabled. Fixes every channel that holds a
long-lived monitor logger (mattermost, matrix, msteams, irc, nextcloud-talk) at
the facade boundary with no plugin-code changes.

* test(plugins): type runtime log-level mock

---------

Co-authored-by: Alex Knight <15041791+amknight@users.noreply.github.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-06-28 19:55:37 -07:00

50 lines
1.8 KiB
TypeScript

// Runtime logging helpers route plugin runtime logs through OpenClaw verbosity controls.
import { shouldLogVerbose } from "../../globals.js";
import { getChildLogger, isFileLogLevelEnabled } from "../../logging.js";
import { normalizeLogLevel } from "../../logging/levels.js";
import type { PluginRuntime } from "./types.js";
function writeRuntimeLog(
log: (...args: unknown[]) => void,
message: string,
meta?: Record<string, unknown>,
): void {
if (meta && Object.keys(meta).length > 0) {
log(meta, message);
return;
}
log(message);
}
type RuntimeLogMethod = "debug" | "info" | "warn" | "error";
/** Creates the plugin runtime logging facade. */
export function createRuntimeLogging(): PluginRuntime["logging"] {
return {
shouldLogVerbose,
getChildLogger: (bindings, opts) => {
const overrideLevel = opts?.level ? normalizeLogLevel(opts.level) : undefined;
const childOpts = overrideLevel ? { level: overrideLevel } : undefined;
// Resolve the child logger per call: tslog snapshots a sublogger's min level at
// creation, so a long-lived plugin logger (e.g. a channel monitor) would keep
// dropping writes after the log level is raised at runtime even though
// shouldLogVerbose() reports the new level. Skip the pre-gate when an override is
// set since it may be more permissive than the current file level.
const emit =
(level: RuntimeLogMethod) => (message: string, meta?: Record<string, unknown>) => {
if (!overrideLevel && !isFileLogLevelEnabled(level)) {
return;
}
const logger = getChildLogger(bindings, childOpts);
writeRuntimeLog(logger[level].bind(logger), message, meta);
};
return {
debug: emit("debug"),
info: emit("info"),
warn: emit("warn"),
error: emit("error"),
};
},
};
}