Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
21 changes: 15 additions & 6 deletions slogattr/attr.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Package slogattr implements utilities for creating [slog.Attr]s.
// Package slogattr implements utilities for creating [slog.Attr].
package slogattr

import (
Expand All @@ -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)
}

Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions slogattr/attr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}),
Expand Down
24 changes: 6 additions & 18 deletions slogctx/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
6 changes: 2 additions & 4 deletions slogctx/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
22 changes: 9 additions & 13 deletions slogctx/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,39 +22,35 @@ 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)
}
}

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)
Expand Down
Loading