fix(logging): read config path in bundled runtime

This commit is contained in:
Vincent Koc
2026-04-25 22:18:29 -07:00
parent d1502c2ba1
commit 5d3168c343
9 changed files with 187 additions and 91 deletions

View File

@@ -81,6 +81,10 @@ Docs: https://docs.openclaw.ai
split-brain installs from stopping or rewriting newer gateway services. Fixes
#57079.
- Gateway: reserve `/healthz` and `/readyz` ahead of plugin, canvas, and Control UI HTTP stages so liveness/readiness probes still answer when a later route handler stalls. Fixes #69674. Thanks @Xike-Creek.
- Logging: load `logging.file` and redaction settings directly from the active
OpenClaw config path in bundled runtimes, so packaged gateways stop falling
back to `/tmp/openclaw`. Fixes #59370, #67168, and #61295. Thanks @KeaneYan,
@Pan9hu, and @zsjlovelike.
- Agents/groups: treat clean empty assistant stops as silent `NO_REPLY` only for always-on groups where silent replies are allowed, while keeping direct and mention-gated sessions on the incomplete-turn retry path. Thanks @MagnaAI.
- macOS/Node: keep native remote app nodes from advertising `browser.proxy`,
start browser-capable CLI node services through the restored

View File

@@ -1,31 +1,78 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { readLoggingConfig } from "./config.js";
const loadConfigMock = vi.hoisted(() => vi.fn());
vi.mock("../config/config.js", async () => {
const actual = await vi.importActual<typeof import("../config/config.js")>("../config/config.js");
return {
...actual,
loadConfig: () => loadConfigMock(),
};
});
const originalArgv = process.argv;
const originalConfigPath = process.env.OPENCLAW_CONFIG_PATH;
let tempDirs: string[] = [];
function writeConfig(source: string): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-logging-config-"));
tempDirs.push(dir);
const configPath = path.join(dir, "openclaw.json");
fs.writeFileSync(configPath, source);
process.env.OPENCLAW_CONFIG_PATH = configPath;
return configPath;
}
describe("readLoggingConfig", () => {
afterEach(() => {
process.argv = originalArgv;
loadConfigMock.mockReset();
if (originalConfigPath === undefined) {
delete process.env.OPENCLAW_CONFIG_PATH;
} else {
process.env.OPENCLAW_CONFIG_PATH = originalConfigPath;
}
for (const dir of tempDirs) {
fs.rmSync(dir, { force: true, recursive: true });
}
tempDirs = [];
});
it("skips mutating config loads for config schema", async () => {
process.argv = ["node", "openclaw", "config", "schema"];
loadConfigMock.mockImplementation(() => {
throw new Error("loadConfig should not be called");
});
const configPath = writeConfig(`{ logging: { file: "/tmp/should-not-read.log" } }`);
fs.rmSync(configPath);
expect(readLoggingConfig()).toBeUndefined();
expect(loadConfigMock).not.toHaveBeenCalled();
});
it("reads logging config directly from the active config path", () => {
writeConfig(`{
logging: {
level: "debug",
file: "/tmp/openclaw-custom.log",
maxFileBytes: 1234,
},
}`);
expect(readLoggingConfig()).toMatchObject({
level: "debug",
file: "/tmp/openclaw-custom.log",
maxFileBytes: 1234,
});
});
it("supports JSON5 comments and trailing commas", () => {
writeConfig(`{
// users commonly keep comments in openclaw.json
logging: {
consoleLevel: "warn",
},
}`);
expect(readLoggingConfig()).toMatchObject({
consoleLevel: "warn",
});
});
it("returns undefined for missing or malformed config files", () => {
process.env.OPENCLAW_CONFIG_PATH = path.join(os.tmpdir(), "openclaw-missing-config.json");
expect(readLoggingConfig()).toBeUndefined();
writeConfig(`{ logging: `);
expect(readLoggingConfig()).toBeUndefined();
});
});

View File

