fix(logging): cap file logs with configurable maxFileBytes

Co-authored-by: Xinhua Gu <562450+xinhuagu@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-02-22 17:58:51 +01:00
parent 795db98f6a
commit 8cc744ef1f
7 changed files with 154 additions and 3 deletions

View File

@@ -16,12 +16,14 @@ export const DEFAULT_LOG_FILE = path.join(DEFAULT_LOG_DIR, "openclaw.log"); // l
const LOG_PREFIX = "openclaw";
const LOG_SUFFIX = ".log";
const MAX_LOG_AGE_MS = 24 * 60 * 60 * 1000; // 24h
const DEFAULT_MAX_LOG_FILE_BYTES = 500 * 1024 * 1024; // 500 MB
const requireConfig = resolveNodeRequireFromMeta(import.meta.url);
export type LoggerSettings = {
level?: LogLevel;
file?: string;
maxFileBytes?: number;
consoleLevel?: LogLevel;
consoleStyle?: ConsoleStyle;
};
@@ -31,6 +33,7 @@ type LogObj = { date?: Date } & Record<string, unknown>;
type ResolvedSettings = {
level: LogLevel;
file: string;
maxFileBytes: number;
};
export type LoggerResolvedSettings = ResolvedSettings;
export type LogTransportRecord = Record<string, unknown>;
@@ -72,14 +75,15 @@ function resolveSettings(): ResolvedSettings {
const envLevel = resolveEnvLogLevelOverride();
const level = envLevel ?? fromConfig;
const file = cfg?.file ?? defaultRollingPathForToday();
return { level, file };
const maxFileBytes = resolveMaxLogFileBytes(cfg?.maxFileBytes);
return { level, file, maxFileBytes };
}
function settingsChanged(a: ResolvedSettings | null, b: ResolvedSettings) {
if (!a) {
return true;
}
return a.level !== b.level || a.file !== b.file;
return a.level !== b.level || a.file !== b.file || a.maxFileBytes !== b.maxFileBytes;
}
export function isFileLogLevelEnabled(level: LogLevel): boolean {
@@ -99,6 +103,8 @@ function buildLogger(settings: ResolvedSettings): TsLogger<LogObj> {
if (isRollingPath(settings.file)) {
pruneOldRollingLogs(path.dirname(settings.file));
}
let currentFileBytes = getCurrentLogFileBytes(settings.file);
let warnedAboutSizeCap = false;
const logger = new TsLogger<LogObj>({
name: "openclaw",
minLevel: levelToMinLevel(settings.level),
@@ -109,7 +115,28 @@ function buildLogger(settings: ResolvedSettings): TsLogger<LogObj> {
try {
const time = logObj.date?.toISOString?.() ?? new Date().toISOString();
const line = JSON.stringify({ ...logObj, time });
fs.appendFileSync(settings.file, `${line}\n`, { encoding: "utf8" });
const payload = `${line}\n`;
const payloadBytes = Buffer.byteLength(payload, "utf8");
const nextBytes = currentFileBytes + payloadBytes;
if (nextBytes > settings.maxFileBytes) {
if (!warnedAboutSizeCap) {
warnedAboutSizeCap = true;
const warningLine = JSON.stringify({
time: new Date().toISOString(),
level: "warn",
subsystem: "logging",
message: `log file size cap reached; suppressing writes file=${settings.file} maxFileBytes=${settings.maxFileBytes}`,
});
appendLogLine(settings.file, `${warningLine}\n`);
process.stderr.write(
`[openclaw] log file size cap reached; suppressing writes file=${settings.file} maxFileBytes=${settings.maxFileBytes}\n`,
);
}
return;
}
if (appendLogLine(settings.file, payload)) {
currentFileBytes = nextBytes;
}
} catch {
// never block on logging failures
}
@@ -121,6 +148,30 @@ function buildLogger(settings: ResolvedSettings): TsLogger<LogObj> {
return logger;
}
function resolveMaxLogFileBytes(raw: unknown): number {
if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) {
return Math.floor(raw);
}
return DEFAULT_MAX_LOG_FILE_BYTES;
}
function getCurrentLogFileBytes(file: string): number {
try {
return fs.statSync(file).size;
} catch {
return 0;
}
}
function appendLogLine(file: string, line: string): boolean {
try {
fs.appendFileSync(file, line, { encoding: "utf8" });
return true;
} catch {
return false;
}
}
export function getLogger(): TsLogger<LogObj> {
const settings = resolveSettings();
const cachedLogger = loggingState.cachedLogger as TsLogger<LogObj> | null;