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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/oklog/ulid/v2"
|
"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 {
|
func setupDatabase() error {
|
||||||
_, err := db.Exec(`
|
_, err := db.Exec(`
|
||||||
CREATE TABLE IF NOT EXISTS articles (
|
CREATE TABLE IF NOT EXISTS articles (
|
||||||
@ -351,11 +547,14 @@ func setupLogging() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up structured logger
|
// Set up structured logger with custom colorized handler for console
|
||||||
jsonHandler := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
|
// and JSON handler for file logging
|
||||||
Level: slog.LevelInfo,
|
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() {
|
func flushLog() {
|
||||||
|
Loading…
Reference in New Issue
Block a user