@@ -1,32 +1,47 @@
import fs from "node:fs";
import JSON5 from "json5";
import { getCommandPathWithRootOptions } from "../cli/argv.js";
import { resolveConfigPath } from "../config/paths.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { resolveNodeRequireFromMeta } from "./node-require.js";
type LoggingConfig = OpenClawConfig["logging"];
const requireConfig = resolveNodeRequireFromMeta(import.meta.url);
let cachedLoggingConfig:
| {
path: string;
logging: LoggingConfig | undefined;
}
| undefined;
export function shouldSkipMutatingLoggingConfigRead(argv: string[] = process.argv): boolean {
const [primary, secondary] = getCommandPathWithRootOptions(argv, 2);
return primary === "config" && (secondary === "schema" || secondary === "validate");
}
function isObjectRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
export function readLoggingConfig(): LoggingConfig | undefined {
if (shouldSkipMutatingLoggingConfigRead()) {
return undefined;
}
try {
const loaded = requireConfig?.("../config/config.js") as
| {
loadConfig?: () => OpenClawConfig;
}
| undefined;
const parsed = loaded?.loadConfig?.();
const logging = parsed?.logging;
if (!logging || typeof logging !== "object" || Array.isArray(logging)) {
const configPath = resolveConfigPath();
if (cachedLoggingConfig?.path === configPath) {
return cachedLoggingConfig.logging;
}
if (!fs.existsSync(configPath)) {
return undefined;
}
return logging as LoggingConfig;
const parsed = JSON5.parse(fs.readFileSync(configPath, "utf8"));
const logging = isObjectRecord(parsed) ? parsed.logging : undefined;
const resolved = isObjectRecord(logging) ? (logging as LoggingConfig) : undefined;
cachedLoggingConfig = {
path: configPath,
logging: resolved,
};
return resolved;
} catch {
return undefined;
}

View File

@@ -6,7 +6,6 @@ import { readLoggingConfig, shouldSkipMutatingLoggingConfigRead } from "./config
import { resolveEnvLogLevelOverride } from "./env-log-level.js";
import { type LogLevel, normalizeLogLevel } from "./levels.js";
import { getLogger } from "./logger.js";
import { resolveNodeRequireFromMeta } from "./node-require.js";
import { redactSensitiveText } from "./redact.js";
import { loggingState } from "./state.js";
import { formatLocalIsoWithOffset, formatTimestamp } from "./timestamps.js";
@@ -19,20 +18,8 @@ type ConsoleSettings = {
};
export type ConsoleLoggerSettings = ConsoleSettings;
const requireConfig = resolveNodeRequireFromMeta(import.meta.url);
type ConsoleConfigLoader = () => OpenClawConfig["logging"] | undefined;
const loadConfigFallbackDefault: ConsoleConfigLoader = () => {
try {
const loaded = requireConfig?.("../config/config.js") as
| {
loadConfig?: () => OpenClawConfig;
}
| undefined;
return loaded?.loadConfig?.().logging;
} catch {
return undefined;
}
};
const loadConfigFallbackDefault: ConsoleConfigLoader = () => undefined;
let loadConfigFallback: ConsoleConfigLoader = loadConfigFallbackDefault;
export function setConsoleConfigLoaderForTests(loader?: ConsoleConfigLoader): void {

View File

@@ -5,12 +5,24 @@ import { createSuiteLogPathTracker } from "./log-test-helpers.js";
const secret = "sk-testsecret1234567890abcd";
const logPathTracker = createSuiteLogPathTracker("openclaw-log-redaction-");
const originalConfigPath = process.env.OPENCLAW_CONFIG_PATH;
const originalTestFileLog = process.env.OPENCLAW_TEST_FILE_LOG;
beforeAll(async () => {
await logPathTracker.setup();
});
afterEach(() => {
if (originalConfigPath === undefined) {
delete process.env.OPENCLAW_CONFIG_PATH;
} else {
process.env.OPENCLAW_CONFIG_PATH = originalConfigPath;
}
if (originalTestFileLog === undefined) {
delete process.env.OPENCLAW_TEST_FILE_LOG;
} else {
process.env.OPENCLAW_TEST_FILE_LOG = originalTestFileLog;
}
resetLogger();
setLoggerOverride(null);
});
@@ -42,4 +54,25 @@ describe("file log redaction", () => {
expect(content).toContain("Authorization: Bearer");
expect(content).not.toContain(secret);
});
it("uses logging.file from the active config path", () => {
const logPath = logPathTracker.nextPath();
const configPath = logPathTracker.nextPath();
fs.writeFileSync(
configPath,
JSON.stringify({
logging: {
level: "info",
file: logPath,
},
}),
);
process.env.OPENCLAW_CONFIG_PATH = configPath;
process.env.OPENCLAW_TEST_FILE_LOG = "1";
getLogger().info({ message: "configured log path works" });
const content = fs.readFileSync(logPath, "utf8");
expect(content).toContain("configured log path works");
});
});

View File

@@ -1,23 +1,15 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const { fallbackRequireMock, readLoggingConfigMock, shouldSkipMutatingLoggingConfigReadMock } =
vi.hoisted(() => ({
readLoggingConfigMock: vi.fn(() => undefined),
shouldSkipMutatingLoggingConfigReadMock: vi.fn(() => false),
fallbackRequireMock: vi.fn(() => {
throw new Error("config fallback should not be used in this test");
}),
}));
const { readLoggingConfigMock, shouldSkipMutatingLoggingConfigReadMock } = vi.hoisted(() => ({
readLoggingConfigMock: vi.fn<() => unknown>(() => undefined),
shouldSkipMutatingLoggingConfigReadMock: vi.fn(() => false),
}));
vi.mock("./config.js", () => ({
readLoggingConfig: readLoggingConfigMock,
shouldSkipMutatingLoggingConfigRead: shouldSkipMutatingLoggingConfigReadMock,
}));
vi.mock("./node-require.js", () => ({
resolveNodeRequireFromMeta: () => fallbackRequireMock,
}));
let originalTestFileLog: string | undefined;
let originalOpenClawLogLevel: string | undefined;
let logging: typeof import("../logging.js");
@@ -31,10 +23,10 @@ beforeEach(() => {
originalOpenClawLogLevel = process.env.OPENCLAW_LOG_LEVEL;
delete process.env.OPENCLAW_TEST_FILE_LOG;
delete process.env.OPENCLAW_LOG_LEVEL;
readLoggingConfigMock.mockClear();
readLoggingConfigMock.mockReset();
readLoggingConfigMock.mockReturnValue(undefined);
shouldSkipMutatingLoggingConfigReadMock.mockReset();
shouldSkipMutatingLoggingConfigReadMock.mockReturnValue(false);
fallbackRequireMock.mockClear();
logging.resetLogger();
logging.setLoggerOverride(null);
});
@@ -60,22 +52,31 @@ describe("getResolvedLoggerSettings", () => {
const settings = logging.getResolvedLoggerSettings();
expect(settings.level).toBe("silent");
expect(readLoggingConfigMock).not.toHaveBeenCalled();
expect(fallbackRequireMock).not.toHaveBeenCalled();
});
it("reads logging config when test file logging is explicitly enabled", () => {
process.env.OPENCLAW_TEST_FILE_LOG = "1";
readLoggingConfigMock.mockReturnValue({
level: "debug",
file: "/tmp/openclaw-configured.log",
maxFileBytes: 2048,
});
const settings = logging.getResolvedLoggerSettings();
expect(settings.level).toBe("info");
expect(settings).toMatchObject({
level: "debug",
file: "/tmp/openclaw-configured.log",
maxFileBytes: 2048,
});
});
it("skips fallback config loads for config schema", () => {
it("uses defaults when config schema skips logging config reads", () => {
process.env.OPENCLAW_TEST_FILE_LOG = "1";
shouldSkipMutatingLoggingConfigReadMock.mockReturnValue(true);
const settings = logging.getResolvedLoggerSettings();
expect(settings.level).toBe("info");
expect(fallbackRequireMock).not.toHaveBeenCalled();
});
});

View File

@@ -17,7 +17,6 @@ import {
import { readLoggingConfig, shouldSkipMutatingLoggingConfigRead } from "./config.js";
import { resolveEnvLogLevelOverride } from "./env-log-level.js";
import { type LogLevel, levelToMinLevel, normalizeLogLevel } from "./levels.js";
import { resolveNodeRequireFromMeta } from "./node-require.js";
import { redactSensitiveText } from "./redact.js";
import { loggingState } from "./state.js";
import { formatTimestamp } from "./timestamps.js";
@@ -58,8 +57,6 @@ 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);
type LogObj = { date?: Date } & Record<string, unknown>;
type ResolvedSettings = {
@@ -355,20 +352,8 @@ function resolveSettings(): ResolvedSettings {
};
}
let cfg: OpenClawConfig["logging"] | undefined =
const cfg: OpenClawConfig["logging"] | undefined =
(loggingState.overrideSettings as LoggerSettings | null) ?? readLoggingConfig();
if (!cfg && !shouldSkipMutatingLoggingConfigRead()) {
try {
const loaded = requireConfig?.("../config/config.js") as
| {
loadConfig?: () => OpenClawConfig;
}
| undefined;
cfg = loaded?.loadConfig?.().logging;
} catch {
cfg = undefined;
}
}
const defaultLevel =
process.env.VITEST === "true" && process.env.OPENCLAW_TEST_FILE_LOG !== "1" ? "silent" : "info";
const fromConfig = normalizeLogLevel(cfg?.level, defaultLevel);

View File

@@ -1,4 +1,7 @@
import { describe, expect, it } from "vitest";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
getDefaultRedactPatterns,
redactSensitiveLines,
@@ -7,6 +10,28 @@ import {
} from "./redact.js";
const defaults = getDefaultRedactPatterns();
const originalConfigPath = process.env.OPENCLAW_CONFIG_PATH;
let tempDirs: string[] = [];
function writeConfig(source: string): void {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-redact-config-"));
tempDirs.push(dir);
const configPath = path.join(dir, "openclaw.json");
fs.writeFileSync(configPath, source);
process.env.OPENCLAW_CONFIG_PATH = configPath;
}
afterEach(() => {
if (originalConfigPath === undefined) {
delete process.env.OPENCLAW_CONFIG_PATH;
} else {
process.env.OPENCLAW_CONFIG_PATH = originalConfigPath;
}
for (const dir of tempDirs) {
fs.rmSync(dir, { force: true, recursive: true });
}
tempDirs = [];
});
describe("redactSensitiveText", () => {
it("masks env assignments while keeping the key", () => {
@@ -134,6 +159,18 @@ describe("redactSensitiveText", () => {
expect(output).toBe(input);
});
it("honors logging redaction settings from the active config path", () => {
writeConfig(`{
logging: {
redactSensitive: "off",
},
}`);
expect(redactSensitiveText("OPENAI_API_KEY=sk-1234567890abcdef")).toBe(
"OPENAI_API_KEY=sk-1234567890abcdef",
);
});
it("does not resolve patterns when mode is off", () => {
const options = {
mode: "off" as const,

View File

@@ -1,10 +1,7 @@
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { compileConfigRegex } from "../security/config-regex.js";
import { resolveNodeRequireFromMeta } from "./node-require.js";
import { readLoggingConfig } from "./config.js";
import { replacePatternBounded } from "./redact-bounded.js";
const requireConfig = resolveNodeRequireFromMeta(import.meta.url);
export type RedactSensitiveMode = "off" | "tools";
type RedactPattern = string | RegExp;
@@ -117,17 +114,7 @@ function redactText(text: string, patterns: RegExp[]): string {
}
function resolveConfigRedaction(): RedactOptions {
let cfg: OpenClawConfig["logging"] | undefined;
try {
const loaded = requireConfig?.("../config/config.js") as
| {
loadConfig?: () => OpenClawConfig;
}
| undefined;
cfg = loaded?.loadConfig?.().logging;
} catch {
cfg = undefined;
}
const cfg = readLoggingConfig();
return {
mode: normalizeMode(cfg?.redactSensitive),
patterns: cfg?.redactPatterns,