// Package logger wraps log/slog with a thin, ergonomic API. // // The key addition over raw slog is the WithField / WithFields helpers that // return a *Logger (not a *slog.Logger), so callers stay in the typed world // and can chain field attachments without importing slog directly. // // Usage: // // log := logger.New("info").Expect("create logger") // log.Info("server started") // // req := log.WithField("request_id", rid).WithField("user_id", uid) // req.Info("handling request") package logger import ( "io" "log/slog" "os" "gitea.djmil.dev/go/template/pkg/result" ) // Logger is a thin wrapper around *slog.Logger. // All slog methods (Info, Error, Debug, Warn, …) are available via embedding. type Logger struct { *slog.Logger } // New creates a JSON logger writing to stderr for the given level string. // Valid levels: debug, info, warn, error. func New(level string) result.Expect[*Logger] { lvl := parseLevel(level) if lvl.Err() != nil { return result.Errw[*Logger](lvl.Err(), "parse log level") } handler := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: lvl.Value()}) logger := &Logger{slog.New(handler)} return result.Ok(logger) } // NewDevelopment creates a human-friendly text logger writing to stderr. // Use this in local dev; prefer New() in any deployed environment. func NewDevelopment() *Logger { h := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}) return &Logger{slog.New(h)} } // NewNop returns a no-op logger. Useful in tests that don't care about logs. func NewNop() *Logger { return &Logger{slog.New(slog.NewTextHandler(io.Discard, nil))} } // WithField returns a child logger that always includes key=value in every log line. func (l *Logger) WithField(key string, value any) *Logger { return &Logger{l.Logger.With(key, value)} } // WithFields returns a child logger enriched with every key/value in fields. // Prefer WithField for one or two fields; use WithFields for structured context // objects (e.g. attaching a request span). func (l *Logger) WithFields(fields map[string]any) *Logger { args := make([]any, 0, len(fields)*2) for k, v := range fields { args = append(args, k, v) } return &Logger{l.Logger.With(args...)} } // ── helpers ─────────────────────────────────────────────────────────────────── func parseLevel(level string) result.Expect[slog.Level] { var lvl slog.Level if err := lvl.UnmarshalText([]byte(level)); err != nil { return result.Errw[slog.Level](err, "unknown level (use debug|info|warn|error)") } return result.Ok(lvl) }