go-template/internal/logger/logger.go
2026-03-05 21:52:10 +01:00

102 lines
3.0 KiB
Go

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