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