// 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() }