mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:20:43 +00:00
fix(logging): read config path in bundled runtime
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user