102 lines
3.0 KiB
Go
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
|
|
}
|