mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 13:26:04 -06:00
fix(logging): report unavailable log tails (#126481)
Only missing-path metadata failures remain empty successes. Operational filesystem errors now reach existing Gateway, UI, CLI, channel, and diagnostics error paths.\n\nCloses #126467
This commit is contained in:
committed by
GitHub
parent
13fd888281
commit
c4c9f6d464
@@ -6,6 +6,16 @@ import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
|
||||
import { resetLogger, setLoggerOverride } from "../logging.js";
|
||||
|
||||
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
||||
const metadataBoundaries = [
|
||||
"configured stat",
|
||||
"rolling readdir",
|
||||
"candidate stat",
|
||||
"final stat",
|
||||
] as const;
|
||||
const operationalErrorCodes = ["EACCES", "EIO", "EMFILE"] as const;
|
||||
const operationalMetadataFailures = metadataBoundaries.flatMap((boundary) =>
|
||||
operationalErrorCodes.map((code) => ({ boundary, code })),
|
||||
);
|
||||
|
||||
const resolvedRedaction = { mode: "tools" as const, patterns: [/custom-secret-[a-z]+/g] };
|
||||
type PositionalRead = (
|
||||
@@ -123,6 +133,44 @@ describe("readConfiguredLogTail", () => {
|
||||
expect(result.lines[0]?.trimEnd()).toBe("first-line-in-window");
|
||||
});
|
||||
|
||||
it.each(operationalMetadataFailures)(
|
||||
"rethrows $code from the $boundary boundary",
|
||||
async ({ boundary, code }) => {
|
||||
const tempDir = tempDirs.make("openclaw-log-tail-");
|
||||
const configured = path.join(tempDir, "openclaw-2026-01-22.log");
|
||||
const candidate = path.join(tempDir, "openclaw-2026-01-21.log");
|
||||
const error = Object.assign(new Error(`${code} injected`), { code });
|
||||
const realStat = fs.stat.bind(fs);
|
||||
|
||||
if (boundary === "candidate stat") {
|
||||
await fs.writeFile(candidate, "candidate\n");
|
||||
} else if (boundary !== "rolling readdir") {
|
||||
await fs.writeFile(configured, "configured\n");
|
||||
}
|
||||
setLoggerOverride({ file: configured });
|
||||
|
||||
if (boundary === "configured stat") {
|
||||
vi.spyOn(fs, "stat").mockRejectedValueOnce(error);
|
||||
} else if (boundary === "rolling readdir") {
|
||||
vi.spyOn(fs, "readdir").mockRejectedValueOnce(error);
|
||||
} else if (boundary === "candidate stat") {
|
||||
vi.spyOn(fs, "stat").mockImplementation(async (...args: Parameters<typeof fs.stat>) => {
|
||||
if (String(args[0]) === candidate) {
|
||||
throw error;
|
||||
}
|
||||
return realStat(...args);
|
||||
});
|
||||
} else {
|
||||
vi.spyOn(fs, "stat")
|
||||
.mockImplementationOnce((...args: Parameters<typeof fs.stat>) => realStat(...args))
|
||||
.mockRejectedValueOnce(error);
|
||||
}
|
||||
|
||||
const { readConfiguredLogTail } = await import("./log-tail.js");
|
||||
await expect(readConfiguredLogTail()).rejects.toBe(error);
|
||||
},
|
||||
);
|
||||
|
||||
it("falls back only within the active profile's rolling log family", async () => {
|
||||
const tempDir = tempDirs.make("openclaw-log-tail-");
|
||||
const missing = path.join(tempDir, "openclaw-2026-01-22.log");
|
||||
|
||||
+12
-4
@@ -1,6 +1,7 @@
|
||||
// Log tail helpers read recent log lines with optional parsing and redaction.
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { isMissingPathError } from "../infra/errno.js";
|
||||
import { readFileWindowFully } from "../infra/file-read.js";
|
||||
import { clamp } from "../utils.js";
|
||||
import { isRollingLogFilePath, isSameRollingLogFileFamily } from "./log-file-path.js";
|
||||
@@ -15,6 +16,13 @@ const DEFAULT_MAX_BYTES = 250_000;
|
||||
const MAX_LIMIT = 5000;
|
||||
const MAX_BYTES = 1_000_000;
|
||||
|
||||
function missingPathToNull(error: unknown): null {
|
||||
if (!isMissingPathError(error)) {
|
||||
throw error;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Payload returned to log-tail callers with cursor and truncation metadata. */
|
||||
export type LogTailPayload = {
|
||||
file: string;
|
||||
@@ -32,7 +40,7 @@ type ParsedLogTailPayload = Omit<LogTailPayload, "lines"> & {
|
||||
|
||||
/** Resolves a rolling daily log path to the newest existing rolling log when needed. */
|
||||
async function resolveLogFile(file: string, options?: { rolling?: boolean }): Promise<string> {
|
||||
const stat = await fs.stat(file).catch(() => null);
|
||||
const stat = await fs.stat(file).catch(missingPathToNull);
|
||||
if (stat) {
|
||||
return file;
|
||||
}
|
||||
@@ -41,7 +49,7 @@ async function resolveLogFile(file: string, options?: { rolling?: boolean }): Pr
|
||||
}
|
||||
|
||||
const dir = path.dirname(file);
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => null);
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true }).catch(missingPathToNull);
|
||||
if (!entries) {
|
||||
return file;
|
||||
}
|
||||
@@ -51,7 +59,7 @@ async function resolveLogFile(file: string, options?: { rolling?: boolean }): Pr
|
||||
.filter((entry) => entry.isFile() && isSameRollingLogFileFamily(file, entry.name))
|
||||
.map(async (entry) => {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
const fileStat = await fs.stat(fullPath).catch(() => null);
|
||||
const fileStat = await fs.stat(fullPath).catch(missingPathToNull);
|
||||
return fileStat ? { path: fullPath, mtimeMs: fileStat.mtimeMs } : null;
|
||||
}),
|
||||
);
|
||||
@@ -68,7 +76,7 @@ async function readLogSlice(params: {
|
||||
maxBytes: number;
|
||||
filter?: (line: string) => boolean;
|
||||
}): Promise<Omit<LogTailPayload, "file">> {
|
||||
const stat = await fs.stat(params.file).catch(() => null);
|
||||
const stat = await fs.stat(params.file).catch(missingPathToNull);
|
||||
if (!stat) {
|
||||
return {
|
||||
cursor: 0,
|
||||
|
||||
Reference in New Issue
Block a user