mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-15 23:24:03 -06:00
1f0c6a66a6
* 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>
92 lines
3.5 KiB
TypeScript
92 lines
3.5 KiB
TypeScript
// Runtime logging tests cover plugin runtime log routing and verbosity behavior.
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const loggingMocks = vi.hoisted(() => {
|
|
const childLogger = {
|
|
debug: vi.fn(),
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
};
|
|
const isFileLogLevelEnabled = vi.fn((_level: string) => true);
|
|
return {
|
|
childLogger,
|
|
getChildLogger: vi.fn(() => childLogger),
|
|
isFileLogLevelEnabled,
|
|
};
|
|
});
|
|
|
|
vi.mock("../../globals.js", () => ({
|
|
shouldLogVerbose: vi.fn(() => false),
|
|
}));
|
|
|
|
vi.mock("../../logging.js", () => ({
|
|
getChildLogger: loggingMocks.getChildLogger,
|
|
isFileLogLevelEnabled: loggingMocks.isFileLogLevelEnabled,
|
|
}));
|
|
|
|
let createRuntimeLogging: typeof import("./runtime-logging.js").createRuntimeLogging;
|
|
|
|
beforeEach(async () => {
|
|
vi.clearAllMocks();
|
|
loggingMocks.getChildLogger.mockReturnValue(loggingMocks.childLogger);
|
|
loggingMocks.isFileLogLevelEnabled.mockReturnValue(true);
|
|
({ createRuntimeLogging } = await import("./runtime-logging.js"));
|
|
});
|
|
|
|
describe("createRuntimeLogging", () => {
|
|
it("forwards structured metadata to child loggers", () => {
|
|
const logging = createRuntimeLogging();
|
|
const logger = logging.getChildLogger({ plugin: "discord" }, { level: "warn" });
|
|
const meta = {
|
|
errorName: "Error",
|
|
errorCauseName: "TypeError",
|
|
};
|
|
|
|
logger.debug?.("debug details", meta);
|
|
logger.info("info details", meta);
|
|
logger.warn("warn details", meta);
|
|
logger.error("error details", meta);
|
|
|
|
expect(loggingMocks.getChildLogger).toHaveBeenCalledWith(
|
|
{ plugin: "discord" },
|
|
{ level: "warn" },
|
|
);
|
|
expect(loggingMocks.childLogger.debug).toHaveBeenCalledWith(meta, "debug details");
|
|
expect(loggingMocks.childLogger.info).toHaveBeenCalledWith(meta, "info details");
|
|
expect(loggingMocks.childLogger.warn).toHaveBeenCalledWith(meta, "warn details");
|
|
expect(loggingMocks.childLogger.error).toHaveBeenCalledWith(meta, "error details");
|
|
});
|
|
|
|
it("resolves the child logger per call so a runtime log-level change takes effect", () => {
|
|
const logging = createRuntimeLogging();
|
|
// Mirror a long-lived channel monitor: capture the logger once, log later.
|
|
const logger = logging.getChildLogger({ module: "mattermost" });
|
|
|
|
// Level is below debug when the monitor starts: the write is dropped.
|
|
loggingMocks.isFileLogLevelEnabled.mockReturnValue(false);
|
|
logger.debug?.("dropped before debug enabled");
|
|
expect(loggingMocks.childLogger.debug).not.toHaveBeenCalled();
|
|
expect(loggingMocks.getChildLogger).not.toHaveBeenCalled();
|
|
|
|
// Log level raised to debug on the running gateway: the same captured logger
|
|
// must now write, because it re-resolves the child logger per call.
|
|
loggingMocks.isFileLogLevelEnabled.mockReturnValue(true);
|
|
logger.debug?.("written after debug enabled");
|
|
expect(loggingMocks.getChildLogger).toHaveBeenCalledWith({ module: "mattermost" }, undefined);
|
|
expect(loggingMocks.childLogger.debug).toHaveBeenCalledWith("written after debug enabled");
|
|
});
|
|
|
|
it("pre-gates on the current file level when no override is set", () => {
|
|
loggingMocks.isFileLogLevelEnabled.mockImplementation((level: string) => level !== "debug");
|
|
const logging = createRuntimeLogging();
|
|
const logger = logging.getChildLogger({ module: "mattermost" });
|
|
|
|
logger.debug?.("debug suppressed at info");
|
|
logger.info("info written at info");
|
|
|
|
expect(loggingMocks.childLogger.debug).not.toHaveBeenCalled();
|
|
expect(loggingMocks.childLogger.info).toHaveBeenCalledWith("info written at info");
|
|
});
|
|
});
|