fix(logs): find active log file across date boundaries

Fixes #42875

When gateway runs across midnight, openclaw channels logs was looking
for today's log file instead of the active one. This change makes
the CLI find the most recently modified log file as a fallback.
This commit is contained in:
ethanclaw
2026-03-11 14:42:23 +08:00
committed by vincentkoc
parent bb0461b682
commit fba6b88e86
4 changed files with 65 additions and 6 deletions

View File

@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- CLI/channels logs: reuse the rolling log-file resolver so `openclaw channels logs` falls back to the active dated log across date boundaries without reading unrelated custom log files. Fixes #42875; carries forward #42904 and #43043. Thanks @ethanclaw and @wdskuki.
- NVIDIA/NIM: persist the `NVIDIA_API_KEY` provider marker and mark bundled NVIDIA Chat Completions models as string-content compatible, so NIM models load from `models.json` and OpenAI-compatible subagent calls send plain text content. Fixes #73013 and #50107; refs #73014. Thanks @bautrey, @iot2edge, @ifearghal, and @futhgar.
- Channels/Discord: let text-only configs drop the `GuildVoiceStates` gateway intent and expose a bounded `/gateway/bot` metadata timeout with rate-limited fallback logs, reducing idle CPU and warning floods. Fixes #73709 and #73585. Thanks @sanchezm86 and @trac3r00.
- CLI/plugins: use plugin metadata snapshots for install slot selection and add opt-in plugin lifecycle timing traces, so plugin install avoids runtime-loading the plugin registry for metadata-only decisions. Thanks @shakkernerd.

View File

@@ -37,6 +37,14 @@ function logLine(params: { module: string; message: string }) {
});
}
function readJsonPayload() {
return JSON.parse(String(runtime.log.mock.calls[0]?.[0])) as {
file: string;
channel: string;
lines: Array<{ message: string }>;
};
}
describe("channelsLogsCommand", () => {
let tempDir: string;
let logPath: string;
@@ -75,11 +83,60 @@ describe("channelsLogsCommand", () => {
includeDisabled: true,
}),
);
const payload = JSON.parse(String(runtime.log.mock.calls[0]?.[0])) as {
channel: string;
lines: Array<{ message: string }>;
};
const payload = readJsonPayload();
expect(payload.channel).toBe("external-chat");
expect(payload.lines.map((line) => line.message)).toEqual(["external sent"]);
});
it("falls back to the latest rolling log when the configured rolling file is missing", async () => {
const configuredFile = path.join(tempDir, "openclaw-2026-04-26.log");
const fallbackFile = path.join(tempDir, "openclaw-2026-04-25.log");
setLoggerOverride({ file: configuredFile });
await fs.writeFile(
fallbackFile,
logLine({ module: "gateway/channels/external-chat/send", message: "fallback sent" }),
);
await channelsLogsCommand({ channel: "external-chat", json: true }, runtime);
const payload = readJsonPayload();
expect(payload.file).toBe(fallbackFile);
expect(payload.lines.map((line) => line.message)).toEqual(["fallback sent"]);
});
it("prefers the configured rolling log when it exists", async () => {
const configuredFile = path.join(tempDir, "openclaw-2026-04-26.log");
const fallbackFile = path.join(tempDir, "openclaw-2026-04-25.log");
setLoggerOverride({ file: configuredFile });
await fs.writeFile(
fallbackFile,
logLine({ module: "gateway/channels/external-chat/send", message: "fallback sent" }),
);
await fs.writeFile(
configuredFile,
logLine({ module: "gateway/channels/external-chat/send", message: "current sent" }),
);
await channelsLogsCommand({ channel: "external-chat", json: true }, runtime);
const payload = readJsonPayload();
expect(payload.file).toBe(configuredFile);
expect(payload.lines.map((line) => line.message)).toEqual(["current sent"]);
});
it("does not fall back to rolling logs for a missing custom log file", async () => {
const configuredFile = path.join(tempDir, "custom-channel.log");
const fallbackFile = path.join(tempDir, "openclaw-2026-04-25.log");
setLoggerOverride({ file: configuredFile });
await fs.writeFile(
fallbackFile,
logLine({ module: "gateway/channels/external-chat/send", message: "fallback sent" }),
);
await channelsLogsCommand({ channel: "external-chat", json: true }, runtime);
const payload = readJsonPayload();
expect(payload.file).toBe(configuredFile);
expect(payload.lines).toEqual([]);
});
});

View File

@@ -1,6 +1,7 @@
import fs from "node:fs/promises";
import { normalizeChannelId as normalizeBundledChannelId } from "../../channels/registry.js";
import { getResolvedLoggerSettings } from "../../logging.js";
import { resolveLogFile } from "../../logging/log-tail.js";
import { parseLogLine } from "../../logging/parse-log-line.js";
import {
listPluginContributionIds,
@@ -103,7 +104,7 @@ export async function channelsLogsCommand(
? Math.floor(limitRaw)
: DEFAULT_LIMIT;
const file = getResolvedLoggerSettings().file;
const file = await resolveLogFile(getResolvedLoggerSettings().file);
const rawLines = await readTailLines(file, limit * 4);
const parsed = rawLines
.map(parseLogLine)

View File

@@ -23,7 +23,7 @@ function isRollingLogFile(file: string): boolean {
return ROLLING_LOG_RE.test(path.basename(file));
}
async function resolveLogFile(file: string): Promise<string> {
export async function resolveLogFile(file: string): Promise<string> {
const stat = await fs.stat(file).catch(() => null);
if (stat) {
return file;