logsdx
Version:
log streaming with dx on the ðŸ§
945 lines (901 loc) • 29.4 kB
text/typescript
import chalk, { type ChalkInstance } from "chalk";
import JSON5 from "json5";
import type { LogLevel } from "@/src/types";
import { loadConfig } from "./loader";
// Force chalk to use colors (level 3) for consistent testing
chalk.level = 3;
// Theme configuration type
export type ThemeConfig = {
name: string;
levels: {
[key in LogLevel]?: {
color: string;
bold?: boolean;
dim?: boolean;
italic?: boolean;
};
};
fields: {
[key: string]: {
color: string;
bold?: boolean;
dim?: boolean;
italic?: boolean;
};
};
status: {
[key: string]: {
color: string;
bold?: boolean;
dim?: boolean;
italic?: boolean;
};
};
patterns: {
[key: string]: {
regex: string | RegExp;
color: string;
bold?: boolean;
dim?: boolean;
italic?: boolean;
};
};
elements: {
brackets: {
color: string;
bold?: boolean;
dim?: boolean;
italic?: boolean;
};
keyValue: {
key: {
color: string;
bold?: boolean;
dim?: boolean;
italic?: boolean;
};
separator: {
color: string;
bold?: boolean;
dim?: boolean;
italic?: boolean;
};
value: {
color: string;
bold?: boolean;
dim?: boolean;
italic?: boolean;
};
};
};
};
// Built-in themes
export const THEMES = {
default: {
name: "default",
levels: {
error: { color: "red", bold: true },
warn: { color: "yellow" },
info: { color: "blue" },
debug: { color: "gray", dim: true },
success: { color: "green" },
trace: { color: "white", dim: true },
},
fields: {
timestamp: { color: "gray" },
service: { color: "cyan" },
action: { color: "magenta" },
user: { color: "blue" },
duration: { color: "yellow" },
requestId: { color: "gray" },
correlationId: { color: "gray" },
environment: { color: "green" },
version: { color: "magenta" },
method: { color: "cyan" },
path: { color: "white" },
statusCode: { color: "yellow" },
ip: { color: "gray" },
userAgent: { color: "gray", dim: true },
},
status: {
success: { color: "green" },
failure: { color: "red" },
error: { color: "red" },
pending: { color: "yellow" },
cancelled: { color: "gray" },
timeout: { color: "red" },
},
patterns: {
error: {
regex: /\b(error|exception|failed|failure)\b/i,
color: "red",
bold: true,
},
warning: { regex: /\b(warning|warn|caution)\b/i, color: "yellow" },
success: { regex: /\b(success|succeeded|completed)\b/i, color: "green" },
http: {
regex: /\b(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\b/,
color: "cyan",
},
url: { regex: /https?:\/\/[^\s]+/, color: "blue", italic: true },
ip: { regex: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/, color: "gray" },
email: { regex: /\b[\w\.-]+@[\w\.-]+\.\w+\b/, color: "blue" },
uuid: {
regex:
/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i,
color: "magenta",
},
timestamp: {
regex:
/\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})?\b/,
color: "gray",
},
json: { regex: /\{.*\}|\[.*\]/, color: "white" },
},
elements: {
brackets: { color: "yellow" },
keyValue: {
key: { color: "cyan" },
separator: { color: "gray" },
value: { color: "white" },
},
},
},
dark: {
name: "dark",
levels: {
error: { color: "#ff5555", bold: true },
warn: { color: "#ffb86c" },
info: { color: "#8be9fd" },
debug: { color: "#6272a4", dim: true },
success: { color: "#50fa7b" },
trace: { color: "#f8f8f2", dim: true },
},
fields: {
timestamp: { color: "#6272a4" },
service: { color: "#8be9fd" },
action: { color: "#bd93f9" },
user: { color: "#8be9fd" },
duration: { color: "#ffb86c" },
requestId: { color: "#6272a4" },
correlationId: { color: "#6272a4" },
environment: { color: "#50fa7b" },
version: { color: "#bd93f9" },
method: { color: "#8be9fd" },
path: { color: "#f8f8f2" },
statusCode: { color: "#ffb86c" },
ip: { color: "#6272a4" },
userAgent: { color: "#6272a4", dim: true },
},
status: {
success: { color: "#50fa7b" },
failure: { color: "#ff5555" },
error: { color: "#ff5555" },
pending: { color: "#ffb86c" },
cancelled: { color: "#6272a4" },
timeout: { color: "#ff5555" },
},
patterns: {
error: {
regex: /\b(error|exception|failed|failure)\b/i,
color: "#ff5555",
bold: true,
},
warning: { regex: /\b(warning|warn|caution)\b/i, color: "#ffb86c" },
success: {
regex: /\b(success|succeeded|completed)\b/i,
color: "#50fa7b",
},
http: {
regex: /\b(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\b/,
color: "#8be9fd",
},
url: { regex: /https?:\/\/[^\s]+/, color: "#8be9fd", italic: true },
ip: { regex: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/, color: "#6272a4" },
email: { regex: /\b[\w\.-]+@[\w\.-]+\.\w+\b/, color: "#8be9fd" },
uuid: {
regex:
/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i,
color: "#bd93f9",
},
timestamp: {
regex:
/\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})?\b/,
color: "#6272a4",
},
json: { regex: /\{.*\}|\[.*\]/, color: "#f8f8f2" },
},
elements: {
brackets: { color: "#6272a4" },
keyValue: {
key: { color: "#8be9fd" },
separator: { color: "#6272a4" },
value: { color: "#f8f8f2" },
},
},
},
light: {
name: "light",
levels: {
error: { color: "#d32f2f", bold: true },
warn: { color: "#f57c00" },
info: { color: "#1976d2" },
debug: { color: "#757575", dim: true },
success: { color: "#388e3c" },
trace: { color: "#212121", dim: true },
},
fields: {
timestamp: { color: "#757575" },
service: { color: "#0097a7" },
action: { color: "#7b1fa2" },
user: { color: "#1976d2" },
duration: { color: "#f57c00" },
requestId: { color: "#757575" },
correlationId: { color: "#757575" },
environment: { color: "#388e3c" },
version: { color: "#7b1fa2" },
method: { color: "#0097a7" },
path: { color: "#212121" },
statusCode: { color: "#f57c00" },
ip: { color: "#757575" },
userAgent: { color: "#757575", dim: true },
},
status: {
success: { color: "#388e3c" },
failure: { color: "#d32f2f" },
error: { color: "#d32f2f" },
pending: { color: "#f57c00" },
cancelled: { color: "#757575" },
timeout: { color: "#d32f2f" },
},
patterns: {
error: {
regex: /\b(error|exception|failed|failure)\b/i,
color: "#d32f2f",
bold: true,
},
warning: { regex: /\b(warning|warn|caution)\b/i, color: "#f57c00" },
success: {
regex: /\b(success|succeeded|completed)\b/i,
color: "#388e3c",
},
http: {
regex: /\b(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\b/,
color: "#0097a7",
},
url: { regex: /https?:\/\/[^\s]+/, color: "#1976d2", italic: true },
ip: { regex: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/, color: "#757575" },
email: { regex: /\b[\w\.-]+@[\w\.-]+\.\w+\b/, color: "#1976d2" },
uuid: {
regex:
/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i,
color: "#7b1fa2",
},
timestamp: {
regex:
/\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})?\b/,
color: "#757575",
},
json: { regex: /\{.*\}|\[.*\]/, color: "#212121" },
},
elements: {
brackets: { color: "#757575" },
keyValue: {
key: { color: "#0097a7" },
separator: { color: "#757575" },
value: { color: "#212121" },
},
},
},
minimal: {
name: "minimal",
levels: {
error: { color: "red" },
warn: { color: "yellow" },
info: { color: "white" },
debug: { color: "gray", dim: true },
success: { color: "green" },
trace: { color: "gray", dim: true },
},
fields: {
timestamp: { color: "gray" },
service: { color: "white" },
action: { color: "white" },
user: { color: "white" },
duration: { color: "white" },
requestId: { color: "gray" },
correlationId: { color: "gray" },
environment: { color: "white" },
version: { color: "white" },
method: { color: "white" },
path: { color: "white" },
statusCode: { color: "white" },
ip: { color: "gray" },
userAgent: { color: "gray", dim: true },
},
status: {
success: { color: "green" },
failure: { color: "red" },
error: { color: "red" },
pending: { color: "yellow" },
cancelled: { color: "gray" },
timeout: { color: "red" },
},
patterns: {
error: { regex: /\b(error|exception|failed|failure)\b/i, color: "red" },
warning: { regex: /\b(warning|warn|caution)\b/i, color: "yellow" },
success: { regex: /\b(success|succeeded|completed)\b/i, color: "green" },
http: {
regex: /\b(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\b/,
color: "white",
},
url: { regex: /https?:\/\/[^\s]+/, color: "white" },
ip: { regex: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/, color: "gray" },
email: { regex: /\b[\w\.-]+@[\w\.-]+\.\w+\b/, color: "white" },
uuid: {
regex:
/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i,
color: "white",
},
timestamp: {
regex:
/\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})?\b/,
color: "gray",
},
json: { regex: /\{.*\}|\[.*\]/, color: "white" },
},
elements: {
brackets: { color: "gray" },
keyValue: {
key: { color: "white" },
separator: { color: "gray" },
value: { color: "white" },
},
},
},
dracula: {
name: "dracula",
levels: {
error: { color: "#ff5555", bold: true }, // Red
warn: { color: "#f1fa8c" }, // Yellow
info: { color: "#8be9fd" }, // Cyan
debug: { color: "#6272a4", dim: true }, // Comment Purple
success: { color: "#50fa7b" }, // Green
trace: { color: "#f8f8f2", dim: true }, // Foreground
},
fields: {
timestamp: { color: "#6272a4" },
service: { color: "#ff79c6" }, // Pink
action: { color: "#bd93f9" }, // Purple
user: { color: "#8be9fd" }, // Cyan (Placeholder - adjust if needed)
duration: { color: "#f1fa8c" }, // Yellow
requestId: { color: "#6272a4" },
correlationId: { color: "#6272a4" },
environment: { color: "#50fa7b" }, // Green
version: { color: "#bd93f9" }, // Purple
method: { color: "#8be9fd" }, // Cyan
path: { color: "#f8f8f2" }, // Foreground
statusCode: { color: "#ffb86c" }, // Orange
ip: { color: "#6272a4" },
userAgent: { color: "#6272a4", dim: true },
},
status: {
success: { color: "#50fa7b" },
failure: { color: "#ff5555" },
error: { color: "#ff5555" },
pending: { color: "#f1fa8c" },
cancelled: { color: "#6272a4" },
timeout: { color: "#ff5555" },
},
patterns: {
// Colors adjusted to match Dracula
error: {
regex: /\b(error|exception|failed|failure)\b/i,
color: "#ff5555",
bold: true,
},
warning: { regex: /\b(warning|warn|caution)\b/i, color: "#f1fa8c" },
success: {
regex: /\b(success|succeeded|completed)\b/i,
color: "#50fa7b",
},
http: {
regex: /\b(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\b/,
color: "#8be9fd",
},
url: { regex: /https?:\/\/[^\s]+/, color: "#8be9fd", italic: true },
ip: { regex: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/, color: "#6272a4" },
email: { regex: /\b[\w\.-]+@[\w\.-]+\.\w+\b/, color: "#8be9fd" },
uuid: {
regex:
/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i,
color: "#bd93f9",
},
timestamp: {
regex:
/\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})?\b/,
color: "#6272a4",
},
json: { regex: /\{.*\}|\[.*\]/, color: "#f8f8f2" },
},
elements: {
brackets: { color: "#f8f8f2" }, // Foreground for {}, [], "
keyValue: {
key: { color: "#8be9fd" }, // Cyan for keys
separator: { color: "#ff79c6" }, // Pink for :
value: { color: "#f8f8f2" }, // Foreground for values
},
},
},
githubLight: {
name: "githubLight",
levels: {
error: { color: "#d73a49", bold: true }, // Red-600
warn: { color: "#b08800" }, // Yellow-700
info: { color: "#0366d6" }, // Blue-600
debug: { color: "#586069", dim: true }, // Gray-600
success: { color: "#22863a" }, // Green-600
trace: { color: "#24292e", dim: true }, // Gray-900 (Default Text)
},
fields: {
timestamp: { color: "#586069" },
service: { color: "#6f42c1" }, // Purple-600
action: { color: "#005cc5" }, // Blue-700
user: { color: "#0366d6" }, // Blue-600
duration: { color: "#e36209" }, // Orange-600
requestId: { color: "#586069" },
correlationId: { color: "#586069" },
environment: { color: "#22863a" }, // Green-600
version: { color: "#6f42c1" }, // Purple-600
method: { color: "#005cc5" }, // Blue-700
path: { color: "#24292e" }, // Gray-900
statusCode: { color: "#b08800" }, // Yellow-700
ip: { color: "#586069" },
userAgent: { color: "#586069", dim: true },
},
status: {
success: { color: "#22863a" },
failure: { color: "#d73a49" },
error: { color: "#d73a49" },
pending: { color: "#b08800" },
cancelled: { color: "#586069" },
timeout: { color: "#d73a49" },
},
patterns: {
error: {
regex: /\b(error|exception|failed|failure)\b/i,
color: "#d73a49",
bold: true,
},
warning: { regex: /\b(warning|warn|caution)\b/i, color: "#b08800" },
success: {
regex: /\b(success|succeeded|completed)\b/i,
color: "#22863a",
},
http: {
regex: /\b(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\b/,
color: "#005cc5",
},
url: { regex: /https?:\/\/[^\s]+/, color: "#0366d6", italic: true },
ip: { regex: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/, color: "#586069" },
email: { regex: /\b[\w\.-]+@[\w\.-]+\.\w+\b/, color: "#0366d6" },
uuid: {
regex:
/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i,
color: "#6f42c1",
},
timestamp: {
regex:
/\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})?\b/,
color: "#586069",
},
json: { regex: /\{.*\}|\[.*\]/, color: "#24292e" },
},
elements: {
brackets: { color: "#586069" },
keyValue: {
key: { color: "#005cc5" },
separator: { color: "#586069" },
value: { color: "#24292e" },
},
},
},
githubDark: {
name: "githubDark",
levels: {
error: { color: "#f97583", bold: true },
warn: { color: "#dbab09" },
info: { color: "#79b8ff" },
debug: { color: "#6a737d", dim: true },
success: { color: "#85e89d" },
trace: { color: "#d1d5da", dim: true },
},
fields: {
timestamp: { color: "#6a737d" },
service: { color: "#b392f0" },
action: { color: "#58a6ff" },
user: { color: "#79b8ff" },
duration: { color: "#ffab70" },
requestId: { color: "#6a737d" },
correlationId: { color: "#6a737d" },
environment: { color: "#85e89d" },
version: { color: "#b392f0" },
method: { color: "#58a6ff" },
path: { color: "#d1d5da" },
statusCode: { color: "#dbab09" },
ip: { color: "#6a737d" },
userAgent: { color: "#6a737d", dim: true },
},
status: {
success: { color: "#85e89d" },
failure: { color: "#f97583" },
error: { color: "#f97583" },
pending: { color: "#dbab09" },
cancelled: { color: "#6a737d" },
timeout: { color: "#f97583" },
},
patterns: {
error: {
regex: /\b(error|exception|failed|failure)\b/i,
color: "#f97583",
bold: true,
},
warning: { regex: /\b(warning|warn|caution)\b/i, color: "#dbab09" },
success: {
regex: /\b(success|succeeded|completed)\b/i,
color: "#85e89d",
},
http: {
regex: /\b(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\b/,
color: "#58a6ff",
},
url: { regex: /https?:\/\/[^\s]+/, color: "#79b8ff", italic: true },
ip: { regex: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/, color: "#6a737d" },
email: { regex: /\b[\w\.-]+@[\w\.-]+\.\w+\b/, color: "#79b8ff" },
uuid: {
regex:
/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i,
color: "#b392f0",
},
timestamp: {
regex:
/\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})?\b/,
color: "#6a737d",
},
json: { regex: /\{.*\}|\[.*\]/, color: "#d1d5da" },
},
elements: {
brackets: { color: "#6a737d" },
keyValue: {
key: { color: "#79b8ff" },
separator: { color: "#6a737d" },
value: { color: "#d1d5da" },
},
},
},
} as const;
// Create a chalk style from theme config
export function createStyle(
config:
| ThemeConfig["levels"][LogLevel]
| ThemeConfig["fields"][string]
| ThemeConfig["status"][string]
| ThemeConfig["patterns"][string]
| ThemeConfig["elements"]["brackets"]
| ThemeConfig["elements"]["keyValue"]["key"]
| ThemeConfig["elements"]["keyValue"]["separator"]
| ThemeConfig["elements"]["keyValue"]["value"],
): ChalkInstance {
if (!config) return chalk.white;
const color = config.color;
let style: ChalkInstance;
// Handle hex colors
if (color.startsWith("#")) {
style = chalk.hex(color);
} else {
// Handle chalk colors
style =
(chalk[color as keyof typeof chalk] as ChalkInstance) || chalk.white;
}
if (config.bold) style = style.bold;
if (config.dim) style = style.dim;
if (config.italic) style = style.italic;
return style;
}
// Style manager class for handling themes and caching
export class StyleManager {
private static instance: StyleManager;
public currentTheme: ThemeConfig;
private styleCache: Map<string, ChalkInstance>;
private patternCache: Map<string, { regex: RegExp; style: ChalkInstance }>;
private constructor() {
this.currentTheme = THEMES.default;
this.styleCache = new Map();
this.patternCache = new Map();
this.initializeCache();
}
public static getInstance(): StyleManager {
if (!StyleManager.instance) {
StyleManager.instance = new StyleManager();
}
return StyleManager.instance;
}
private initializeCache(): void {
// Cache level styles
Object.entries(this.currentTheme.levels).forEach(([level, config]) => {
this.styleCache.set(`level:${level}`, createStyle(config));
});
// Cache field styles
Object.entries(this.currentTheme.fields).forEach(([field, config]) => {
this.styleCache.set(`field:${field}`, createStyle(config));
});
// Cache status styles
Object.entries(this.currentTheme.status).forEach(([status, config]) => {
this.styleCache.set(`status:${status}`, createStyle(config));
});
// Cache element styles
this.styleCache.set(
"brackets",
createStyle(this.currentTheme.elements.brackets),
);
this.styleCache.set(
"keyValue:key",
createStyle(this.currentTheme.elements.keyValue.key),
);
this.styleCache.set(
"keyValue:separator",
createStyle(this.currentTheme.elements.keyValue.separator),
);
this.styleCache.set(
"keyValue:value",
createStyle(this.currentTheme.elements.keyValue.value),
);
// Cache pattern styles
Object.entries(this.currentTheme.patterns).forEach(([pattern, config]) => {
this.patternCache.set(pattern, {
regex:
typeof config.regex === "string"
? new RegExp(config.regex)
: config.regex,
style: createStyle(config),
});
});
}
public setTheme(
themeName: keyof typeof THEMES | Partial<ThemeConfig> | string,
): void {
if (typeof themeName === "string") {
// Check if it's a built-in theme
if (themeName in THEMES) {
this.currentTheme = THEMES[themeName as keyof typeof THEMES];
} else {
// Try to load from config
const config = loadConfig();
if (
config?.customThemes &&
themeName in config.customThemes &&
config.customThemes[themeName]
) {
// Ensure we have a valid theme config by merging with default
const customTheme = {
...THEMES.default,
...config.customThemes[themeName],
name: themeName,
};
this.currentTheme = customTheme;
} else {
// Fall back to default theme if not found
this.currentTheme = THEMES.default;
}
}
} else {
// Merge with default theme to ensure all required properties are present
this.currentTheme = {
name: themeName.name || "custom",
levels: { ...THEMES.default.levels, ...themeName.levels },
fields: { ...THEMES.default.fields, ...themeName.fields },
status: { ...THEMES.default.status, ...themeName.status },
patterns: { ...THEMES.default.patterns, ...themeName.patterns },
elements: {
brackets: {
...THEMES.default.elements.brackets,
...themeName.elements?.brackets,
},
keyValue: {
key: {
...THEMES.default.elements.keyValue.key,
...themeName.elements?.keyValue?.key,
},
separator: {
...THEMES.default.elements.keyValue.separator,
...themeName.elements?.keyValue?.separator,
},
value: {
...THEMES.default.elements.keyValue.value,
...themeName.elements?.keyValue?.value,
},
},
},
};
}
this.styleCache.clear();
this.patternCache.clear();
this.initializeCache();
}
public getLevelStyle(level: LogLevel): ChalkInstance {
return this.styleCache.get(`level:${level}`) || chalk.white;
}
public getFieldStyle(field: string): ChalkInstance {
return this.styleCache.get(`field:${field}`) || chalk.white;
}
public getStatusStyle(status: string): ChalkInstance {
return this.styleCache.get(`status:${status}`) || chalk.white;
}
public getBracketStyle(): ChalkInstance {
return this.styleCache.get("brackets") || chalk.gray;
}
public getKeyValueStyles(): {
key: ChalkInstance;
separator: ChalkInstance;
value: ChalkInstance;
} {
return {
key: this.styleCache.get("keyValue:key") || chalk.cyan,
separator: this.styleCache.get("keyValue:separator") || chalk.gray,
value: this.styleCache.get("keyValue:value") || chalk.white,
};
}
public applyPatternStyles(text: string): string {
let result = text;
// Sort patterns by length (longest first) to avoid overlapping matches
const sortedPatterns = Array.from(this.patternCache.entries()).sort(
(a, b) => {
const aLength = a[1].regex.toString().length;
const bLength = b[1].regex.toString().length;
return bLength - aLength;
},
);
for (const [_, { regex, style }] of sortedPatterns) {
result = result.replace(regex, (match) => style(match));
}
return result;
}
public formatJson(obj: Record<string, any>): string {
const lines: string[] = [];
const bracketStyle = this.getBracketStyle();
const {
key: keyStyle,
separator: separatorStyle,
value: valueStyle,
} = this.getKeyValueStyles();
lines.push(bracketStyle("{"));
const entries = Object.entries(obj);
const lastEntry = entries[entries.length - 1];
for (const [key, val] of entries) {
const formattedValue =
typeof val === "object" ? JSON.stringify(val, null, 2) : String(val);
const isLast = lastEntry && lastEntry[0] === key;
lines.push(
` ${bracketStyle('"')}${keyStyle(key)}${bracketStyle('"')}${separatorStyle(":")} ${valueStyle(formattedValue)}${isLast ? "" : separatorStyle(",")}`,
);
}
lines.push(bracketStyle("}"));
return lines.join("\n");
}
styleLine(
line: string,
parsed?: { level?: LogLevel; [key: string]: any },
parserName?: string,
): string {
// If parser is json, try to format it compactly
if (parserName === "json") {
try {
const json = JSON5.parse(line);
return formatJsonCompact(json, this); // Use the compact formatter
} catch (error) {
// JSON parsing failed, fall back to default styling for the raw line
// Use level from parsed object if available, otherwise default to 'info'
const level = parsed?.level || "info";
const style = this.getLevelStyle(level);
return this.applyPatternStyles(style(line));
}
}
// Fallback for non-JSON parsers or if parsing failed above
const level = parsed?.level || "info";
const style = this.getLevelStyle(level);
const styledLine = style(line);
return this.applyPatternStyles(styledLine);
}
}
// Export singleton instance
export const styleManager = StyleManager.getInstance();
// UPDATE: Accept optional parserName and pass it to the manager's method
export const styleLine = (
line: string,
parsed?: { level?: LogLevel; [key: string]: any },
parserName?: string, // Added parserName argument
): string => {
// Remove the previous logic, delegate entirely to the manager instance
return styleManager.styleLine(line, parsed, parserName);
// OLD Logic moved/modified into styleManager.styleLine:
// if (parsed.language === "json") { ... }
// Apply level-based styling
// const level = parsed.level || "info";
// const styledLine = styleManager.getLevelStyle(level)(line);
// Apply pattern-based styling
// return styleManager.applyPatternStyles(styledLine);
};
// Export theme-related functions
export const setTheme = (
theme: keyof typeof THEMES | Partial<ThemeConfig> | string,
): void => {
if (typeof theme === "string") {
// Check if it's a built-in theme
if (theme in THEMES) {
styleManager.setTheme(theme as keyof typeof THEMES);
} else {
// Try to load from config
const config = loadConfig();
if (
config?.customThemes &&
theme in config.customThemes &&
config.customThemes[theme]
) {
// Ensure we have a valid theme config by merging with default
const customTheme = {
...THEMES.default,
...config.customThemes[theme],
name: theme,
};
styleManager.setTheme(customTheme);
} else {
// Fall back to default theme if not found
styleManager.setTheme("default");
}
}
} else {
styleManager.setTheme(theme);
}
};
export const getTheme = () => styleManager.currentTheme;
// NEW function: Format JSON compactly on a single line with colors
export function formatJsonCompact(
obj: any,
styleManager: StyleManager,
isRoot = true,
): string {
const bracketStyle = styleManager.getBracketStyle();
const {
key: keyStyle,
separator: separatorStyle,
value: valueStyle,
} = styleManager.getKeyValueStyles();
if (obj === null) {
return valueStyle("null");
}
if (typeof obj === "string") {
// NEW: Style quotes with bracketStyle, content with valueStyle
return `${bracketStyle('"')}${valueStyle(obj)}${bracketStyle('"')}`;
}
if (typeof obj === "number" || typeof obj === "boolean") {
return valueStyle(String(obj));
}
if (Array.isArray(obj)) {
const items = obj
.map((item) => formatJsonCompact(item, styleManager, false))
.join(separatorStyle(", "));
return `${bracketStyle("[")}${items}${bracketStyle("]")}`;
}
if (typeof obj === "object") {
const entries = Object.entries(obj)
.map(([key, value]) => {
const formattedKey = `${bracketStyle('"')}${keyStyle(key)}${bracketStyle('"')}`;
const formattedValue = formatJsonCompact(value, styleManager, false);
return `${formattedKey}${separatorStyle(":")}${formattedValue}`;
})
.join(separatorStyle(", "));
return `${bracketStyle("{")}${entries}${bracketStyle("}")}`;
}
// Fallback for unexpected types
return valueStyle(String(obj));
}