// Logging config helpers read and normalize logger configuration. import fs from "node:fs"; import { isRecord as isObjectRecord } from "@openclaw/normalization-core/record-coerce"; import { resolveConfigEnvVars } from "../config/env-substitution.js"; import { resolveConfigIncludes, resolveConfigIncludesForTopLevelKey } from "../config/includes.js"; import { resolveConfigPath, resolveIncludeRoots } from "../config/paths.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { tryProcessCwd } from "../infra/safe-cwd.js"; import { parseJsonWithJson5Fallback } from "../utils/parse-json-compat.js"; import { APPLIED_LOGGING_CONFIG_UNOWNED, loggingState } from "./state.js"; // Lightweight logging-config reader used before the full config runtime is safe to load. type LoggingConfig = NonNullable; let cachedLoggingConfig: | { selector: string; logging: LoggingConfig | undefined; } | undefined; export function invalidateLoggingConfigCache(): void { cachedLoggingConfig = undefined; } function resolveLoggingConfigSelector(): string { const env = process.env; return [ env.OPENCLAW_CONFIG_PATH, env.OPENCLAW_STATE_DIR, env.OPENCLAW_HOME, env.OPENCLAW_PROFILE, env.HOME, env.USERPROFILE, env.HOMEDRIVE, env.HOMEPATH, env.PREFIX, env.ANDROID_DATA, env.OPENCLAW_TEST_FAST, tryProcessCwd() ?? "", ] .map((value) => value ?? "") .join("\0"); } function resolvePartialDiagnosticLoggingConfig(logging: unknown): LoggingConfig | undefined { if (!isObjectRecord(logging)) { return undefined; } const partial: Record = {}; if (typeof logging.consoleStyle === "string") { try { const resolved = resolveConfigEnvVars({ consoleStyle: logging.consoleStyle }); if ( isObjectRecord(resolved) && (resolved.consoleStyle === "pretty" || resolved.consoleStyle === "compact" || resolved.consoleStyle === "json") ) { partial.consoleStyle = resolved.consoleStyle; } } catch { // Keep resolving independent diagnostic fields. } } if (Array.isArray(logging.redactPatterns)) { try { const resolved = resolveConfigEnvVars({ redactPatterns: logging.redactPatterns }); if ( isObjectRecord(resolved) && Array.isArray(resolved.redactPatterns) && resolved.redactPatterns.every((entry) => typeof entry === "string") ) { partial.redactPatterns = resolved.redactPatterns; } } catch { // A missing variable in one pattern must not hide a separately resolvable style. } } return Object.keys(partial).length > 0 ? (partial as LoggingConfig) : undefined; } /** Reads the logging block from config, caching by resolved config path. */ export function readLoggingConfig(): LoggingConfig | undefined { try { if (loggingState.appliedConfig !== APPLIED_LOGGING_CONFIG_UNOWNED) { return loggingState.appliedConfig; } const selector = resolveLoggingConfigSelector(); if (cachedLoggingConfig?.selector === selector) { return cachedLoggingConfig.logging; } const configPath = resolveConfigPath(); if (!fs.existsSync(configPath)) { cachedLoggingConfig = { selector, logging: undefined }; return undefined; } const parsed = parseJsonWithJson5Fallback(fs.readFileSync(configPath, "utf8")); const allowedRoots = resolveIncludeRoots(); let includedConfig: unknown; try { includedConfig = resolveConfigIncludesForTopLevelKey( parsed, configPath, "logging", undefined, { allowedRoots }, ); } catch { // Validation commands still need a directly-authored logging style when an unrelated // root include is malformed. Resolve only that subtree through the same canonical rules. const directLogging = isObjectRecord(parsed) ? parsed.logging : undefined; if (directLogging === undefined) { return undefined; } try { includedConfig = resolveConfigIncludes({ logging: directLogging }, configPath, undefined, { allowedRoots, }); } catch { const logging = resolvePartialDiagnosticLoggingConfig(directLogging); return logging; } } let resolvedConfig: unknown; try { resolvedConfig = resolveConfigEnvVars(includedConfig); } catch { const includedLogging = isObjectRecord(includedConfig) ? includedConfig.logging : undefined; const logging = resolvePartialDiagnosticLoggingConfig(includedLogging); return logging; } const logging = isObjectRecord(resolvedConfig) ? resolvedConfig.logging : undefined; const resolvedLogging = isObjectRecord(logging) ? (logging as LoggingConfig) : undefined; cachedLoggingConfig = { selector, logging: resolvedLogging, }; return resolvedLogging; } catch { return undefined; } }