Implement structured logging with slog
- 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
This commit is contained in:
@@ -1,23 +1,188 @@
|
||||
// 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"
|
||||
|
||||
zlog "github.com/rs/zerolog/log"
|
||||
"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{}) {
|
||||
zlog.Debug().Msg(fmt.Sprintf(format, args...))
|
||||
if slogLogger != nil {
|
||||
slogLogger.Debug(fmt.Sprintf(format, args...))
|
||||
}
|
||||
}
|
||||
|
||||
func LogInfo(format string, args ...interface{}) {
|
||||
zlog.Info().Msg(fmt.Sprintf(format, args...))
|
||||
if slogLogger != nil {
|
||||
slogLogger.Info(fmt.Sprintf(format, args...))
|
||||
}
|
||||
}
|
||||
|
||||
func LogWarn(format string, args ...interface{}) {
|
||||
zlog.Warn().Msg(fmt.Sprintf(format, args...))
|
||||
if slogLogger != nil {
|
||||
slogLogger.Warn(fmt.Sprintf(format, args...))
|
||||
}
|
||||
}
|
||||
|
||||
func LogError(format string, args ...interface{}) {
|
||||
zlog.Error().Err(fmt.Errorf(format, args...)).Msg("")
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user