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:
2025-05-22 12:44:08 +03:00
committed by antanst
parent b8ea6fab4a
commit 4ef3f70f1f
7 changed files with 393 additions and 322 deletions

View File

@@ -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
}