- Replace zerolog with Go's standard slog package - Add ColorHandler for terminal color output - Add context-aware logging system - Format attributes on the same line as log messages - Use green color for INFO level logs - Set up context value extraction helpers
189 lines
4.7 KiB
Go
189 lines
4.7 KiB
Go
// Package logging provides a simple, structured logging interface using slog.
|
|
// It offers colored output for better readability in terminal environments.
|
|
package logging
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// Global logger instance.
|
|
var slogLogger *slog.Logger
|
|
|
|
// Current log level - used to filter logs.
|
|
var currentLogLevel = slog.LevelInfo
|
|
|
|
// ANSI color codes for terminal output.
|
|
const (
|
|
colorReset = "\033[0m"
|
|
levelDebug = "\033[37m" // Gray
|
|
levelInfo = "\033[32m" // Green
|
|
levelWarn = "\033[33m" // Yellow
|
|
levelError = "\033[31m" // Red
|
|
)
|
|
|
|
// Standard helper functions for logging
|
|
func LogDebug(format string, args ...interface{}) {
|
|
if slogLogger != nil {
|
|
slogLogger.Debug(fmt.Sprintf(format, args...))
|
|
}
|
|
}
|
|
|
|
func LogInfo(format string, args ...interface{}) {
|
|
if slogLogger != nil {
|
|
slogLogger.Info(fmt.Sprintf(format, args...))
|
|
}
|
|
}
|
|
|
|
func LogWarn(format string, args ...interface{}) {
|
|
if slogLogger != nil {
|
|
slogLogger.Warn(fmt.Sprintf(format, args...))
|
|
}
|
|
}
|
|
|
|
func LogError(format string, args ...interface{}) {
|
|
if slogLogger != nil {
|
|
msg := fmt.Sprintf(format, args...)
|
|
slogLogger.Error(msg, slog.String("error", msg))
|
|
}
|
|
}
|
|
|
|
// InitSlogger initializes the slog logger with custom handler.
|
|
func InitSlogger(level slog.Level) {
|
|
// Set the global log level
|
|
currentLogLevel = level
|
|
|
|
// Create the handler with color support
|
|
baseHandler := NewColorHandler(os.Stderr)
|
|
|
|
// Create and set the new logger
|
|
slogLogger = slog.New(baseHandler)
|
|
|
|
// Set as default logger
|
|
slog.SetDefault(slogLogger)
|
|
|
|
// Print a startup message to verify logging is working
|
|
slogLogger.Info("Slog initialized", "level", level.String())
|
|
}
|
|
|
|
// GetSlogger returns the current global slog logger instance.
|
|
// Can be used by other packages
|
|
func GetSlogger() *slog.Logger {
|
|
if slogLogger == nil {
|
|
return slog.Default()
|
|
}
|
|
return slogLogger
|
|
}
|
|
|
|
// ColorHandler formats logs with colors for better terminal readability
|
|
type ColorHandler struct {
|
|
out io.Writer
|
|
mu *sync.Mutex
|
|
attrs []slog.Attr // Store attributes for this handler
|
|
}
|
|
|
|
// NewColorHandler creates a new handler that writes colored logs to the provided writer
|
|
func NewColorHandler(w io.Writer) *ColorHandler {
|
|
if w == nil {
|
|
w = os.Stderr
|
|
}
|
|
return &ColorHandler{
|
|
out: w,
|
|
mu: &sync.Mutex{},
|
|
attrs: make([]slog.Attr, 0),
|
|
}
|
|
}
|
|
|
|
// Enabled checks if the given log level is enabled
|
|
func (h *ColorHandler) Enabled(_ context.Context, level slog.Level) bool {
|
|
return level >= currentLogLevel
|
|
}
|
|
|
|
// Handle processes a log record, formatting it with colors
|
|
func (h *ColorHandler) Handle(ctx context.Context, r slog.Record) error {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
// Format time
|
|
timeStr := fmt.Sprintf("[%s]", r.Time.Format("2006-01-02 15:04:05"))
|
|
|
|
// Format level
|
|
var levelStr string
|
|
switch r.Level {
|
|
case slog.LevelDebug:
|
|
levelStr = fmt.Sprintf("%sDEBUG%s", levelDebug, colorReset)
|
|
case slog.LevelInfo:
|
|
levelStr = fmt.Sprintf("%sINFO%s", levelInfo, colorReset)
|
|
case slog.LevelWarn:
|
|
levelStr = fmt.Sprintf("%sWARN%s", levelWarn, colorReset)
|
|
case slog.LevelError:
|
|
levelStr = fmt.Sprintf("%sERROR%s", levelError, colorReset)
|
|
default:
|
|
levelStr = r.Level.String()
|
|
}
|
|
|
|
// Build prefix
|
|
prefix := fmt.Sprintf("%s %s ", timeStr, levelStr)
|
|
|
|
// Format message - we'll collect any special fields separately
|
|
attrMap := make(map[string]string)
|
|
|
|
// First collect attributes from the handler itself
|
|
for _, attr := range h.attrs {
|
|
attrMap[attr.Key] = attr.Value.String()
|
|
}
|
|
|
|
// Then extract from record attributes, which might override handler attributes
|
|
r.Attrs(func(a slog.Attr) bool {
|
|
attrMap[a.Key] = a.Value.String()
|
|
return true
|
|
})
|
|
|
|
// Format message with attributes on the same line
|
|
msg := fmt.Sprintf("%s%s", prefix, r.Message)
|
|
|
|
// Add attributes to the same line if present
|
|
if len(attrMap) > 0 {
|
|
// Add a space after the message
|
|
msg += " "
|
|
|
|
// Build attribute string
|
|
attrs := make([]string, 0, len(attrMap))
|
|
for k, v := range attrMap {
|
|
attrs = append(attrs, fmt.Sprintf("%s=%s", k, v))
|
|
}
|
|
|
|
// Join all attributes with spaces
|
|
msg += strings.Join(attrs, " ")
|
|
}
|
|
|
|
// Add newline at the end
|
|
msg += "\n"
|
|
|
|
// Write to output
|
|
_, err := io.WriteString(h.out, msg)
|
|
return err
|
|
}
|
|
|
|
// WithGroup returns a new Handler that inherits from this Handler
|
|
func (h *ColorHandler) WithGroup(name string) slog.Handler {
|
|
return h // For simplicity, we don't support groups
|
|
}
|
|
|
|
// WithAttrs returns a new Handler whose attributes include both
|
|
// the receiver's attributes and the arguments
|
|
func (h *ColorHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
|
// Create a new handler with the same output but additional attributes
|
|
newHandler := &ColorHandler{
|
|
out: h.out,
|
|
mu: h.mu,
|
|
attrs: append(append([]slog.Attr{}, h.attrs...), attrs...),
|
|
}
|
|
return newHandler
|
|
}
|