diff --git a/Makefile b/Makefile index ae94934..1eddf37 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ test: @go test -race -shuffle=on -coverprofile=coverage.out ./... test/bench: test - @go test -bench ./... + @go test -bench=. ./... test/cover: test @go tool cover -html=coverage.out diff --git a/slogattr/attr.go b/slogattr/attr.go index 35d1f97..77dadaf 100644 --- a/slogattr/attr.go +++ b/slogattr/attr.go @@ -1,4 +1,4 @@ -// Package slogattr implements utilities for creating [slog.Attr]s. +// Package slogattr implements utilities for creating [slog.Attr]. package slogattr import ( @@ -9,16 +9,22 @@ import ( ) // ErrorKey is the key used by the [Error] function. -// The associated value is an [error]. +// The associated value is an error. const ErrorKey = "error" -// Error returns a [slog.Attr] for an [error] value. +// Error returns a [slog.Attr] for an error value. func Error(err error) slog.Attr { + if err == nil { + return slog.Attr{} + } return slog.Any(ErrorKey, err) } // Slice returns a [slog.Attr] for a slice of [cmp.Ordered] values. func Slice[T cmp.Ordered](key string, ts []T) slog.Attr { + if len(ts) == 0 { + return slog.Attr{} + } return slog.Any(key, ts) } @@ -33,11 +39,14 @@ func LogValuer(key string, v slog.LogValuer) slog.Attr { } // LogValuers returns a [slog.Attr] for a slice of [slog.LogValuer] values. -// Given to [slog.JSONHandler], it will be written as {"key":{"1":...,"2":...}}, +// Given to [slog.JSONHandler], it will be written as +// +// {"key":{"1":...,"2":...}} +// // where ... is the [slog.LogValuer] itself. func LogValuers[T slog.LogValuer](key string, ts []T) slog.Attr { - // A [slog.LogValuer] in a slice is not supported by [log/slog]. - // As a workaround, we convert the slice into a [slog.GroupValue]. + // A slog.LogValuer in a slice is not supported by log/slog. + // As a workaround, we convert the slice into a slog.GroupValue. // More: // - https://github.com/golang/go/issues/63204 // - https://github.com/golang/go/issues/71088 diff --git a/slogattr/attr_test.go b/slogattr/attr_test.go index 1654074..8984f4b 100644 --- a/slogattr/attr_test.go +++ b/slogattr/attr_test.go @@ -23,7 +23,9 @@ func TestAll(t *testing.T) { l := slog.New(h) l.Info("", + slogattr.Error(nil), slogattr.Error(errors.New("oops")), + slogattr.Slice("empty_slice", []int{}), slogattr.Slice("slice", []int{1, 2, 3}), slogattr.Stringer("stringer", slog.LevelInfo), slogattr.LogValuer("log_valuer", point{1, 2}), diff --git a/slogctx/context.go b/slogctx/context.go index c5e0cce..d92af3e 100644 --- a/slogctx/context.go +++ b/slogctx/context.go @@ -3,30 +3,18 @@ package slogctx import ( "context" - "sync" + "slices" ) type ctxKey struct{} -type payload struct { - mu sync.RWMutex - args []any -} - -// With returns a [context.Context] with the given arguments attached. +// With returns a derived [context.Context] with the given arguments. // If called with this context, a [slog.Logger] created with [NewHandler] will automatically record these arguments. // The arguments are processed as if by [slog.Logger.Log]. func With(ctx context.Context, args ...any) context.Context { - p, ok := ctx.Value(ctxKey{}).(*payload) - if !ok { - a := make([]any, 0, max(10, len(args))) - a = append(a, args...) - return context.WithValue(ctx, ctxKey{}, &payload{args: a}) + if a, ok := ctx.Value(ctxKey{}).([]any); ok { + // Clip the slice to ensure a new backing array is allocated. + return context.WithValue(ctx, ctxKey{}, append(slices.Clip(a), args...)) } - - p.mu.Lock() - p.args = append(p.args, args...) - p.mu.Unlock() - - return ctx + return context.WithValue(ctx, ctxKey{}, args) } diff --git a/slogctx/handler.go b/slogctx/handler.go index 0187e4b..c4f33ea 100644 --- a/slogctx/handler.go +++ b/slogctx/handler.go @@ -16,10 +16,8 @@ func NewHandler(h slog.Handler) slog.Handler { // Handle implements [slog.Handler]. func (h *handler) Handle(ctx context.Context, r slog.Record) error { //nolint:gocritic // hugeParam: can't change the signature. - if p, ok := ctx.Value(ctxKey{}).(*payload); ok { - p.mu.RLock() - r.Add(p.args...) - p.mu.RUnlock() + if args, ok := ctx.Value(ctxKey{}).([]any); ok { + r.Add(args...) } return h.Handler.Handle(ctx, r) } diff --git a/slogctx/handler_test.go b/slogctx/handler_test.go index 5cb405e..5401c53 100644 --- a/slogctx/handler_test.go +++ b/slogctx/handler_test.go @@ -22,16 +22,13 @@ func TestHandler(t *testing.T) { l := slog.New(slogctx.NewHandler(h)) ctx := t.Context() - ctx = slogctx.With(ctx) - + ctx = slogctx.With(ctx, "x", 1) foo(ctx, l) - l.InfoContext(ctx, "got foo bar") got := "\n" + buf.String() want := ` -msg="adding foo" -msg="adding bar" foo=1 -msg="got foo bar" foo=1 bar=2 +msg="hello from foo" x=1 +msg="hello from bar" x=1 y=2 ` if got != want { t.Errorf("\ngot: %s\nwant: %s", got, want) @@ -39,22 +36,21 @@ msg="got foo bar" foo=1 bar=2 } func foo(ctx context.Context, l *slog.Logger) { - l.InfoContext(ctx, "adding foo") - ctx = slogctx.With(ctx, "foo", 1) + l.InfoContext(ctx, "hello from foo") + ctx = slogctx.With(ctx, "y", 2) bar(ctx, l) } func bar(ctx context.Context, l *slog.Logger) { - l.InfoContext(ctx, "adding bar") - _ = slogctx.With(ctx, "bar", 2) + l.InfoContext(ctx, "hello from bar") } // goos: darwin // goarch: arm64 -// pkg: go-simpler.org/slogutil/slogutil +// pkg: go-simpler.org/slogutil/slogctx // cpu: Apple M1 Pro -// BenchmarkHandler/enabled-8 204999273 5.743 ns/op 0 B/op 0 allocs/op -// BenchmarkHandler/disabled-8 261334262 4.591 ns/op 0 B/op 0 allocs/op +// BenchmarkHandler/enabled-8 205089428 5.726 ns/op 0 B/op 0 allocs/op +// BenchmarkHandler/disabled-8 262692470 4.568 ns/op 0 B/op 0 allocs/op func BenchmarkHandler(b *testing.B) { b.Run("enabled", func(b *testing.B) { benchmarkHandler(b, true)