Skip to content

Commit

Permalink
feat: v0.3.0 (#1)
Browse files Browse the repository at this point in the history
* refactor: simplify logger interface

* chore: readd context methods

* docs: adjust logger interface godoc

* chore: ignore revive issues in tests

* chore: test case naming
  • Loading branch information
lvlcn-t authored Mar 2, 2024
1 parent b4c7dde commit 337bbdb
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 136 deletions.
1 change: 1 addition & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
152 changes: 128 additions & 24 deletions internal/logger/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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]
}
80 changes: 12 additions & 68 deletions internal/logger/extensions.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,124 +3,68 @@ 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)
}

// Panicf logs at LevelPanic and then panics.
// 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]
}
Loading

0 comments on commit 337bbdb

Please sign in to comment.