sp4-v2-1770499323/pkg/logging/logger.go
jordan be7936490f
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/manual/woodpecker Pipeline was successful
Initialize project from skeleton template
2026-02-07 21:22:04 +00:00

246 lines
5.3 KiB
Go

// Package logging provides slog-based structured logging with context integration.
//
// This package standardizes logging across all services with:
// - Environment-aware formatting (JSON for production, text for development)
// - Request-scoped loggers with context propagation
// - Convenience methods for common logging patterns
//
// Usage:
//
// // Create a logger based on environment
// logger := logging.New(logging.Config{
// Level: logging.LevelInfo,
// Format: logging.FormatJSON,
// Environment: "production",
// })
//
// // Or use convenience constructors
// logger := logging.NewDevelopment() // text format, debug level
// logger := logging.NewProduction() // JSON format, info level
package logging
import (
"io"
"log/slog"
"os"
"strings"
)
// Level represents the logging level.
type Level int
const (
LevelDebug Level = iota
LevelInfo
LevelWarn
LevelError
)
// String returns the string representation of the level.
func (l Level) String() string {
switch l {
case LevelDebug:
return "debug"
case LevelInfo:
return "info"
case LevelWarn:
return "warn"
case LevelError:
return "error"
default:
return "info"
}
}
// ParseLevel parses a string into a Level.
// Returns LevelInfo if the string is not recognized.
func ParseLevel(s string) Level {
switch strings.ToLower(strings.TrimSpace(s)) {
case "debug":
return LevelDebug
case "info":
return LevelInfo
case "warn", "warning":
return LevelWarn
case "error":
return LevelError
default:
return LevelInfo
}
}
func (l Level) toSlog() slog.Level {
switch l {
case LevelDebug:
return slog.LevelDebug
case LevelInfo:
return slog.LevelInfo
case LevelWarn:
return slog.LevelWarn
case LevelError:
return slog.LevelError
default:
return slog.LevelInfo
}
}
// Format represents the output format.
type Format int
const (
FormatJSON Format = iota
FormatText
)
// String returns the string representation of the format.
func (f Format) String() string {
switch f {
case FormatJSON:
return "json"
case FormatText:
return "text"
default:
return "json"
}
}
// ParseFormat parses a string into a Format.
// Returns FormatJSON if the string is not recognized.
func ParseFormat(s string) Format {
switch strings.ToLower(strings.TrimSpace(s)) {
case "text", "console":
return FormatText
case "json":
return FormatJSON
default:
return FormatJSON
}
}
// Config holds the logger configuration.
type Config struct {
// Level sets the minimum log level.
// Default: LevelInfo
Level Level
// Format sets the output format.
// Default: FormatJSON
Format Format
// Output sets the output writer.
// Default: os.Stdout
Output io.Writer
// AddSource adds source file and line number to log entries.
// Default: false
AddSource bool
// Environment determines default format if not specified.
// "development" uses text format, others use JSON.
Environment string
}
// Logger wraps slog.Logger with additional convenience methods.
type Logger struct {
*slog.Logger
}
// New creates a new Logger with the given configuration.
func New(cfg Config) *Logger {
if cfg.Output == nil {
cfg.Output = os.Stdout
}
// Auto-detect format based on environment if not explicitly set
format := cfg.Format
if cfg.Environment == "development" && format == FormatJSON {
format = FormatText
}
opts := &slog.HandlerOptions{
Level: cfg.Level.toSlog(),
AddSource: cfg.AddSource,
}
var handler slog.Handler
switch format {
case FormatText:
handler = slog.NewTextHandler(cfg.Output, opts)
default:
handler = slog.NewJSONHandler(cfg.Output, opts)
}
return &Logger{
Logger: slog.New(handler),
}
}
// NewDevelopment creates a logger configured for development.
// Uses text format, debug level, and includes source location.
func NewDevelopment() *Logger {
return New(Config{
Level: LevelDebug,
Format: FormatText,
AddSource: true,
})
}
// NewProduction creates a logger configured for production.
// Uses JSON format and info level.
func NewProduction() *Logger {
return New(Config{
Level: LevelInfo,
Format: FormatJSON,
})
}
// With returns a new Logger with the given attributes.
func (l *Logger) With(args ...any) *Logger {
return &Logger{
Logger: l.Logger.With(args...),
}
}
// WithGroup returns a new Logger with the given group name.
func (l *Logger) WithGroup(name string) *Logger {
return &Logger{
Logger: l.Logger.WithGroup(name),
}
}
// WithError returns a new Logger with the error attribute.
func (l *Logger) WithError(err error) *Logger {
if err == nil {
return l
}
return l.With("error", err.Error())
}
// WithComponent returns a new Logger with the component attribute.
func (l *Logger) WithComponent(name string) *Logger {
return l.With("component", name)
}
// WithService returns a new Logger with the service attribute.
func (l *Logger) WithService(name string) *Logger {
return l.With("service", name)
}
// Nop returns a logger that discards all output.
func Nop() *Logger {
return New(Config{
Output: io.Discard,
Level: LevelError,
})
}
// Default returns the default logger configured for the current environment.
// Uses APP_ENVIRONMENT env var to determine format.
func Default() *Logger {
env := os.Getenv("APP_ENVIRONMENT")
if env == "development" || env == "" {
return NewDevelopment()
}
return NewProduction()
}