feat(logging): add console color modes
This commit is contained in:
@@ -49,6 +49,7 @@ If set, CLAWDIS derives defaults (only when you haven’t set them explicitly):
|
|||||||
- Console output can be tuned separately via:
|
- Console output can be tuned separately via:
|
||||||
- `logging.consoleLevel` (defaults to `info`, bumps to `debug` when `--verbose`)
|
- `logging.consoleLevel` (defaults to `info`, bumps to `debug` when `--verbose`)
|
||||||
- `logging.consoleStyle` (`pretty` | `compact` | `json`)
|
- `logging.consoleStyle` (`pretty` | `compact` | `json`)
|
||||||
|
- `logging.consoleColor` (`auto` | `always` | `never`)
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
@@ -56,7 +57,8 @@ If set, CLAWDIS derives defaults (only when you haven’t set them explicitly):
|
|||||||
level: "info",
|
level: "info",
|
||||||
file: "/tmp/clawdis/clawdis.log",
|
file: "/tmp/clawdis/clawdis.log",
|
||||||
consoleLevel: "info",
|
consoleLevel: "info",
|
||||||
consoleStyle: "pretty"
|
consoleStyle: "pretty",
|
||||||
|
consoleColor: "auto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -74,7 +74,8 @@ Subsystem loggers are created via `createSubsystemLogger("gateway")`.
|
|||||||
Behavior:
|
Behavior:
|
||||||
|
|
||||||
- **Subsystem prefixes** on every line (e.g. `[gateway]`, `[canvas]`, `[tailscale]`)
|
- **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 }`)
|
- **Sub-loggers by subsystem** (auto prefix + structured field `{ subsystem }`)
|
||||||
- **`logRaw()`** for QR/UX output (no prefix, no formatting)
|
- **`logRaw()`** for QR/UX output (no prefix, no formatting)
|
||||||
- **Console styles** (e.g. `pretty | compact | json`)
|
- **Console styles** (e.g. `pretty | compact | json`)
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export type LoggingConfig = {
|
|||||||
| "debug"
|
| "debug"
|
||||||
| "trace";
|
| "trace";
|
||||||
consoleStyle?: "pretty" | "compact" | "json";
|
consoleStyle?: "pretty" | "compact" | "json";
|
||||||
|
consoleColor?: "auto" | "always" | "never";
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WebReconnectConfig = {
|
export type WebReconnectConfig = {
|
||||||
@@ -269,6 +270,9 @@ const ClawdisSchema = z.object({
|
|||||||
consoleStyle: z
|
consoleStyle: z
|
||||||
.union([z.literal("pretty"), z.literal("compact"), z.literal("json")])
|
.union([z.literal("pretty"), z.literal("compact"), z.literal("json")])
|
||||||
.optional(),
|
.optional(),
|
||||||
|
consoleColor: z
|
||||||
|
.union([z.literal("auto"), z.literal("always"), z.literal("never")])
|
||||||
|
.optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
browser: z
|
browser: z
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export type LoggerSettings = {
|
|||||||
file?: string;
|
file?: string;
|
||||||
consoleLevel?: Level;
|
consoleLevel?: Level;
|
||||||
consoleStyle?: ConsoleStyle;
|
consoleStyle?: ConsoleStyle;
|
||||||
|
consoleColor?: ConsoleColor;
|
||||||
};
|
};
|
||||||
|
|
||||||
type LogObj = { date?: Date } & Record<string, unknown>;
|
type LogObj = { date?: Date } & Record<string, unknown>;
|
||||||
@@ -45,9 +46,11 @@ type ResolvedSettings = {
|
|||||||
export type LoggerResolvedSettings = ResolvedSettings;
|
export type LoggerResolvedSettings = ResolvedSettings;
|
||||||
|
|
||||||
export type ConsoleStyle = "pretty" | "compact" | "json";
|
export type ConsoleStyle = "pretty" | "compact" | "json";
|
||||||
|
export type ConsoleColor = "auto" | "always" | "never";
|
||||||
type ConsoleSettings = {
|
type ConsoleSettings = {
|
||||||
level: Level;
|
level: Level;
|
||||||
style: ConsoleStyle;
|
style: ConsoleStyle;
|
||||||
|
color: ConsoleColor;
|
||||||
};
|
};
|
||||||
export type ConsoleLoggerSettings = ConsoleSettings;
|
export type ConsoleLoggerSettings = ConsoleSettings;
|
||||||
|
|
||||||
@@ -87,7 +90,8 @@ function resolveConsoleSettings(): ConsoleSettings {
|
|||||||
overrideSettings ?? loadConfig().logging;
|
overrideSettings ?? loadConfig().logging;
|
||||||
const level = normalizeConsoleLevel(cfg?.consoleLevel);
|
const level = normalizeConsoleLevel(cfg?.consoleLevel);
|
||||||
const style = normalizeConsoleStyle(cfg?.consoleStyle);
|
const style = normalizeConsoleStyle(cfg?.consoleStyle);
|
||||||
return { level, style };
|
const color = normalizeConsoleColor(cfg?.consoleColor);
|
||||||
|
return { level, style, color };
|
||||||
}
|
}
|
||||||
|
|
||||||
function settingsChanged(a: ResolvedSettings | null, b: ResolvedSettings) {
|
function settingsChanged(a: ResolvedSettings | null, b: ResolvedSettings) {
|
||||||
@@ -100,7 +104,7 @@ function consoleSettingsChanged(
|
|||||||
b: ConsoleSettings,
|
b: ConsoleSettings,
|
||||||
) {
|
) {
|
||||||
if (!a) return true;
|
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 {
|
function levelToMinLevel(level: Level): number {
|
||||||
@@ -133,6 +137,13 @@ function normalizeConsoleStyle(style?: string): ConsoleStyle {
|
|||||||
return "pretty";
|
return "pretty";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeConsoleColor(color?: string): ConsoleColor {
|
||||||
|
if (color === "auto" || color === "always" || color === "never") {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
return "auto";
|
||||||
|
}
|
||||||
|
|
||||||
function buildLogger(settings: ResolvedSettings): TsLogger<LogObj> {
|
function buildLogger(settings: ResolvedSettings): TsLogger<LogObj> {
|
||||||
fs.mkdirSync(path.dirname(settings.file), { recursive: true });
|
fs.mkdirSync(path.dirname(settings.file), { recursive: true });
|
||||||
// Clean up stale rolling logs when using a dated log filename.
|
// 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;
|
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 =
|
const supports =
|
||||||
process.stdout.isTTY &&
|
process.stdout.isTTY &&
|
||||||
!process.env.NO_COLOR &&
|
!process.env.NO_COLOR &&
|
||||||
@@ -347,11 +360,31 @@ function getColorForConsole(): Chalk {
|
|||||||
return supports ? chalk : new Chalk({ level: 0 });
|
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: {
|
function formatConsoleLine(opts: {
|
||||||
level: Level;
|
level: Level;
|
||||||
subsystem: string;
|
subsystem: string;
|
||||||
message: string;
|
message: string;
|
||||||
style: ConsoleStyle;
|
style: ConsoleStyle;
|
||||||
|
colorMode: ConsoleColor;
|
||||||
meta?: Record<string, unknown>;
|
meta?: Record<string, unknown>;
|
||||||
}): string {
|
}): string {
|
||||||
if (opts.style === "json") {
|
if (opts.style === "json") {
|
||||||
@@ -363,8 +396,9 @@ function formatConsoleLine(opts: {
|
|||||||
...opts.meta,
|
...opts.meta,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const color = getColorForConsole();
|
const color = getColorForConsole(opts.colorMode);
|
||||||
const prefix = `[${opts.subsystem}]`;
|
const prefix = `[${opts.subsystem}]`;
|
||||||
|
const prefixColor = pickSubsystemColor(color, opts.subsystem);
|
||||||
const levelColor =
|
const levelColor =
|
||||||
opts.level === "error" || opts.level === "fatal"
|
opts.level === "error" || opts.level === "fatal"
|
||||||
? color.red
|
? color.red
|
||||||
@@ -377,8 +411,7 @@ function formatConsoleLine(opts: {
|
|||||||
opts.style === "pretty"
|
opts.style === "pretty"
|
||||||
? color.gray(new Date().toISOString().slice(11, 19))
|
? color.gray(new Date().toISOString().slice(11, 19))
|
||||||
: "";
|
: "";
|
||||||
const prefixToken =
|
const prefixToken = prefixColor(prefix);
|
||||||
opts.style === "pretty" ? color.gray(prefix) : prefix;
|
|
||||||
const head = [time, prefixToken].filter(Boolean).join(" ");
|
const head = [time, prefixToken].filter(Boolean).join(" ");
|
||||||
return `${head} ${levelColor(opts.message)}`;
|
return `${head} ${levelColor(opts.message)}`;
|
||||||
}
|
}
|
||||||
@@ -422,6 +455,7 @@ export function createSubsystemLogger(subsystem: string): SubsystemLogger {
|
|||||||
subsystem,
|
subsystem,
|
||||||
message,
|
message,
|
||||||
style: consoleSettings.style,
|
style: consoleSettings.style,
|
||||||
|
colorMode: consoleSettings.color,
|
||||||
meta,
|
meta,
|
||||||
});
|
});
|
||||||
writeConsoleLine(level, line);
|
writeConsoleLine(level, line);
|
||||||
|
|||||||
Reference in New Issue
Block a user