| import util from "node:util"; |
| 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"; |
| import { loggingState } from "./state.js"; |
| import { formatLocalIsoWithOffset } from "./timestamps.js"; |
|
|
| export type ConsoleStyle = "pretty" | "compact" | "json"; |
| type ConsoleSettings = { |
| level: LogLevel; |
| style: ConsoleStyle; |
| }; |
| 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; |
| } |
| }; |
| let loadConfigFallback: ConsoleConfigLoader = loadConfigFallbackDefault; |
|
|
| export function setConsoleConfigLoaderForTests(loader?: ConsoleConfigLoader): void { |
| loadConfigFallback = loader ?? loadConfigFallbackDefault; |
| } |
|
|
| function normalizeConsoleLevel(level?: string): LogLevel { |
| if (isVerbose()) { |
| return "debug"; |
| } |
| if (!level && process.env.VITEST === "true" && process.env.OPENCLAW_TEST_CONSOLE !== "1") { |
| return "silent"; |
| } |
| return normalizeLogLevel(level, "info"); |
| } |
|
|
| function normalizeConsoleStyle(style?: string): ConsoleStyle { |
| if (style === "compact" || style === "json" || style === "pretty") { |
| return style; |
| } |
| if (!process.stdout.isTTY) { |
| return "compact"; |
| } |
| return "pretty"; |
| } |
|
|
| function resolveConsoleSettings(): ConsoleSettings { |
| const envLevel = resolveEnvLogLevelOverride(); |
| |
| |
| if ( |
| process.env.VITEST === "true" && |
| process.env.OPENCLAW_TEST_CONSOLE !== "1" && |
| !isVerbose() && |
| !envLevel && |
| !loggingState.overrideSettings |
| ) { |
| return { level: "silent", style: normalizeConsoleStyle(undefined) }; |
| } |
|
|
| let cfg: OpenClawConfig["logging"] | undefined = |
| (loggingState.overrideSettings as LoggerSettings | null) ?? readLoggingConfig(); |
| if (!cfg) { |
| if (loggingState.resolvingConsoleSettings) { |
| cfg = undefined; |
| } else { |
| loggingState.resolvingConsoleSettings = true; |
| try { |
| cfg = loadConfigFallback(); |
| } finally { |
| loggingState.resolvingConsoleSettings = false; |
| } |
| } |
| } |
| const level = envLevel ?? normalizeConsoleLevel(cfg?.consoleLevel); |
| const style = normalizeConsoleStyle(cfg?.consoleStyle); |
| return { level, style }; |
| } |
|
|
| function consoleSettingsChanged(a: ConsoleSettings | null, b: ConsoleSettings) { |
| if (!a) { |
| return true; |
| } |
| return a.level !== b.level || a.style !== b.style; |
| } |
|
|
| export function getConsoleSettings(): ConsoleLoggerSettings { |
| const settings = resolveConsoleSettings(); |
| const cached = loggingState.cachedConsoleSettings as ConsoleSettings | null; |
| if (!cached || consoleSettingsChanged(cached, settings)) { |
| loggingState.cachedConsoleSettings = settings; |
| } |
| return loggingState.cachedConsoleSettings as ConsoleSettings; |
| } |
|
|
| export function getResolvedConsoleSettings(): ConsoleLoggerSettings { |
| return getConsoleSettings(); |
| } |
|
|
| |
| |
| export function routeLogsToStderr(): void { |
| loggingState.forceConsoleToStderr = true; |
| } |
|
|
| export function setConsoleSubsystemFilter(filters?: string[] | null): void { |
| if (!filters || filters.length === 0) { |
| loggingState.consoleSubsystemFilter = null; |
| return; |
| } |
| const normalized = filters.map((value) => value.trim()).filter((value) => value.length > 0); |
| loggingState.consoleSubsystemFilter = normalized.length > 0 ? normalized : null; |
| } |
|
|
| export function setConsoleTimestampPrefix(enabled: boolean): void { |
| loggingState.consoleTimestampPrefix = enabled; |
| } |
|
|
| export function shouldLogSubsystemToConsole(subsystem: string): boolean { |
| const filter = loggingState.consoleSubsystemFilter; |
| if (!filter || filter.length === 0) { |
| return true; |
| } |
| return filter.some((prefix) => subsystem === prefix || subsystem.startsWith(`${prefix}/`)); |
| } |
|
|
| const SUPPRESSED_CONSOLE_PREFIXES = [ |
| "Closing session:", |
| "Opening session:", |
| "Removing old closed session:", |
| "Session already closed", |
| "Session already open", |
| ] as const; |
|
|
| function shouldSuppressConsoleMessage(message: string): boolean { |
| if (isVerbose()) { |
| return false; |
| } |
| if (SUPPRESSED_CONSOLE_PREFIXES.some((prefix) => message.startsWith(prefix))) { |
| return true; |
| } |
| if ( |
| message.startsWith("[EventQueue] Slow listener detected") && |
| message.includes("DiscordMessageListener") |
| ) { |
| return true; |
| } |
| return false; |
| } |
|
|
| function isEpipeError(err: unknown): boolean { |
| const code = (err as { code?: string })?.code; |
| return code === "EPIPE" || code === "EIO"; |
| } |
|
|
| export function formatConsoleTimestamp(style: ConsoleStyle): string { |
| const now = new Date(); |
| if (style === "pretty") { |
| const h = String(now.getHours()).padStart(2, "0"); |
| const m = String(now.getMinutes()).padStart(2, "0"); |
| const s = String(now.getSeconds()).padStart(2, "0"); |
| return `${h}:${m}:${s}`; |
| } |
| return formatLocalIsoWithOffset(now); |
| } |
|
|
| function hasTimestampPrefix(value: string): boolean { |
| return /^(?:\d{2}:\d{2}:\d{2}|\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?)/.test( |
| value, |
| ); |
| } |
|
|
| function isJsonPayload(value: string): boolean { |
| const trimmed = value.trim(); |
| if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) { |
| return false; |
| } |
| try { |
| JSON.parse(trimmed); |
| return true; |
| } catch { |
| return false; |
| } |
| } |
|
|
| |
| |
| |
| |
| export function enableConsoleCapture(): void { |
| if (loggingState.consolePatched) { |
| return; |
| } |
| loggingState.consolePatched = true; |
|
|
| |
| |
| |
| |
| |
| |
| if (!loggingState.streamErrorHandlersInstalled) { |
| loggingState.streamErrorHandlersInstalled = true; |
| for (const stream of [process.stdout, process.stderr]) { |
| stream.on("error", (err) => { |
| if (isEpipeError(err)) { |
| return; |
| } |
| throw err; |
| }); |
| } |
| } |
|
|
| let logger: ReturnType<typeof getLogger> | null = null; |
| const getLoggerLazy = () => { |
| if (!logger) { |
| logger = getLogger(); |
| } |
| return logger; |
| }; |
|
|
| const original = { |
| log: console.log, |
| info: console.info, |
| warn: console.warn, |
| error: console.error, |
| debug: console.debug, |
| trace: console.trace, |
| }; |
| loggingState.rawConsole = { |
| log: original.log, |
| info: original.info, |
| warn: original.warn, |
| error: original.error, |
| }; |
|
|
| const forward = |
| (level: LogLevel, orig: (...args: unknown[]) => void) => |
| (...args: unknown[]) => { |
| const formatted = util.format(...args); |
| if (shouldSuppressConsoleMessage(formatted)) { |
| return; |
| } |
| const trimmed = stripAnsi(formatted).trimStart(); |
| const shouldPrefixTimestamp = |
| loggingState.consoleTimestampPrefix && |
| trimmed.length > 0 && |
| !hasTimestampPrefix(trimmed) && |
| !isJsonPayload(trimmed); |
| const timestamp = shouldPrefixTimestamp |
| ? formatConsoleTimestamp(getConsoleSettings().style) |
| : ""; |
| try { |
| const resolvedLogger = getLoggerLazy(); |
| |
| if (level === "trace") { |
| resolvedLogger.trace(formatted); |
| } else if (level === "debug") { |
| resolvedLogger.debug(formatted); |
| } else if (level === "info") { |
| resolvedLogger.info(formatted); |
| } else if (level === "warn") { |
| resolvedLogger.warn(formatted); |
| } else if (level === "error" || level === "fatal") { |
| resolvedLogger.error(formatted); |
| } else { |
| resolvedLogger.info(formatted); |
| } |
| } catch { |
| |
| } |
| if (loggingState.forceConsoleToStderr) { |
| |
| try { |
| const line = timestamp ? `${timestamp} ${formatted}` : formatted; |
| process.stderr.write(`${line}\n`); |
| } catch (err) { |
| if (isEpipeError(err)) { |
| return; |
| } |
| throw err; |
| } |
| } else { |
| try { |
| if (!timestamp) { |
| orig.apply(console, args as []); |
| return; |
| } |
| if (args.length === 0) { |
| orig.call(console, timestamp); |
| return; |
| } |
| if (typeof args[0] === "string") { |
| orig.call(console, `${timestamp} ${args[0]}`, ...args.slice(1)); |
| return; |
| } |
| orig.call(console, timestamp, ...args); |
| } catch (err) { |
| if (isEpipeError(err)) { |
| return; |
| } |
| throw err; |
| } |
| } |
| }; |
|
|
| console.log = forward("info", original.log); |
| console.info = forward("info", original.info); |
| console.warn = forward("warn", original.warn); |
| console.error = forward("error", original.error); |
| console.debug = forward("debug", original.debug); |
| console.trace = forward("trace", original.trace); |
| } |
|
|