diff --git a/docs/configuration.md b/docs/configuration.md index a4e5b9e10..109682962 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -49,6 +49,7 @@ If set, CLAWDIS derives defaults (only when you haven’t set them explicitly): - Console output can be tuned separately via: - `logging.consoleLevel` (defaults to `info`, bumps to `debug` when `--verbose`) - `logging.consoleStyle` (`pretty` | `compact` | `json`) + - `logging.consoleColor` (`auto` | `always` | `never`) ```json5 { @@ -56,7 +57,8 @@ If set, CLAWDIS derives defaults (only when you haven’t set them explicitly): level: "info", file: "/tmp/clawdis/clawdis.log", consoleLevel: "info", - consoleStyle: "pretty" + consoleStyle: "pretty", + consoleColor: "auto" } } ``` diff --git a/docs/logging.md b/docs/logging.md index be0704df2..7997b3faf 100644 --- a/docs/logging.md +++ b/docs/logging.md @@ -74,7 +74,8 @@ Subsystem loggers are created via `createSubsystemLogger("gateway")`. Behavior: - **Subsystem prefixes** on every line (e.g. `[gateway]`, `[canvas]`, `[tailscale]`) -- **Color only when TTY** (`process.stdout.isTTY` + `NO_COLOR` respected) +- **Subsystem colors** (stable per subsystem) plus level coloring +- **Color modes** (`logging.consoleColor`: `auto`/`always`/`never`; `auto` honors `NO_COLOR` and TTY) - **Sub-loggers by subsystem** (auto prefix + structured field `{ subsystem }`) - **`logRaw()`** for QR/UX output (no prefix, no formatting) - **Console styles** (e.g. `pretty | compact | json`) diff --git a/src/config/config.ts b/src/config/config.ts index 9f0bb55ea..5371c8d9f 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -29,6 +29,7 @@ export type LoggingConfig = { | "debug" | "trace"; consoleStyle?: "pretty" | "compact" | "json"; + consoleColor?: "auto" | "always" | "never"; }; export type WebReconnectConfig = { @@ -269,6 +270,9 @@ const ClawdisSchema = z.object({ consoleStyle: z .union([z.literal("pretty"), z.literal("compact"), z.literal("json")]) .optional(), + consoleColor: z + .union([z.literal("auto"), z.literal("always"), z.literal("never")]) + .optional(), }) .optional(), browser: z diff --git a/src/logging.ts b/src/logging.ts index 0d3040ede..5a671e564 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -34,6 +34,7 @@ export type LoggerSettings = { file?: string; consoleLevel?: Level; consoleStyle?: ConsoleStyle; + consoleColor?: ConsoleColor; }; type LogObj = { date?: Date } & Record; @@ -45,9 +46,11 @@ type ResolvedSettings = { export type LoggerResolvedSettings = ResolvedSettings; export type ConsoleStyle = "pretty" | "compact" | "json"; +export type ConsoleColor = "auto" | "always" | "never"; type ConsoleSettings = { level: Level; style: ConsoleStyle; + color: ConsoleColor; }; export type ConsoleLoggerSettings = ConsoleSettings; @@ -87,7 +90,8 @@ function resolveConsoleSettings(): ConsoleSettings { overrideSettings ?? loadConfig().logging; const level = normalizeConsoleLevel(cfg?.consoleLevel); const style = normalizeConsoleStyle(cfg?.consoleStyle); - return { level, style }; + const color = normalizeConsoleColor(cfg?.consoleColor); + return { level, style, color }; } function settingsChanged(a: ResolvedSettings | null, b: ResolvedSettings) { @@ -100,7 +104,7 @@ function consoleSettingsChanged( b: ConsoleSettings, ) { if (!a) return true; - return a.level !== b.level || a.style !== b.style; + return a.level !== b.level || a.style !== b.style || a.color !== b.color; } function levelToMinLevel(level: Level): number { @@ -133,6 +137,13 @@ function normalizeConsoleStyle(style?: string): ConsoleStyle { return "pretty"; } +function normalizeConsoleColor(color?: string): ConsoleColor { + if (color === "auto" || color === "always" || color === "never") { + return color; + } + return "auto"; +} + function buildLogger(settings: ResolvedSettings): TsLogger { fs.mkdirSync(path.dirname(settings.file), { recursive: true }); // Clean up stale rolling logs when using a dated log filename. @@ -339,7 +350,9 @@ function shouldLogToConsole(level: Level, settings: ConsoleSettings): boolean { return current <= min; } -function getColorForConsole(): Chalk { +function getColorForConsole(mode: ConsoleColor): Chalk { + if (mode === "never") return new Chalk({ level: 0 }); + if (mode === "always") return new Chalk({ level: 1 }); const supports = process.stdout.isTTY && !process.env.NO_COLOR && @@ -347,11 +360,31 @@ function getColorForConsole(): Chalk { return supports ? chalk : new Chalk({ level: 0 }); } +const SUBSYSTEM_COLORS = [ + "cyan", + "green", + "yellow", + "blue", + "magenta", + "red", +] as const; + +function pickSubsystemColor(color: Chalk, subsystem: string): Chalk { + let hash = 0; + for (let i = 0; i < subsystem.length; i += 1) { + hash = (hash * 31 + subsystem.charCodeAt(i)) | 0; + } + const idx = Math.abs(hash) % SUBSYSTEM_COLORS.length; + const name = SUBSYSTEM_COLORS[idx]; + return color[name]; +} + function formatConsoleLine(opts: { level: Level; subsystem: string; message: string; style: ConsoleStyle; + colorMode: ConsoleColor; meta?: Record; }): string { if (opts.style === "json") { @@ -363,8 +396,9 @@ function formatConsoleLine(opts: { ...opts.meta, }); } - const color = getColorForConsole(); + const color = getColorForConsole(opts.colorMode); const prefix = `[${opts.subsystem}]`; + const prefixColor = pickSubsystemColor(color, opts.subsystem); const levelColor = opts.level === "error" || opts.level === "fatal" ? color.red @@ -377,8 +411,7 @@ function formatConsoleLine(opts: { opts.style === "pretty" ? color.gray(new Date().toISOString().slice(11, 19)) : ""; - const prefixToken = - opts.style === "pretty" ? color.gray(prefix) : prefix; + const prefixToken = prefixColor(prefix); const head = [time, prefixToken].filter(Boolean).join(" "); return `${head} ${levelColor(opts.message)}`; } @@ -422,6 +455,7 @@ export function createSubsystemLogger(subsystem: string): SubsystemLogger { subsystem, message, style: consoleSettings.style, + colorMode: consoleSettings.color, meta, }); writeConsoleLine(level, line);