Skip to content

Commit f52db02

Browse files
100menotu001Craig
andauthored
fix(discord): log component registry error details
Log structured details when Discord persistent component registry state falls back after a store failure. - Format Error name, message, stack, and cause metadata at the Discord registry warning call site. - Forward plugin runtime logger metadata to the underlying child logger. - Add focused regression coverage for the Discord fallback warning and runtime logging adapter. - Add changelog credit for @100menotu001. Fixes #84185. Co-authored-by: OpenClaw Contributor <100menotu001@users.noreply.github.com> Co-authored-by: Craig <froelich@craigs.mac.studio.froho>
1 parent 98af517 commit f52db02

5 files changed

Lines changed: 140 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai
2222

2323
- Infra/json: retry transient `File changed during read` races while loading JSON state so config and state reads recover instead of failing the turn. (#84285)
2424
- Providers/Ollama: resolve configured Ollama Cloud `OLLAMA_API_KEY` markers to the real discovery key so cloud provider entries keep authenticated model catalog access. (#85037)
25+
- Discord: keep persistent component registry fallback warnings actionable by forwarding structured error and cause metadata through the runtime logger. Fixes #84185. (#84190) Thanks @100menotu001.
2526
- Gateway/sessions: preserve compatible session auth profile overrides when switching models within the same provider, including provider-auth aliases. Fixes #81837. (#81886) Thanks @TurboTheTurtle.
2627
- Gateway/status: surface inbound delivery telemetry counters and transport-liveness warnings in `openclaw status --all`. Fixes #49577. (#72724)
2728
- Docker: prune package-excluded plugin source workspaces and dependency closures so runtime images do not keep packages for plugins that were not opted in.

extensions/discord/src/components-registry.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,60 @@ function reportPersistentComponentRegistryError(error: unknown): void {
4848
try {
4949
getOptionalDiscordRuntime()
5050
?.logging.getChildLogger({ plugin: "discord", feature: "component-registry-state" })
51-
.warn("Discord persistent component registry state failed", { error: String(error) });
51+
.warn("Discord persistent component registry state failed", formatRegistryError(error));
5252
} catch {
5353
// Best effort only: persistent state must never break Discord interactions.
5454
}
5555
}
5656

57+
function formatRegistryError(error: unknown): Record<string, unknown> {
58+
if (!(error instanceof Error)) {
59+
return { error: formatRegistryErrorValue(error) };
60+
}
61+
const details: Record<string, unknown> = {
62+
error: String(error),
63+
errorName: error.name,
64+
errorMessage: error.message,
65+
};
66+
if (error.stack) {
67+
details.errorStack = error.stack;
68+
}
69+
const cause = (error as { cause?: unknown }).cause;
70+
if (cause instanceof Error) {
71+
details.errorCause = String(cause);
72+
details.errorCauseName = cause.name;
73+
details.errorCauseMessage = cause.message;
74+
if (cause.stack) {
75+
details.errorCauseStack = cause.stack;
76+
}
77+
} else if (cause !== undefined) {
78+
details.errorCause = formatRegistryErrorValue(cause);
79+
}
80+
return details;
81+
}
82+
83+
function formatRegistryErrorValue(value: unknown): string {
84+
if (typeof value === "string") {
85+
return value;
86+
}
87+
if (
88+
typeof value === "number" ||
89+
typeof value === "boolean" ||
90+
typeof value === "bigint" ||
91+
typeof value === "symbol"
92+
) {
93+
return String(value);
94+
}
95+
if (value === null) {
96+
return "null";
97+
}
98+
try {
99+
return JSON.stringify(value) ?? Object.prototype.toString.call(value);
100+
} catch {
101+
return Object.prototype.toString.call(value);
102+
}
103+
}
104+
57105
function disablePersistentComponentRegistry(error: unknown): void {
58106
persistentRegistryDisabled = true;
59107
persistentComponentStore = undefined;

extensions/discord/src/components.test.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -354,11 +354,14 @@ describe("discord component registry", () => {
354354

355355
it("falls back to the in-memory registry when persistent state cannot open", async () => {
356356
const warn = vi.fn();
357+
const cause = new TypeError("disk busy");
357358
const { setDiscordRuntime } = await import("./runtime.js");
358359
setDiscordRuntime({
359360
state: {
360361
openKeyedStore: vi.fn(() => {
361-
throw new Error("sqlite unavailable");
362+
const error = new Error("sqlite unavailable") as Error & { cause?: unknown };
363+
error.cause = cause;
364+
throw error;
362365
}),
363366
},
364367
logging: { getChildLogger: () => ({ warn }) },
@@ -375,6 +378,16 @@ describe("discord component registry", () => {
375378
expect(fallbackEntry?.label).toBe("Fallback");
376379
expect(typeof fallbackEntry?.createdAt).toBe("number");
377380
expect(typeof fallbackEntry?.expiresAt).toBe("number");
378-
expect(warn).toHaveBeenCalled();
381+
expect(warn).toHaveBeenCalledWith(
382+
"Discord persistent component registry state failed",
383+
expect.objectContaining({
384+
error: "Error: sqlite unavailable",
385+
errorName: "Error",
386+
errorMessage: "sqlite unavailable",
387+
errorCause: "TypeError: disk busy",
388+
errorCauseName: "TypeError",
389+
errorCauseMessage: "disk busy",
390+
}),
391+
);
379392
});
380393
});
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
const loggingMocks = vi.hoisted(() => {
4+
const childLogger = {
5+
debug: vi.fn(),
6+
info: vi.fn(),
7+
warn: vi.fn(),
8+
error: vi.fn(),
9+
};
10+
return {
11+
childLogger,
12+
getChildLogger: vi.fn(() => childLogger),
13+
};
14+
});
15+
16+
vi.mock("../../globals.js", () => ({
17+
shouldLogVerbose: vi.fn(() => false),
18+
}));
19+
20+
vi.mock("../../logging.js", () => ({
21+
getChildLogger: loggingMocks.getChildLogger,
22+
}));
23+
24+
let createRuntimeLogging: typeof import("./runtime-logging.js").createRuntimeLogging;
25+
26+
beforeEach(async () => {
27+
vi.clearAllMocks();
28+
loggingMocks.getChildLogger.mockReturnValue(loggingMocks.childLogger);
29+
({ createRuntimeLogging } = await import("./runtime-logging.js"));
30+
});
31+
32+
describe("createRuntimeLogging", () => {
33+
it("forwards structured metadata to child loggers", () => {
34+
const logging = createRuntimeLogging();
35+
const logger = logging.getChildLogger({ plugin: "discord" }, { level: "warn" });
36+
const meta = {
37+
errorName: "Error",
38+
errorCauseName: "TypeError",
39+
};
40+
41+
logger.debug?.("debug details", meta);
42+
logger.info("info details", meta);
43+
logger.warn("warn details", meta);
44+
logger.error("error details", meta);
45+
46+
expect(loggingMocks.getChildLogger).toHaveBeenCalledWith(
47+
{ plugin: "discord" },
48+
{ level: "warn" },
49+
);
50+
expect(loggingMocks.childLogger.debug).toHaveBeenCalledWith(meta, "debug details");
51+
expect(loggingMocks.childLogger.info).toHaveBeenCalledWith(meta, "info details");
52+
expect(loggingMocks.childLogger.warn).toHaveBeenCalledWith(meta, "warn details");
53+
expect(loggingMocks.childLogger.error).toHaveBeenCalledWith(meta, "error details");
54+
});
55+
});

src/plugins/runtime/runtime-logging.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@ import { getChildLogger } from "../../logging.js";
33
import { normalizeLogLevel } from "../../logging/levels.js";
44
import type { PluginRuntime } from "./types.js";
55

6+
function writeRuntimeLog(
7+
log: (...args: unknown[]) => void,
8+
message: string,
9+
meta?: Record<string, unknown>,
10+
): void {
11+
if (meta && Object.keys(meta).length > 0) {
12+
log(meta, message);
13+
return;
14+
}
15+
log(message);
16+
}
17+
618
export function createRuntimeLogging(): PluginRuntime["logging"] {
719
return {
820
shouldLogVerbose,
@@ -11,10 +23,14 @@ export function createRuntimeLogging(): PluginRuntime["logging"] {
1123
level: opts?.level ? normalizeLogLevel(opts.level) : undefined,
1224
});
1325
return {
14-
debug: (message) => logger.debug?.(message),
15-
info: (message) => logger.info(message),
16-
warn: (message) => logger.warn(message),
17-
error: (message) => logger.error(message),
26+
debug: (message, meta) => {
27+
if (logger.debug) {
28+
writeRuntimeLog(logger.debug.bind(logger), message, meta);
29+
}
30+
},
31+
info: (message, meta) => writeRuntimeLog(logger.info.bind(logger), message, meta),
32+
warn: (message, meta) => writeRuntimeLog(logger.warn.bind(logger), message, meta),
33+
error: (message, meta) => writeRuntimeLog(logger.error.bind(logger), message, meta),
1834
};
1935
},
2036
};

0 commit comments

Comments
 (0)