// Package logging provides structured logging backed by log/slog, with UTC // RFC3339 timestamps and a level-gated API compatible with the previous // *log.Logger-based implementation. package logging import ( "fmt" "io" "log/slog" "os" "time" ) // Logger provides structured logging backed by log/slog, with UTC RFC3339 // timestamps and a level-gated API compatible with the previous // *log.Logger-based implementation. type Logger struct { slogger *slog.Logger level LogLevel } // LogLevel represents the logging level type LogLevel int // Logging levels, in increasing order of severity. DebugLevel is the most // verbose and ErrorLevel the least. const ( DebugLevel LogLevel = iota InfoLevel WarnLevel ErrorLevel ) // toSlogLevel maps our LogLevel to the equivalent slog.Level. func (l LogLevel) toSlogLevel() slog.Level { switch l { case DebugLevel: return slog.LevelDebug case InfoLevel: return slog.LevelInfo case WarnLevel: return slog.LevelWarn case ErrorLevel: return slog.LevelError default: return slog.LevelInfo } } // replaceAttr normalizes the slog time attribute to a UTC RFC3339 timestamp // so log output has a stable, predictable format regardless of handler. func replaceAttr(_ []string, a slog.Attr) slog.Attr { if a.Key == slog.TimeKey { if t, ok := a.Value.Any().(time.Time); ok { a.Value = slog.StringValue(t.UTC().Format(time.RFC3339)) } } return a } // newHandler builds a slog.Handler for the given format ("json" or "text", // with "json" as the default), level, and output writer. func newHandler(format string, level LogLevel, output io.Writer) slog.Handler { opts := &slog.HandlerOptions{ Level: level.toSlogLevel(), ReplaceAttr: replaceAttr, } switch format { case "text": return slog.NewTextHandler(output, opts) default: return slog.NewJSONHandler(output, opts) } } // New creates a new logger using the text format with the given level and // output writer. func New(level LogLevel, output io.Writer) *Logger { if output == nil { output = os.Stdout } return &Logger{ slogger: slog.New(newHandler("text", level, output)), level: level, } } // NewFromConfig creates a logger from configuration using the json format, // matching the documented default for logging.format. func NewFromConfig(levelStr string, outputPath string) (*Logger, error) { return NewFromOptions(levelStr, "json", outputPath) } // NewFromOptions creates a logger from configuration with an explicit // format ("json", "text", or "" which defaults to "json"). outputPath may be // "stdout", "stderr", "" (defaults to stdout), or a file path, which is // opened for append (creating it if necessary). func NewFromOptions(levelStr string, format string, outputPath string) (*Logger, error) { level := parseLevel(levelStr) var output io.Writer switch outputPath { case "stdout", "": output = os.Stdout case "stderr": output = os.Stderr default: file, err := os.OpenFile(outputPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) //nolint:gosec // outputPath is operator-configured (logging.output), not user-controlled input if err != nil { return nil, err } output = file } return &Logger{ slogger: slog.New(newHandler(format, level, output)), level: level, }, nil } // Slog exposes the underlying *slog.Logger for structured call sites. func (l *Logger) Slog() *slog.Logger { return l.slogger } // Debug logs a debug message func (l *Logger) Debug(msg string) { l.slogger.Debug(msg) } // Debugf logs a formatted debug message func (l *Logger) Debugf(format string, args ...interface{}) { l.slogger.Debug(fmt.Sprintf(format, args...)) } // Info logs an info message func (l *Logger) Info(msg string) { l.slogger.Info(msg) } // Infof logs a formatted info message func (l *Logger) Infof(format string, args ...interface{}) { l.slogger.Info(fmt.Sprintf(format, args...)) } // Warn logs a warning message func (l *Logger) Warn(msg string) { l.slogger.Warn(msg) } // Warnf logs a formatted warning message func (l *Logger) Warnf(format string, args ...interface{}) { l.slogger.Warn(fmt.Sprintf(format, args...)) } // Error logs an error message func (l *Logger) Error(msg string) { l.slogger.Error(msg) } // Errorf logs a formatted error message func (l *Logger) Errorf(format string, args ...interface{}) { l.slogger.Error(fmt.Sprintf(format, args...)) } // Fatal logs a fatal message at error level and exits func (l *Logger) Fatal(msg string) { l.slogger.Error(msg) os.Exit(1) } // Fatalf logs a formatted fatal message at error level and exits func (l *Logger) Fatalf(format string, args ...interface{}) { l.slogger.Error(fmt.Sprintf(format, args...)) os.Exit(1) } // parseLevel parses a log level string func parseLevel(levelStr string) LogLevel { switch levelStr { case "debug": return DebugLevel case "info": return InfoLevel case "warn", "warning": return WarnLevel case "error": return ErrorLevel default: return InfoLevel } }