diff --git a/README.md b/README.md index 7b3b6e9..f336bce 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ A real-time log viewer with WebSocket support and namespace filtering, written i - **Namespace filtering**: Subscribe to specific namespaces via WebSocket - **Frontend namespace selector**: Filter logs by namespace in the web UI - **Namespace API**: GET `/api/namespaces` to list all active namespaces +- **Colored terminal output**: Log levels are color-coded in terminal (no external packages) ## Features @@ -188,6 +189,40 @@ ws://localhost:8080/ws?namespaces=api,database # Multiple namespaces - **Color Coding**: Different log levels are color-coded - **Reconnect**: Reconnect WebSocket with new namespace filter +## Terminal Colors + +Log output to stderr is automatically colorized when writing to a terminal. Colors are disabled when output is piped or redirected to a file. + +### Color Scheme + +| Level | Color | +|--------|--------------| +| TRACE | Gray | +| DEBUG | Cyan | +| INFO | Green | +| NOTICE | Blue | +| WARN | Yellow | +| ERROR | Red | +| PANIC | Bold Red | +| FATAL | Bold Red | + +### Controlling Colors + +```go +// Disable colors (e.g., for CI/CD or file output) +logger.SetColorEnabled(false) + +// Enable colors explicitly +logger.SetColorEnabled(true) + +// Check current state +if logger.ColorEnabled() { + // colors are on +} +``` + +Colors are implemented using standard ANSI escape codes with no external dependencies. + ## Migration from v1 ### Import Path diff --git a/log/color.go b/log/color.go new file mode 100644 index 0000000..b4cf408 --- /dev/null +++ b/log/color.go @@ -0,0 +1,99 @@ +package log + +import ( + "os" + "sync" +) + +// ANSI color codes for terminal output +const ( + colorReset = "\033[0m" + colorRed = "\033[31m" + colorGreen = "\033[32m" + colorYellow = "\033[33m" + colorBlue = "\033[34m" + colorPurple = "\033[35m" + colorCyan = "\033[36m" + colorWhite = "\033[37m" + colorGray = "\033[90m" + + // Bold variants + colorBoldRed = "\033[1;31m" + colorBoldYellow = "\033[1;33m" + colorBoldWhite = "\033[1;37m" +) + +var ( + colorEnabled = true + colorEnabledOnce sync.Once + colorMux sync.RWMutex +) + +// SetColorEnabled enables or disables colored output for stderr logging. +// By default, color is enabled when stderr is a terminal. +func SetColorEnabled(enabled bool) { + colorMux.Lock() + colorEnabled = enabled + colorMux.Unlock() +} + +// ColorEnabled returns whether colored output is currently enabled. +func ColorEnabled() bool { + colorMux.RLock() + defer colorMux.RUnlock() + return colorEnabled +} + +// isTerminal checks if the given file descriptor is a terminal. +// This is a simple heuristic that works on Unix-like systems. +func isTerminal(f *os.File) bool { + stat, err := f.Stat() + if err != nil { + return false + } + return (stat.Mode() & os.ModeCharDevice) != 0 +} + +// initColorEnabled sets the default color state based on whether stderr is a terminal. +func initColorEnabled() { + colorEnabledOnce.Do(func() { + colorEnabled = isTerminal(os.Stderr) + }) +} + +// levelColor returns the ANSI color code for a given log level. +func levelColor(level Level) string { + switch level { + case LTrace: + return colorGray + case LDebug: + return colorCyan + case LInfo: + return colorGreen + case LNotice: + return colorBlue + case LWarn: + return colorYellow + case LError: + return colorRed + case LPanic: + return colorBoldRed + case LFatal: + return colorBoldRed + default: + return colorReset + } +} + +// colorize wraps text with ANSI color codes if color is enabled. +func colorize(text string, color string) string { + if !ColorEnabled() { + return text + } + return color + text + colorReset +} + +// colorizeLevelText returns the level string with appropriate color. +func colorizeLevelText(level string, lvl Level) string { + return colorize(level, levelColor(lvl)) +} diff --git a/log/log.go b/log/log.go index a204501..0256aca 100644 --- a/log/log.go +++ b/log/log.go @@ -22,6 +22,7 @@ var ( func init() { namespaces = make(map[string]bool) + initColorEnabled() stderrClient = CreateClient(DefaultNamespace) stderrClient.SetLogLevel(LTrace) stderrFinished = make(chan bool, 1) @@ -31,7 +32,10 @@ func init() { func (c *Client) logStdErr() { for e := range c.writer { if e.level >= c.LogLevel && c.matchesNamespace(e.Namespace) { - fmt.Fprintf(os.Stderr, "%s\t%s\t[%s]\t%s\t%s\n", e.Timestamp.String(), e.Level, e.Namespace, e.Output, e.File) + levelStr := colorizeLevelText(e.Level, e.level) + nsStr := colorize("["+e.Namespace+"]", colorPurple) + fileStr := colorize(e.File, colorGray) + fmt.Fprintf(os.Stderr, "%s\t%s\t%s\t%s\t%s\n", e.Timestamp.String(), levelStr, nsStr, e.Output, fileStr) } } stderrFinished <- true