Feature: Add colorized log handler for improved human-readable console output

This commit is contained in:
Jeffrey Paul 2025-05-22 07:00:36 -07:00
parent 9958c4e352
commit e769487555

View File

@ -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() {