Add Writer io.Writer field to Options; when nil, defaults to os.Stdout (no breaking change). Enables log capture in tests via the public API and supports writing to files, multi-writers, or any io.Writer implementation in production. All remaining roadmap items validated in production (xerrors enrichment concurrency, WithFields merge semantics, Logger interface finality). API committed as stable.
154 lines
4.2 KiB
Go
154 lines
4.2 KiB
Go
package logz
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
)
|
|
|
|
// errorWithCode is satisfied by errors that expose a machine-readable error code.
|
|
// xerrors.Err satisfies this interface via its ErrorCode() method.
|
|
type errorWithCode interface {
|
|
ErrorCode() string
|
|
}
|
|
|
|
// errorWithContext is satisfied by errors that expose structured key-value context fields.
|
|
// xerrors.Err satisfies this interface via its ErrorContext() method.
|
|
type errorWithContext interface {
|
|
ErrorContext() map[string]any
|
|
}
|
|
|
|
// Options configures a Logger instance.
|
|
// The zero value is valid: INFO level, text output, os.Stdout, no static args.
|
|
type Options struct {
|
|
// Level is the minimum log level. Default: slog.LevelInfo (zero value).
|
|
Level slog.Level
|
|
// JSON enables JSON output. Default: false (text output).
|
|
JSON bool
|
|
// StaticArgs are key-value pairs attached to every log record.
|
|
StaticArgs []any
|
|
// Writer is the output destination. Defaults to os.Stdout when nil.
|
|
// Accepts any io.Writer: *os.File, bytes.Buffer, io.MultiWriter, etc.
|
|
Writer io.Writer
|
|
}
|
|
|
|
// Logger is the interface for structured logging.
|
|
type Logger interface {
|
|
// Debug logs a message at DEBUG level.
|
|
Debug(msg string, args ...any)
|
|
// Info logs a message at INFO level.
|
|
Info(msg string, args ...any)
|
|
// Warn logs a message at WARN level.
|
|
Warn(msg string, args ...any)
|
|
// Error logs a message at ERROR level.
|
|
// If err satisfies errorWithCode or errorWithContext, the structured fields
|
|
// are automatically appended to the log record.
|
|
Error(msg string, err error, args ...any)
|
|
// With returns a new Logger with the given key-value attributes pre-attached.
|
|
With(args ...any) Logger
|
|
// WithContext returns a new Logger enriched with request_id and any extra
|
|
// fields stored in ctx via WithRequestID / WithField / WithFields.
|
|
WithContext(ctx context.Context) Logger
|
|
}
|
|
|
|
// slogLogger is the concrete implementation of Logger using slog.
|
|
type slogLogger struct {
|
|
logger *slog.Logger
|
|
}
|
|
|
|
// New returns a new Logger configured by opts.
|
|
func New(opts Options) Logger {
|
|
w := opts.Writer
|
|
if w == nil {
|
|
w = os.Stdout
|
|
}
|
|
|
|
handlerOpts := &slog.HandlerOptions{Level: opts.Level}
|
|
var handler slog.Handler
|
|
if opts.JSON {
|
|
handler = slog.NewJSONHandler(w, handlerOpts)
|
|
} else {
|
|
handler = slog.NewTextHandler(w, handlerOpts)
|
|
}
|
|
|
|
base := slog.New(handler)
|
|
if len(opts.StaticArgs) > 0 {
|
|
base = base.With(opts.StaticArgs...)
|
|
}
|
|
|
|
return &slogLogger{logger: base}
|
|
}
|
|
|
|
// Debug implements Logger.
|
|
func (l *slogLogger) Debug(msg string, args ...any) { l.logger.Debug(msg, args...) }
|
|
|
|
// Info implements Logger.
|
|
func (l *slogLogger) Info(msg string, args ...any) { l.logger.Info(msg, args...) }
|
|
|
|
// Warn implements Logger.
|
|
func (l *slogLogger) Warn(msg string, args ...any) { l.logger.Warn(msg, args...) }
|
|
|
|
// Error implements Logger.
|
|
func (l *slogLogger) Error(msg string, err error, args ...any) {
|
|
args = enrichErrorAttrs(err, args)
|
|
if err != nil {
|
|
args = append(args, slog.Any("error", err))
|
|
}
|
|
l.logger.Error(msg, args...)
|
|
}
|
|
|
|
// With implements Logger.
|
|
func (l *slogLogger) With(args ...any) Logger {
|
|
return &slogLogger{logger: l.logger.With(args...)}
|
|
}
|
|
|
|
// WithContext implements Logger.
|
|
func (l *slogLogger) WithContext(ctx context.Context) Logger {
|
|
if ctx == nil {
|
|
return l
|
|
}
|
|
|
|
newLogger := l.logger
|
|
modified := false
|
|
|
|
if id, ok := ctx.Value(ctxRequestIDKey{}).(string); ok && id != "" {
|
|
newLogger = newLogger.With(slog.String("request_id", id))
|
|
modified = true
|
|
}
|
|
|
|
if fields, ok := ctx.Value(ctxExtraFieldsKey{}).(map[string]any); ok {
|
|
for k, v := range fields {
|
|
newLogger = newLogger.With(k, v)
|
|
}
|
|
modified = true
|
|
}
|
|
|
|
if !modified {
|
|
return l
|
|
}
|
|
|
|
return &slogLogger{logger: newLogger}
|
|
}
|
|
|
|
// enrichErrorAttrs appends error_code and context fields from err to attrs
|
|
// when err satisfies the errorWithCode or errorWithContext duck-type interfaces.
|
|
// Returns attrs unchanged if err is nil or does not satisfy either interface.
|
|
func enrichErrorAttrs(err error, attrs []any) []any {
|
|
if err == nil {
|
|
return attrs
|
|
}
|
|
var ec errorWithCode
|
|
if errors.As(err, &ec) {
|
|
attrs = append(attrs, "error_code", ec.ErrorCode())
|
|
}
|
|
var ectx errorWithContext
|
|
if errors.As(err, &ectx) {
|
|
for k, v := range ectx.ErrorContext() {
|
|
attrs = append(attrs, k, v)
|
|
}
|
|
}
|
|
return attrs
|
|
}
|