slog: have Logger.With,WithGroup preserve context
When we create a new Logger using With or WithGroup, preserve
the context from the original Logger.
Change-Id: I62cc73aaff50d6ae755ca57f5a934604c4645f2e
Reviewed-on: https://go-review.googlesource.com/c/exp/+/459615
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
diff --git a/slog/logger.go b/slog/logger.go
index 08dc7ce..d949a3d 100644
--- a/slog/logger.go
+++ b/slog/logger.go
@@ -69,15 +69,20 @@
ctx context.Context
}
+func (l *Logger) clone() *Logger {
+ c := *l
+ return &c
+}
+
// Handler returns l's Handler.
func (l *Logger) Handler() Handler { return l.handler }
-// Context returns l's context.
+// Context returns l's context, which may be nil.
func (l *Logger) Context() context.Context { return l.ctx }
// With returns a new Logger that includes the given arguments, converted to
// Attrs as in [Logger.Log]. The Attrs will be added to each output from the
-// Logger.
+// Logger. The new Logger shares the old Logger's context.
//
// The new Logger's handler is the result of calling WithAttrs on the receiver's
// handler.
@@ -90,28 +95,40 @@
attr, args = argsToAttr(args)
attrs = append(attrs, attr)
}
- return New(l.handler.WithAttrs(attrs))
+ c := l.clone()
+ c.handler = l.handler.WithAttrs(attrs)
+ return c
}
// WithGroup returns a new Logger that starts a group. The keys of all
// attributes added to the Logger will be qualified by the given name.
+// The new Logger shares the old Logger's context.
//
// The new Logger's handler is the result of calling WithGroup on the receiver's
// handler.
func (l *Logger) WithGroup(name string) *Logger {
- return New(l.handler.WithGroup(name))
+ c := l.clone()
+ c.handler = l.handler.WithGroup(name)
+ return c
+
}
// WithContext returns a new Logger with the same handler
// as the receiver and the given context.
+// It uses the same handler as the original.
func (l *Logger) WithContext(ctx context.Context) *Logger {
- l2 := *l
- l2.ctx = ctx
- return &l2
+ c := l.clone()
+ c.ctx = ctx
+ return c
}
-// New creates a new Logger with the given Handler.
-func New(h Handler) *Logger { return &Logger{handler: h} }
+// New creates a new Logger with the given non-nil Handler and a nil context.
+func New(h Handler) *Logger {
+ if h == nil {
+ panic("nil Handler")
+ }
+ return &Logger{handler: h}
+}
// With calls Logger.With on the default logger.
func With(args ...any) *Logger {
diff --git a/slog/logger_test.go b/slog/logger_test.go
index 141307a..5f40cc1 100644
--- a/slog/logger_test.go
+++ b/slog/logger_test.go
@@ -15,6 +15,8 @@
"strings"
"testing"
"time"
+
+ "golang.org/x/exp/slices"
)
const timeRE = `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}(Z|[+-]\d{2}:\d{2})`
@@ -347,6 +349,46 @@
checkLogOutput(t, buf.String(), `level=ERROR msg=msg !BADKEY=a err=EOF`)
}
+func TestLogCopying(t *testing.T) {
+ // Verify that Logger methods that purport to set one field of a new Logger
+ // actually do so while preserving the other field.
+
+ h := &captureHandler{} // Use a captureHandler for convenience.
+ l := New(h)
+ ctx := context.WithValue(context.Background(), "v", 0)
+
+ checkContext := func(l *Logger) {
+ t.Helper()
+ ctx := l.Context()
+ if ctx == nil {
+ t.Error("nil context")
+ } else if got, want := ctx.Value("v"), 0; got != want {
+ t.Errorf("for got %v, want %v", got, want)
+ }
+ }
+
+ // WithContext returns a Logger with the given context and the same handler.
+ l2 := l.WithContext(ctx)
+ checkContext(l2)
+ if l2.Handler() != h {
+ t.Error("WithContext changed handler")
+ }
+
+ // With returns a Logger with a different handler but the same context.
+ l3 := l2.With("a", 1)
+ if l3.Handler() == l2.Handler() {
+ t.Error("With did not change handler")
+ }
+ checkContext(l3)
+
+ // WithGroup also returns a Logger with a different handler but the same context.
+ l4 := l3.WithGroup("g")
+ if l4.Handler() == l3.Handler() {
+ t.Error("With did not change handler")
+ }
+ checkContext(l4)
+}
+
func checkLogOutput(t *testing.T, got, wantRegexp string) {
t.Helper()
got = clean(got)
@@ -369,8 +411,9 @@
}
type captureHandler struct {
- r Record
- attrs []Attr
+ r Record
+ attrs []Attr
+ groups []string
}
func (h *captureHandler) Handle(r Record) error {
@@ -386,8 +429,10 @@
return &c2
}
-func (h *captureHandler) WithGroup(name string) Handler {
- panic("unimplemented")
+func (c *captureHandler) WithGroup(name string) Handler {
+ c2 := *c
+ c2.groups = append(slices.Clip(c2.groups), name)
+ return &c2
}
type discardHandler struct {