package logz import ( "context" "errors" "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, 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 } // 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 { handlerOpts := &slog.HandlerOptions{ Level: opts.Level, AddSource: false, } var handler slog.Handler if opts.JSON { handler = slog.NewJSONHandler(os.Stdout, handlerOpts) } else { handler = slog.NewTextHandler(os.Stdout, 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 }