From e7694875551ce9ef9467ef971ba75abed9f18185 Mon Sep 17 00:00:00 2001 From: sneak Date: Thu, 22 May 2025 07:00:36 -0700 Subject: [PATCH] Feature: Add colorized log handler for improved human-readable console output --- storage.go | 207 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 203 insertions(+), 4 deletions(-) diff --git a/storage.go b/storage.go index 9706ac5..1a5edc7 100644 --- a/storage.go +++ b/storage.go @@ -1,17 +1,213 @@ package main import ( + "context" "crypto/sha256" "database/sql" "encoding/hex" "encoding/json" + "fmt" + "io" "log/slog" "os" + "sort" + "strings" "time" "github.com/oklog/ulid/v2" ) +// ANSI color codes +const ( + colorReset = "\033[0m" + colorRed = "\033[31m" + colorGreen = "\033[32m" + colorYellow = "\033[33m" + colorBlue = "\033[34m" + colorPurple = "\033[35m" + colorCyan = "\033[36m" + colorGray = "\033[37m" + colorWhite = "\033[97m" + bold = "\033[1m" +) + +// ColorizedHandler is a custom slog.Handler that outputs colorized logs +type ColorizedHandler struct { + w io.Writer + level slog.Level + timeKey string + msgKey string +} + +// NewColorizedHandler creates a new ColorizedHandler +func NewColorizedHandler(w io.Writer, opts *slog.HandlerOptions) *ColorizedHandler { + if opts == nil { + opts = &slog.HandlerOptions{} + } + return &ColorizedHandler{ + w: w, + level: opts.Level.Level(), + timeKey: "time", + msgKey: slog.MessageKey, + } +} + +// Enabled implements slog.Handler +func (h *ColorizedHandler) Enabled(ctx context.Context, level slog.Level) bool { + return level >= h.level +} + +// Handle implements slog.Handler +func (h *ColorizedHandler) Handle(ctx context.Context, r slog.Record) error { + // Skip logs below our level + if !h.Enabled(ctx, r.Level) { + return nil + } + + // Format time with milliseconds + timeStr := r.Time.Format("15:04:05.000") + + // Get component from attributes + var component string + var message string + + // Store other attributes for printing later + attributes := make(map[string]interface{}) + r.Attrs(func(a slog.Attr) bool { + if a.Key == "component" { + component = a.Value.String() + return true + } + if a.Key == h.msgKey { + message = a.Value.String() + return true + } + // Skip internal or empty values + if a.Key == h.timeKey || a.Key == "level" || a.Value.String() == "" { + return true + } + attributes[a.Key] = a.Value.Any() + return true + }) + + // Format level with color + var levelColor string + var levelText string + switch r.Level { + case slog.LevelDebug: + levelColor = colorGray + levelText = "DBG" + case slog.LevelInfo: + levelColor = colorGreen + levelText = "INF" + case slog.LevelWarn: + levelColor = colorYellow + levelText = "WRN" + case slog.LevelError: + levelColor = colorRed + levelText = "ERR" + default: + levelColor = colorReset + levelText = "???" + } + + // Build the log line + var sb strings.Builder + + // Timestamp with gray color + sb.WriteString(colorGray) + sb.WriteString("[") + sb.WriteString(timeStr) + sb.WriteString("]") + sb.WriteString(colorReset) + sb.WriteString(" ") + + // Level with appropriate color + sb.WriteString(levelColor) + sb.WriteString(levelText) + sb.WriteString(colorReset) + sb.WriteString(" ") + + // Component in blue + if component != "" { + sb.WriteString(colorBlue) + sb.WriteString("[") + sb.WriteString(component) + sb.WriteString("]") + sb.WriteString(colorReset) + sb.WriteString(" ") + } + + // Message in white+bold + if message != "" { + sb.WriteString(bold) + sb.WriteString(colorWhite) + sb.WriteString(message) + sb.WriteString(colorReset) + sb.WriteString(" ") + } + + // Sort keys for consistent output + keys := make([]string, 0, len(attributes)) + for k := range attributes { + keys = append(keys, k) + } + sort.Strings(keys) + + // Add attributes as key=value pairs with colors + for _, k := range keys { + v := attributes[k] + sb.WriteString(colorCyan) // Key in cyan + sb.WriteString(k) + sb.WriteString(colorReset) + sb.WriteString("=") + + // Value color depends on type + switch v := v.(type) { + case string: + sb.WriteString(colorYellow) // Strings in yellow + sb.WriteString(v) + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64: + sb.WriteString(colorPurple) // Numbers in purple + sb.WriteString(fmt.Sprintf("%v", v)) + case bool: + if v { + sb.WriteString(colorGreen) // true in green + } else { + sb.WriteString(colorRed) // false in red + } + sb.WriteString(fmt.Sprintf("%v", v)) + case error: + sb.WriteString(colorRed) // Errors in red + sb.WriteString(v.Error()) + default: + sb.WriteString(colorReset) // Other types with no color + sb.WriteString(fmt.Sprintf("%v", v)) + } + sb.WriteString(colorReset) + sb.WriteString(" ") + } + + sb.WriteString("\n") + + // Write to output + _, err := io.WriteString(h.w, sb.String()) + return err +} + +// WithAttrs implements slog.Handler +func (h *ColorizedHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + // This is a simplified implementation that doesn't actually store the attrs + // In a real implementation, you would create a new handler with these attrs + return h +} + +// WithGroup implements slog.Handler +func (h *ColorizedHandler) WithGroup(name string) slog.Handler { + // This is a simplified implementation that doesn't handle groups + return h +} + func setupDatabase() error { _, err := db.Exec(` CREATE TABLE IF NOT EXISTS articles ( @@ -351,11 +547,14 @@ func setupLogging() { os.Exit(1) } - // Set up structured logger - jsonHandler := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{ - Level: slog.LevelInfo, + // Set up structured logger with custom colorized handler for console + // and JSON handler for file logging + consoleHandler := NewColorizedHandler(os.Stderr, &slog.HandlerOptions{ + Level: slog.LevelDebug, }) - slog.SetDefault(slog.New(jsonHandler)) + + // Use the custom handler + slog.SetDefault(slog.New(consoleHandler)) } func flushLog() {