Feature: Add colorized log handler for improved human-readable console output
This commit is contained in:
parent
9958c4e352
commit
e769487555
207
storage.go
207
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() {
|
||||
|
Loading…
Reference in New Issue
Block a user