1
0
mirror of https://github.com/taigrr/log-socket synced 2026-03-20 14:52:27 -07:00

feat(log): add colored terminal output without external packages

Adds ANSI color-coded log levels for terminal output:
- TRACE: Gray
- DEBUG: Cyan
- INFO: Green
- NOTICE: Blue
- WARN: Yellow
- ERROR/PANIC/FATAL: Red (bold for PANIC/FATAL)

Colors are auto-detected based on terminal capability and can be
controlled via SetColorEnabled()/ColorEnabled().

Fixes #9
This commit is contained in:
2026-02-08 08:11:26 +00:00
parent 93dd230be0
commit 0453539d29
3 changed files with 139 additions and 1 deletions

View File

@@ -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

99
log/color.go Normal file
View File

@@ -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))
}

View File

@@ -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