mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 10:51:23 +00:00
Feat/logger support log level validation0222 (#23436)
* 1、环境变量**:新增 `OPENCLAW_LOG_LEVEL`,可取值 `silent|fatal|error|warn|info|debug|trace`。设置后同时覆盖**文件日志**与**控制台**的级别,优先级高于配置文件。 2、启动参数**:在 `openclaw gateway run` 上新增 `--log-level <level>`,对该次进程同时生效于文件与控制台;未传时仍使用环境变量或配置文件。 * fix(logging): make log-level override global and precedence-safe --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import type { OpenClawConfig } from "../config/types.js";
|
||||
import { isVerbose } from "../globals.js";
|
||||
import { stripAnsi } from "../terminal/ansi.js";
|
||||
import { readLoggingConfig } from "./config.js";
|
||||
import { resolveEnvLogLevelOverride } from "./env-log-level.js";
|
||||
import { type LogLevel, normalizeLogLevel } from "./levels.js";
|
||||
import { getLogger, type LoggerSettings } from "./logger.js";
|
||||
import { resolveNodeRequireFromMeta } from "./node-require.js";
|
||||
@@ -71,7 +72,8 @@ function resolveConsoleSettings(): ConsoleSettings {
|
||||
}
|
||||
}
|
||||
}
|
||||
const level = normalizeConsoleLevel(cfg?.consoleLevel);
|
||||
const envLevel = resolveEnvLogLevelOverride();
|
||||
const level = envLevel ?? normalizeConsoleLevel(cfg?.consoleLevel);
|
||||
const style = normalizeConsoleStyle(cfg?.consoleStyle);
|
||||
return { level, style };
|
||||
}
|
||||
|
||||
23
src/logging/env-log-level.ts
Normal file
23
src/logging/env-log-level.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ALLOWED_LOG_LEVELS, type LogLevel, tryParseLogLevel } from "./levels.js";
|
||||
import { loggingState } from "./state.js";
|
||||
|
||||
export function resolveEnvLogLevelOverride(): LogLevel | undefined {
|
||||
const raw = process.env.OPENCLAW_LOG_LEVEL;
|
||||
const trimmed = typeof raw === "string" ? raw.trim() : "";
|
||||
if (!trimmed) {
|
||||
loggingState.invalidEnvLogLevelValue = null;
|
||||
return undefined;
|
||||
}
|
||||
const parsed = tryParseLogLevel(trimmed);
|
||||
if (parsed) {
|
||||
loggingState.invalidEnvLogLevelValue = null;
|
||||
return parsed;
|
||||
}
|
||||
if (loggingState.invalidEnvLogLevelValue !== trimmed) {
|
||||
loggingState.invalidEnvLogLevelValue = trimmed;
|
||||
process.stderr.write(
|
||||
`[openclaw] Ignoring invalid OPENCLAW_LOG_LEVEL="${trimmed}" (allowed: ${ALLOWED_LOG_LEVELS.join("|")}).\n`,
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -10,9 +10,16 @@ export const ALLOWED_LOG_LEVELS = [
|
||||
|
||||
export type LogLevel = (typeof ALLOWED_LOG_LEVELS)[number];
|
||||
|
||||
export function tryParseLogLevel(level?: string): LogLevel | undefined {
|
||||
if (typeof level !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const candidate = level.trim();
|
||||
return ALLOWED_LOG_LEVELS.includes(candidate as LogLevel) ? (candidate as LogLevel) : undefined;
|
||||
}
|
||||
|
||||
export function normalizeLogLevel(level?: string, fallback: LogLevel = "info") {
|
||||
const candidate = (level ?? fallback).trim();
|
||||
return ALLOWED_LOG_LEVELS.includes(candidate as LogLevel) ? (candidate as LogLevel) : fallback;
|
||||
return tryParseLogLevel(level) ?? fallback;
|
||||
}
|
||||
|
||||
export function levelToMinLevel(level: LogLevel): number {
|
||||
|
||||
78
src/logging/logger-env.test.ts
Normal file
78
src/logging/logger-env.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
getResolvedConsoleSettings,
|
||||
getResolvedLoggerSettings,
|
||||
resetLogger,
|
||||
setLoggerOverride,
|
||||
} from "../logging.js";
|
||||
import { loggingState } from "./state.js";
|
||||
|
||||
const testLogPath = path.join(os.tmpdir(), "openclaw-test-env-log-level.log");
|
||||
|
||||
describe("OPENCLAW_LOG_LEVEL", () => {
|
||||
let originalEnv: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
originalEnv = process.env.OPENCLAW_LOG_LEVEL;
|
||||
delete process.env.OPENCLAW_LOG_LEVEL;
|
||||
loggingState.invalidEnvLogLevelValue = null;
|
||||
resetLogger();
|
||||
setLoggerOverride(null);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalEnv === undefined) {
|
||||
delete process.env.OPENCLAW_LOG_LEVEL;
|
||||
} else {
|
||||
process.env.OPENCLAW_LOG_LEVEL = originalEnv;
|
||||
}
|
||||
loggingState.invalidEnvLogLevelValue = null;
|
||||
resetLogger();
|
||||
setLoggerOverride(null);
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("applies a valid env override to both file and console levels", () => {
|
||||
setLoggerOverride({
|
||||
level: "error",
|
||||
consoleLevel: "warn",
|
||||
consoleStyle: "json",
|
||||
file: testLogPath,
|
||||
});
|
||||
process.env.OPENCLAW_LOG_LEVEL = "debug";
|
||||
|
||||
expect(getResolvedLoggerSettings()).toEqual({
|
||||
level: "debug",
|
||||
file: testLogPath,
|
||||
});
|
||||
expect(getResolvedConsoleSettings()).toEqual({
|
||||
level: "debug",
|
||||
style: "json",
|
||||
});
|
||||
});
|
||||
|
||||
it("warns once and ignores invalid env values", () => {
|
||||
setLoggerOverride({
|
||||
level: "error",
|
||||
consoleLevel: "warn",
|
||||
consoleStyle: "compact",
|
||||
file: testLogPath,
|
||||
});
|
||||
process.env.OPENCLAW_LOG_LEVEL = "nope";
|
||||
const stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation(
|
||||
() => true as unknown as ReturnType<typeof process.stderr.write>, // preserve stream contract in test spy
|
||||
);
|
||||
|
||||
expect(getResolvedLoggerSettings().level).toBe("error");
|
||||
expect(getResolvedConsoleSettings().level).toBe("warn");
|
||||
expect(getResolvedLoggerSettings().level).toBe("error");
|
||||
|
||||
const warnings = stderrSpy.mock.calls
|
||||
.map(([firstArg]) => String(firstArg))
|
||||
.filter((line) => line.includes("OPENCLAW_LOG_LEVEL"));
|
||||
expect(warnings).toHaveLength(1);
|
||||
expect(warnings[0]).toContain('Ignoring invalid OPENCLAW_LOG_LEVEL="nope"');
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import type { OpenClawConfig } from "../config/types.js";
|
||||
import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
|
||||
import { readLoggingConfig } from "./config.js";
|
||||
import type { ConsoleStyle } from "./console.js";
|
||||
import { resolveEnvLogLevelOverride } from "./env-log-level.js";
|
||||
import { type LogLevel, levelToMinLevel, normalizeLogLevel } from "./levels.js";
|
||||
import { resolveNodeRequireFromMeta } from "./node-require.js";
|
||||
import { loggingState } from "./state.js";
|
||||
@@ -67,7 +68,9 @@ function resolveSettings(): ResolvedSettings {
|
||||
}
|
||||
const defaultLevel =
|
||||
process.env.VITEST === "true" && process.env.OPENCLAW_TEST_FILE_LOG !== "1" ? "silent" : "info";
|
||||
const level = normalizeLogLevel(cfg?.level, defaultLevel);
|
||||
const fromConfig = normalizeLogLevel(cfg?.level, defaultLevel);
|
||||
const envLevel = resolveEnvLogLevelOverride();
|
||||
const level = envLevel ?? fromConfig;
|
||||
const file = cfg?.file ?? defaultRollingPathForToday();
|
||||
return { level, file };
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ export const loggingState = {
|
||||
cachedSettings: null as unknown,
|
||||
cachedConsoleSettings: null as unknown,
|
||||
overrideSettings: null as unknown,
|
||||
invalidEnvLogLevelValue: null as string | null,
|
||||
consolePatched: false,
|
||||
forceConsoleToStderr: false,
|
||||
consoleTimestampPrefix: false,
|
||||
|
||||
Reference in New Issue
Block a user