// Package logger wraps go.uber.org/zap with a thin, ergonomic API. // // The key addition over raw zap is the WithField / WithFields helpers that // return a *Logger (not a *zap.Logger), so callers stay in the typed world and // don't need to import zap just to attach context fields. // // Usage: // // log, _ := logger.New("info") // log.Info("server started") // // req := log.WithField("request_id", rid).WithField("user_id", uid) // req.Info("handling request") package logger import ( "fmt" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) // Logger is a thin wrapper around *zap.Logger. // All zap methods (Info, Error, Debug, …) are available via embedding. type Logger struct { *zap.Logger } // New creates a production-style JSON logger for the given level string. // Valid levels: debug, info, warn, error. func New(level string) (*Logger, error) { lvl, err := parseLevel(level) if err != nil { return nil, err } cfg := zap.NewProductionConfig() cfg.Level = zap.NewAtomicLevelAt(lvl) // ISO8601 timestamps are human-readable and grep-friendly. cfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder z, err := cfg.Build(zap.AddCallerSkip(0)) if err != nil { return nil, fmt.Errorf("logger: build: %w", err) } return &Logger{z}, nil } // NewDevelopment creates a colourised, human-friendly console logger. // Use this in local dev; prefer New() in any deployed environment. func NewDevelopment() (*Logger, error) { z, err := zap.NewDevelopment() if err != nil { return nil, fmt.Errorf("logger: build dev: %w", err) } return &Logger{z}, nil } // NewNop returns a no-op logger. Useful in tests that don't care about logs. func NewNop() *Logger { return &Logger{zap.NewNop()} } // WithField returns a child logger that always includes key=value in every log // line. value can be any type; zap.Any is used internally. func (l *Logger) WithField(key string, value any) *Logger { return &Logger{l.Logger.With(zap.Any(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 { zapFields := make([]zap.Field, 0, len(fields)) for k, v := range fields { zapFields = append(zapFields, zap.Any(k, v)) } return &Logger{l.Logger.With(zapFields...)} } // Sync flushes any buffered log entries. Call this on shutdown: // // defer log.Sync() func (l *Logger) Sync() { // Intentionally ignore the error — os.Stderr sync often fails on some OSes. _ = l.Logger.Sync() } // ── helpers ─────────────────────────────────────────────────────────────────── func parseLevel(level string) (zapcore.Level, error) { var lvl zapcore.Level if err := lvl.UnmarshalText([]byte(level)); err != nil { return lvl, fmt.Errorf("logger: unknown level %q (use debug|info|warn|error)", level) } return lvl, nil }