diff --git a/.golangci.yaml b/.golangci.yaml index a7929df..0166b7a 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -132,6 +132,7 @@ issues: - path: _test\.go linters: - gomnd + - revive - path: pkg/golinters/errcheck.go text: "SA1019: errCfg.Exclude is deprecated: use ExcludeFunctions instead" diff --git a/README.md b/README.md index 84485ac..f47d13a 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,6 @@ Custom handlers also enable integration with various logging backends and servic For further examples and detailed usage, including how to implement and integrate custom `slog.Handler` instances, please refer to the [examples](./examples) directory in our repository. - ## Contributing Contributions are welcome! Please refer to the [CONTRIBUTING](CONTRIBUTING.md) file for guidelines on how to contribute to this project. diff --git a/internal/logger/core.go b/internal/logger/core.go index 3bd36e5..be79f99 100644 --- a/internal/logger/core.go +++ b/internal/logger/core.go @@ -3,38 +3,67 @@ package logger import ( "context" "log/slog" + "runtime" + "time" ) -var _ Core = (*coreLogger)(nil) +var _ Logger = (*logger)(nil) -// Core is the wrapper around slog.Logger that provides the core logging API. -// It is a subset of the Logger interface. -type Core interface { +// Logger is a interface that provides logging methods. +type Logger interface { // Debug logs at LevelDebug. Debug(msg string, args ...any) + // Debugf logs at LevelDebug. + // Arguments are handled in the manner of fmt.Printf. + Debugf(msg string, args ...any) // DebugContext logs at LevelDebug with the given context. DebugContext(ctx context.Context, msg string, args ...any) // Info logs at LevelInfo. Info(msg string, args ...any) + // Infof logs at LevelInfo. + // Arguments are handled in the manner of fmt.Printf. + Infof(msg string, args ...any) // InfoContext logs at LevelInfo with the given context. InfoContext(ctx context.Context, msg string, args ...any) // Warn logs at LevelWarn. Warn(msg string, args ...any) + // Warnf logs at LevelWarn. + // Arguments are handled in the manner of fmt.Printf. + Warnf(msg string, args ...any) // WarnContext logs at LevelWarn with the given context. WarnContext(ctx context.Context, msg string, args ...any) // Error logs at LevelError. Error(msg string, args ...any) + // Errorf logs at LevelError. + // Arguments are handled in the manner of fmt.Printf. + Errorf(msg string, args ...any) // ErrorContext logs at LevelError with the given context. ErrorContext(ctx context.Context, msg string, args ...any) + // Panic logs at LevelPanic and then panics with the given message. + Panic(msg string, args ...any) + // Panicf logs at LevelPanic and then panics. + // Arguments are handled in the manner of fmt.Printf. + Panicf(msg string, args ...any) + // PanicContext logs at LevelPanic with the given context and then panics with the given message. + PanicContext(ctx context.Context, msg string, args ...any) + // Fatal logs at LevelFatal and then calls os.Exit(1). + Fatal(msg string, args ...any) + // Fatalf logs at LevelFatal and then calls os.Exit(1). + // Arguments are handled in the manner of fmt.Printf. + Fatalf(msg string, args ...any) + // FatalContext logs at LevelFatal with the given context and then calls os.Exit(1). + FatalContext(ctx context.Context, msg string, args ...any) + // With calls Logger.With on the default logger. - With(args ...any) *slog.Logger + With(args ...any) Logger // WithGroup returns a Logger that starts a group, if name is non-empty. // The keys of all attributes added to the Logger will be qualified by the given // name. (How that qualification happens depends on the [Handler.WithGroup] // method of the Logger's Handler.) // // If name is empty, WithGroup returns the receiver. - WithGroup(name string) *slog.Logger + WithGroup(name string) Logger + // Log emits a log record with the current time and the given level and message. // The Record's Attrs consist of the Logger's attributes followed by // the Attrs specified by args. @@ -48,35 +77,110 @@ type Core interface { Log(ctx context.Context, level Level, msg string, args ...any) // LogAttrs is a more efficient version of [Logger.Log] that accepts only Attrs. LogAttrs(ctx context.Context, level Level, msg string, attrs ...slog.Attr) + // Handler returns l's Handler. Handler() slog.Handler // Enabled reports whether l emits log records at the given context and level. Enabled(ctx context.Context, level Level) bool + + // ToSlog returns the underlying slog.Logger. + ToSlog() *slog.Logger } -// coreLogger is a wrapper around slog.Logger that implements the Core interface. -type coreLogger struct { - // slog.Logger is the underlying logger. - *slog.Logger +// logger implements the Logger interface. +// It is a wrapper around slog.Logger. +type logger struct{ *slog.Logger } + +// Debug logs at LevelDebug. +func (l *logger) Debug(msg string, a ...any) { + l.logAttrs(context.Background(), LevelDebug, msg, a...) } -// newCoreLogger returns a new Core that wraps the given slog.Handler. -func newCoreLogger(h slog.Handler) *coreLogger { - return &coreLogger{ - slog.New(h), - } +// DebugContext logs at LevelDebug. +func (l *logger) DebugContext(ctx context.Context, msg string, a ...any) { + l.logAttrs(ctx, LevelDebug, msg, a...) } -// With returns a new Core that wraps the given slog.Logger with the given attributes. -func With(l Core, args ...any) *coreLogger { - return &coreLogger{ - l.With(args...), - } +// Info logs at LevelInfo. +func (l *logger) Info(msg string, a ...any) { + l.logAttrs(context.Background(), LevelInfo, msg, a...) +} + +// InfoContext logs at LevelInfo. +func (l *logger) InfoContext(ctx context.Context, msg string, a ...any) { + l.logAttrs(ctx, LevelInfo, msg, a...) +} + +// Warn logs at LevelWarn. +func (l *logger) Warn(msg string, a ...any) { + l.logAttrs(context.Background(), LevelWarn, msg, a...) +} + +// WarnContext logs at LevelWarn. +func (l *logger) WarnContext(ctx context.Context, msg string, a ...any) { + l.logAttrs(ctx, LevelWarn, msg, a...) +} + +// Error logs at LevelError. +func (l *logger) Error(msg string, a ...any) { + l.logAttrs(context.Background(), LevelError, msg, a...) +} + +// ErrorContext logs at LevelError. +func (l *logger) ErrorContext(ctx context.Context, msg string, a ...any) { + l.logAttrs(ctx, LevelError, msg, a...) +} + +// With calls Logger.With on the default logger. +func (l *logger) With(a ...any) Logger { + return &logger{Logger: l.Logger.With(a...)} +} + +// WithGroup returns a Logger that starts a group, if name is non-empty. +func (l *logger) WithGroup(name string) Logger { + return &logger{Logger: l.Logger.WithGroup(name)} +} + +// Log emits a log record with the current time and the given level and message. +func (l *logger) Log(ctx context.Context, level Level, msg string, a ...any) { + l.Logger.Log(ctx, level, msg, a...) +} + +// Logf emits a log record with the current time and the given level, message, and attributes. +func (l *logger) LogAttrs(ctx context.Context, level Level, msg string, attrs ...slog.Attr) { + l.Logger.LogAttrs(ctx, level, msg, attrs...) } -// WithGroup returns a new Core that wraps the given slog.Logger with the given group name. -func WithGroup(l Core, name string) *coreLogger { - return &coreLogger{ - l.WithGroup(name), +// logAttrs emits a log record with the current time and the given level, message, and attributes. +func (l *logger) logAttrs(ctx context.Context, level Level, msg string, a ...any) { + if !l.Enabled(ctx, level) { + return + } + + pc := getCaller(3) + + r := slog.NewRecord(time.Now(), level, msg, pc) + r.Add(a...) + if ctx == nil { + ctx = context.Background() } + + _ = l.Handler().Handle(ctx, r) +} + +// getCaller returns the program counter of the caller at a given depth. +// The depth is the number of stack frames to ascend, with 0 identifying the +// getCaller function itself, 1 identifying the caller that invoked getCaller, +// and so on. +// +// Example: +// +// pc := getCaller(1) // Returns the program counter of the caller of the function that invoked getCaller. +// pc := getCaller(2) // Returns the program counter of the caller of the function that invoked the function that invoked getCaller. +func getCaller(depth uint8) uintptr { + d := int(depth) + 1 + + var pcs [1]uintptr + runtime.Callers(d, pcs[:]) + return pcs[0] } diff --git a/internal/logger/extensions.go b/internal/logger/extensions.go index de1f14d..ddf515d 100644 --- a/internal/logger/extensions.go +++ b/internal/logger/extensions.go @@ -3,62 +3,36 @@ package logger import ( "context" "fmt" - "log/slog" "os" - "runtime" - "time" ) // Debugf logs at LevelDebug. // Arguments are handled in the manner of fmt.Printf. func (l *logger) Debugf(msg string, args ...any) { - if !l.Enabled(context.Background(), LevelDebug) { - return - } - pc := getCaller(2) - r := slog.NewRecord(time.Now(), LevelDebug, fmt.Sprintf(msg, args...), pc) - _ = l.Handler().Handle(context.Background(), r) + l.logAttrs(context.Background(), LevelDebug, fmt.Sprintf(msg, args...)) } // Infof logs at LevelInfo. // Arguments are handled in the manner of fmt.Printf. func (l *logger) Infof(msg string, args ...any) { - if !l.Enabled(context.Background(), LevelInfo) { - return - } - pc := getCaller(2) - r := slog.NewRecord(time.Now(), LevelInfo, fmt.Sprintf(msg, args...), pc) - _ = l.Handler().Handle(context.Background(), r) + l.logAttrs(context.Background(), LevelInfo, fmt.Sprintf(msg, args...)) } // Warnf logs at LevelWarn. // Arguments are handled in the manner of fmt.Printf. func (l *logger) Warnf(msg string, args ...any) { - if !l.Enabled(context.Background(), LevelWarn) { - return - } - pc := getCaller(2) - r := slog.NewRecord(time.Now(), LevelWarn, fmt.Sprintf(msg, args...), pc) - _ = l.Handler().Handle(context.Background(), r) + l.logAttrs(context.Background(), LevelWarn, fmt.Sprintf(msg, args...)) } // Errorf logs at LevelError. // Arguments are handled in the manner of fmt.Printf. func (l *logger) Errorf(msg string, args ...any) { - if !l.Enabled(context.Background(), LevelError) { - return - } - pc := getCaller(2) - r := slog.NewRecord(time.Now(), LevelError, fmt.Sprintf(msg, args...), pc) - _ = l.Handler().Handle(context.Background(), r) + l.logAttrs(context.Background(), LevelError, fmt.Sprintf(msg, args...)) } // Panic logs at [LevelPanic] and then panics. func (l *logger) Panic(msg string, args ...any) { - pc := getCaller(2) - r := slog.NewRecord(time.Now(), LevelPanic, msg, pc) - r.Add(args...) - _ = l.Handler().Handle(context.Background(), r) + l.logAttrs(context.Background(), LevelPanic, msg, args...) panic(msg) } @@ -66,61 +40,31 @@ func (l *logger) Panic(msg string, args ...any) { // Arguments are handled in the manner of fmt.Printf. func (l *logger) Panicf(msg string, args ...any) { fmsg := fmt.Sprintf(msg, args...) - pc := getCaller(2) - r := slog.NewRecord(time.Now(), LevelPanic, fmsg, pc) - _ = l.Handler().Handle(context.Background(), r) + l.logAttrs(context.Background(), LevelPanic, fmsg) panic(fmsg) } -// PanicContext logs at [LevelPanic] with the given context and then panics. +// PanicContext logs at [LevelPanic] and then panics. func (l *logger) PanicContext(ctx context.Context, msg string, args ...any) { - pc := getCaller(2) - r := slog.NewRecord(time.Now(), LevelPanic, msg, pc) - r.Add(args...) - _ = l.Handler().Handle(ctx, r) + l.logAttrs(ctx, LevelPanic, msg, args...) panic(msg) } // Fatal logs at [LevelFatal] and then calls os.Exit(1). func (l *logger) Fatal(msg string, args ...any) { - pc := getCaller(2) - r := slog.NewRecord(time.Now(), LevelFatal, msg, pc) - r.Add(args...) - _ = l.Handler().Handle(context.Background(), r) + l.logAttrs(context.Background(), LevelFatal, msg, args...) os.Exit(1) } // Fatalf logs at LevelFatal and then calls os.Exit(1). // Arguments are handled in the manner of fmt.Printf. func (l *logger) Fatalf(msg string, args ...any) { - pc := getCaller(2) - r := slog.NewRecord(time.Now(), LevelPanic, fmt.Sprintf(msg, args...), pc) - _ = l.Handler().Handle(context.Background(), r) + l.logAttrs(context.Background(), LevelFatal, fmt.Sprintf(msg, args...)) os.Exit(1) } -// FatalContext logs at [LevelFatal] with the given context and then calls os.Exit(1). +// FatalContext logs at [LevelFatal] and then calls os.Exit(1). func (l *logger) FatalContext(ctx context.Context, msg string, args ...any) { - pc := getCaller(2) - r := slog.NewRecord(time.Now(), LevelFatal, msg, pc) - r.Add(args...) - _ = l.Handler().Handle(ctx, r) + l.logAttrs(ctx, LevelFatal, msg, args...) os.Exit(1) } - -// getCaller returns the program counter of the caller at a given depth. -// The depth is the number of stack frames to ascend, with 0 identifying the -// getCaller function itself, 1 identifying the caller that invoked getCaller, -// and so on. -// -// Example: -// -// pc := getCaller(1) // Returns the program counter of the caller of the function that invoked getCaller. -// pc := getCaller(2) // Returns the program counter of the caller of the function that invoked the function that invoked getCaller. -func getCaller(depth uint8) uintptr { //nolint: unparam - d := int(depth) + 1 - - var pcs [1]uintptr - runtime.Callers(d, pcs[:]) - return pcs[0] -} diff --git a/internal/logger/utils.go b/internal/logger/utils.go index 3dacb8e..a6b38d3 100644 --- a/internal/logger/utils.go +++ b/internal/logger/utils.go @@ -11,47 +11,6 @@ import ( clog "github.com/charmbracelet/log" ) -var _ Logger = (*logger)(nil) - -// Logger is the interface for the logger. -// It extends the Core interface with additional logging methods. -type Logger interface { - // Core is the Core interface, which is the wrapper around slog.Logger that provides the core logging API. - Core - // Debugf logs at LevelDebug. - // Arguments are handled in the manner of fmt.Printf. - Debugf(msg string, args ...any) - // Infof logs at LevelInfo. - // Arguments are handled in the manner of fmt.Printf. - Infof(msg string, args ...any) - // Warnf logs at LevelWarn. - // Arguments are handled in the manner of fmt.Printf. - Warnf(msg string, args ...any) - // Errorf logs at LevelError. - // Arguments are handled in the manner of fmt.Printf. - Errorf(msg string, args ...any) - // Panic logs at LevelPanic and then panics with the given message. - Panic(msg string, args ...any) - // Panicf logs at LevelPanic and then panics. - // Arguments are handled in the manner of fmt.Printf. - Panicf(msg string, args ...any) - // PanicContext logs at LevelPanic with the given context and then panics with the given message. - PanicContext(ctx context.Context, msg string, args ...any) - // Fatal logs at LevelFatal and then calls os.Exit(1). - Fatal(msg string, args ...any) - // Fatalf logs at LevelFatal and then calls os.Exit(1). - // Arguments are handled in the manner of fmt.Printf. - Fatalf(msg string, args ...any) - // FatalContext logs at LevelFatal with the given context and then calls os.Exit(1). - FatalContext(ctx context.Context, msg string, args ...any) -} - -// logger implements the Logger interface. -type logger struct { - // coreLogger is the underlying slog.Logger. - *coreLogger -} - // NewLogger creates a new Logger instance. // If handlers are provided, the first handler in the slice is used; otherwise, // a default JSON handler writing to os.Stderr is used. This function allows for @@ -63,7 +22,7 @@ type logger struct { // log.Info("Hello, world!") func NewLogger(h ...slog.Handler) Logger { return &logger{ - coreLogger: newCoreLogger(getHandler(h...)), + Logger: slog.New(getHandler(h...)), } } @@ -73,7 +32,7 @@ func NewLogger(h ...slog.Handler) Logger { // custom configuration of logging handlers. func NewNamedLogger(name string, h ...slog.Handler) Logger { return &logger{ - coreLogger: With(newCoreLogger(getHandler(h...)), "name", name), + Logger: slog.New(getHandler(h...)).With("name", name), } } @@ -115,6 +74,16 @@ func Middleware(ctx context.Context) func(http.Handler) http.Handler { } } +// ToSlog returns the underlying slog.Logger. +func (l *logger) ToSlog() *slog.Logger { + return l.Logger +} + +// FromSlog returns a new Logger instance based on the provided slog.Logger. +func FromSlog(l *slog.Logger) Logger { + return &logger{l} +} + // getHandler returns the first handler in the slice if it exists; otherwise, it returns a new base handler. func getHandler(h ...slog.Handler) slog.Handler { if len(h) > 0 { diff --git a/logger/logger.go b/logger/logger.go index 3ad4ce8..d15cf7c 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -58,3 +58,8 @@ func FromContext(ctx context.Context) logger.Logger { func Middleware(ctx context.Context) func(http.Handler) http.Handler { return logger.Middleware(ctx) } + +// FromSlog returns a new Logger instance from the provided slog.Logger. +func FromSlog(l *slog.Logger) logger.Logger { + return logger.FromSlog(l) +}