slog: add context arguments

Implement a more direct way to pass a context to a Handler.

- Add {Info,Debug,Warn,Error}Ctx functions and methods, that take a context.

- Add an initial context argument to Log and LogAttrs.

- Remove Logger.WithContext, Logger.Context and the Record.Context
  field: the output functions will pass their context argument
  directly to the Handler.

This CL also includes a somewhat unrelated set of changes:

- Add the Record.Add(...any) method, which behaves like Record.AddAttrs
  but processes its arguments like Logger.Log.

- Remove LogDepth and LogAttrsDepth, since they can now be implemented
  with exported API.

A later CL will update the package documentation.

Change-Id: I9216615598e1582a28fd48d78e85ff019d223924
Reviewed-on: https://go-review.googlesource.com/c/exp/+/469856
Run-TryBot: Jonathan Amsterdam <jba@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Alan Donovan <adonovan@google.com>
diff --git a/slog/benchmarks/benchmarks_test.go b/slog/benchmarks/benchmarks_test.go
index 0611257..50143ab 100644
--- a/slog/benchmarks/benchmarks_test.go
+++ b/slog/benchmarks/benchmarks_test.go
@@ -41,7 +41,7 @@
 					// should only be from Duration.String and Time.String.
 					"5 args",
 					func() {
-						logger.LogAttrs(slog.LevelInfo, TestMessage,
+						logger.LogAttrs(nil, slog.LevelInfo, TestMessage,
 							slog.String("string", TestString),
 							slog.Int("status", TestInt),
 							slog.Duration("duration", TestDuration),
@@ -53,7 +53,7 @@
 				{
 					"5 args ctx",
 					func() {
-						logger.WithContext(ctx).LogAttrs(slog.LevelInfo, TestMessage,
+						logger.LogAttrs(ctx, slog.LevelInfo, TestMessage,
 							slog.String("string", TestString),
 							slog.Int("status", TestInt),
 							slog.Duration("duration", TestDuration),
@@ -65,7 +65,7 @@
 				{
 					"10 args",
 					func() {
-						logger.LogAttrs(slog.LevelInfo, TestMessage,
+						logger.LogAttrs(nil, slog.LevelInfo, TestMessage,
 							slog.String("string", TestString),
 							slog.Int("status", TestInt),
 							slog.Duration("duration", TestDuration),
@@ -82,7 +82,7 @@
 				{
 					"40 args",
 					func() {
-						logger.LogAttrs(slog.LevelInfo, TestMessage,
+						logger.LogAttrs(nil, slog.LevelInfo, TestMessage,
 							slog.String("string", TestString),
 							slog.Int("status", TestInt),
 							slog.Duration("duration", TestDuration),
diff --git a/slog/example_custom_levels_test.go b/slog/example_custom_levels_test.go
index d0ec3a7..b701b44 100644
--- a/slog/example_custom_levels_test.go
+++ b/slog/example_custom_levels_test.go
@@ -74,13 +74,13 @@
 	}.NewTextHandler(os.Stdout)
 
 	logger := slog.New(th)
-	logger.Log(LevelEmergency, "missing pilots")
+	logger.Log(nil, LevelEmergency, "missing pilots")
 	logger.Error("failed to start engines", fmt.Errorf("missing fuel"))
 	logger.Warn("falling back to default value")
-	logger.Log(LevelNotice, "all systems are running")
+	logger.Log(nil, LevelNotice, "all systems are running")
 	logger.Info("initiating launch")
 	logger.Debug("starting background job")
-	logger.Log(LevelTrace, "button clicked")
+	logger.Log(nil, LevelTrace, "button clicked")
 
 	// Output:
 	// sev=EMERGENCY msg="missing pilots"
diff --git a/slog/example_depth_test.go b/slog/example_depth_test.go
index 95df718..34630c1 100644
--- a/slog/example_depth_test.go
+++ b/slog/example_depth_test.go
@@ -8,17 +8,26 @@
 	"fmt"
 	"os"
 	"path/filepath"
+	"runtime"
+	"time"
 
 	"golang.org/x/exp/slog"
 )
 
+// Infof is an example of a user-defined logging function that wraps slog.
+// The log record contains the source position of the caller of Infof.
 func Infof(format string, args ...any) {
-	// Use LogDepth to adjust source line information to point to the caller of Infof.
-	// The 1 passed to LogDepth refers to the caller of LogDepth, namely this function.
-	slog.Default().LogDepth(1, slog.LevelInfo, fmt.Sprintf(format, args...))
+	l := slog.Default()
+	if !l.Enabled(nil, slog.LevelInfo) {
+		return
+	}
+	var pcs [1]uintptr
+	runtime.Callers(2, pcs[:]) // skip [Callers, Infof]
+	r := slog.NewRecord(time.Now(), slog.LevelInfo, fmt.Sprintf(format, args...), pcs[0])
+	_ = l.Handler().Handle(nil, r)
 }
 
-func ExampleLogger_LogDepth() {
+func Example_wrapping() {
 	defer func(l *slog.Logger) { slog.SetDefault(l) }(slog.Default())
 
 	replace := func(groups []string, a slog.Attr) slog.Attr {
@@ -37,5 +46,5 @@
 	Infof("message, %s", "formatted")
 
 	// Output:
-	// level=INFO source=example_depth_test.go:37 msg="message, formatted"
+	// level=INFO source=example_depth_test.go:46 msg="message, formatted"
 }
diff --git a/slog/json_handler_test.go b/slog/json_handler_test.go
index 2a61136..dfaf00f 100644
--- a/slog/json_handler_test.go
+++ b/slog/json_handler_test.go
@@ -171,7 +171,7 @@
 			b.ReportAllocs()
 			b.ResetTimer()
 			for i := 0; i < b.N; i++ {
-				l.LogAttrs(LevelInfo, "this is a typical log message",
+				l.LogAttrs(nil, LevelInfo, "this is a typical log message",
 					String("module", "github.com/google/go-cmp"),
 					String("version", "v1.23.4"),
 					Int("count", 23),
@@ -232,7 +232,7 @@
 			b.ReportAllocs()
 			b.ResetTimer()
 			for i := 0; i < b.N; i++ {
-				l.LogAttrs(LevelInfo, "this is a typical log message",
+				l.LogAttrs(nil, LevelInfo, "this is a typical log message",
 					String("module", "github.com/google/go-cmp"),
 					String("version", "v1.23.4"),
 					Int("count", 23),
diff --git a/slog/logger.go b/slog/logger.go
index 7c37db8..1ba8463 100644
--- a/slog/logger.go
+++ b/slog/logger.go
@@ -73,7 +73,6 @@
 // that begins "With".
 type Logger struct {
 	handler Handler // for structured logging
-	ctx     context.Context
 }
 
 func (l *Logger) clone() *Logger {
@@ -84,9 +83,6 @@
 // Handler returns l's Handler.
 func (l *Logger) Handler() Handler { return l.handler }
 
-// 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] and resolved.
 // The Attrs will be added to each output from the Logger.
@@ -120,15 +116,6 @@
 
 }
 
-// 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 {
-	c := l.clone()
-	c.ctx = ctx
-	return c
-}
-
 // New creates a new Logger with the given non-nil Handler and a nil context.
 func New(h Handler) *Logger {
 	if h == nil {
@@ -142,9 +129,9 @@
 	return Default().With(args...)
 }
 
-// Enabled reports whether l emits log records at the given level.
-func (l *Logger) Enabled(level Level) bool {
-	return l.Handler().Enabled(l.ctx, level)
+// Enabled reports whether l emits log records at the given context and level.
+func (l *Logger) Enabled(ctx context.Context, level Level) bool {
+	return l.Handler().Enabled(ctx, level)
 }
 
 // NewLogLogger returns a new log.Logger such that each call to its Output method
@@ -164,73 +151,115 @@
 //     the following argument is treated as the value and the two are combined
 //     into an Attr.
 //   - Otherwise, the argument is treated as a value with key "!BADKEY".
-func (l *Logger) Log(level Level, msg string, args ...any) {
-	l.LogDepth(1, level, msg, args...)
+func (l *Logger) Log(ctx context.Context, level Level, msg string, args ...any) {
+	l.logDepth(ctx, 1, level, msg, args...)
 }
 
-func (l *Logger) logPC(err error, pc uintptr, level Level, msg string, args ...any) {
+func (l *Logger) logPC(ctx context.Context, err error, pc uintptr, level Level, msg string, args ...any) {
 	r := NewRecord(time.Now(), level, msg, pc)
 	if err != nil {
 		r.front[0] = Any(ErrorKey, err)
 		r.nFront++
 	}
-	r.setAttrsFromArgs(args)
-	_ = l.Handler().Handle(l.ctx, r)
+	r.Add(args...)
+	_ = l.Handler().Handle(ctx, r)
 }
 
 // LogAttrs is a more efficient version of [Logger.Log] that accepts only Attrs.
-func (l *Logger) LogAttrs(level Level, msg string, attrs ...Attr) {
-	l.LogAttrsDepth(1, level, msg, attrs...)
+func (l *Logger) LogAttrs(ctx context.Context, level Level, msg string, attrs ...Attr) {
+	l.logAttrsDepth(ctx, 1, level, msg, attrs...)
 }
 
 // Debug logs at LevelDebug.
 func (l *Logger) Debug(msg string, args ...any) {
-	l.LogDepth(1, LevelDebug, msg, args...)
+	l.logDepth(nil, 1, LevelDebug, msg, args...)
+}
+
+// DebugCtx logs at LevelDebug with the given context.
+func (l *Logger) DebugCtx(ctx context.Context, msg string, args ...any) {
+	l.logDepth(ctx, 1, LevelDebug, msg, args...)
 }
 
 // Info logs at LevelInfo.
 func (l *Logger) Info(msg string, args ...any) {
-	l.LogDepth(1, LevelInfo, msg, args...)
+	l.logDepth(nil, 1, LevelInfo, msg, args...)
+}
+
+// InfoCtx logs at LevelInfo with the given context.
+func (l *Logger) InfoCtx(ctx context.Context, msg string, args ...any) {
+	l.logDepth(ctx, 1, LevelInfo, msg, args...)
 }
 
 // Warn logs at LevelWarn.
 func (l *Logger) Warn(msg string, args ...any) {
-	l.LogDepth(1, LevelWarn, msg, args...)
+	l.logDepth(nil, 1, LevelWarn, msg, args...)
+}
+
+// WarnCtx logs at LevelWarn with the given context.
+func (l *Logger) WarnCtx(ctx context.Context, msg string, args ...any) {
+	l.logDepth(ctx, 1, LevelWarn, msg, args...)
 }
 
 // Error logs at LevelError.
 // If err is non-nil, Error adds Any(ErrorKey, err)
 // before the list of attributes.
 func (l *Logger) Error(msg string, err error, args ...any) {
-	l.logDepthErr(err, 1, LevelError, msg, args...)
+	l.logDepthErr(nil, err, 1, LevelError, msg, args...)
+}
+
+// ErrorCtx logs at LevelError with the given context.
+// If err is non-nil, it adds Any(ErrorKey, err)
+// before the list of attributes.
+func (l *Logger) ErrorCtx(ctx context.Context, msg string, err error, args ...any) {
+	l.logDepthErr(ctx, err, 1, LevelError, msg, args...)
 }
 
 // Debug calls Logger.Debug on the default logger.
 func Debug(msg string, args ...any) {
-	Default().LogDepth(1, LevelDebug, msg, args...)
+	Default().logDepth(nil, 1, LevelDebug, msg, args...)
+}
+
+// DebugCtx calls Logger.DebugCtx on the default logger.
+func DebugCtx(ctx context.Context, msg string, args ...any) {
+	Default().logDepth(ctx, 1, LevelDebug, msg, args...)
 }
 
 // Info calls Logger.Info on the default logger.
 func Info(msg string, args ...any) {
-	Default().LogDepth(1, LevelInfo, msg, args...)
+	Default().logDepth(nil, 1, LevelInfo, msg, args...)
+}
+
+// InfoCtx calls Logger.InfoCtx on the default logger.
+func InfoCtx(ctx context.Context, msg string, args ...any) {
+	Default().logDepth(ctx, 1, LevelInfo, msg, args...)
 }
 
 // Warn calls Logger.Warn on the default logger.
 func Warn(msg string, args ...any) {
-	Default().LogDepth(1, LevelWarn, msg, args...)
+	Default().logDepth(nil, 1, LevelWarn, msg, args...)
+}
+
+// WarnCtx calls Logger.WarnCtx on the default logger.
+func WarnCtx(ctx context.Context, msg string, args ...any) {
+	Default().logDepth(ctx, 1, LevelWarn, msg, args...)
 }
 
 // Error calls Logger.Error on the default logger.
 func Error(msg string, err error, args ...any) {
-	Default().logDepthErr(err, 1, LevelError, msg, args...)
+	Default().logDepthErr(nil, err, 1, LevelError, msg, args...)
+}
+
+// ErrorCtx calls Logger.ErrorCtx on the default logger.
+func ErrorCtx(ctx context.Context, msg string, err error, args ...any) {
+	Default().logDepthErr(ctx, err, 1, LevelError, msg, args...)
 }
 
 // Log calls Logger.Log on the default logger.
-func Log(level Level, msg string, args ...any) {
-	Default().LogDepth(1, level, msg, args...)
+func Log(ctx context.Context, level Level, msg string, args ...any) {
+	Default().logDepth(ctx, 1, level, msg, args...)
 }
 
 // LogAttrs calls Logger.LogAttrs on the default logger.
-func LogAttrs(level Level, msg string, attrs ...Attr) {
-	Default().LogAttrsDepth(1, level, msg, attrs...)
+func LogAttrs(ctx context.Context, level Level, msg string, attrs ...Attr) {
+	Default().logAttrsDepth(ctx, 1, level, msg, attrs...)
 }
diff --git a/slog/logger_test.go b/slog/logger_test.go
index 5d5ed07..529a1c6 100644
--- a/slog/logger_test.go
+++ b/slog/logger_test.go
@@ -48,10 +48,10 @@
 	l.Error("bad", io.EOF, "a", 1)
 	check(`level=ERROR msg=bad err=EOF a=1`)
 
-	l.Log(LevelWarn+1, "w", Int("a", 1), String("b", "two"))
+	l.Log(nil, LevelWarn+1, "w", Int("a", 1), String("b", "two"))
 	check(`level=WARN\+1 msg=w a=1 b=two`)
 
-	l.LogAttrs(LevelInfo+1, "a b c", Int("a", 1), String("b", "two"))
+	l.LogAttrs(nil, LevelInfo+1, "a b c", Int("a", 1), String("b", "two"))
 	check(`level=INFO\+1 msg="a b c" a=1 b=two`)
 
 	l.Info("info", "a", []Attr{Int("i", 1)})
@@ -189,9 +189,9 @@
 	startLine = f.Line + 4
 	// Do not change the number of lines between here and the call to check(0).
 
-	logger.Log(LevelInfo, "")
+	logger.Log(nil, LevelInfo, "")
 	check(0)
-	logger.LogAttrs(LevelInfo, "")
+	logger.LogAttrs(nil, LevelInfo, "")
 	check(1)
 	logger.Debug("")
 	check(2)
@@ -209,9 +209,9 @@
 	check(8)
 	Error("", nil)
 	check(9)
-	Log(LevelInfo, "")
+	Log(nil, LevelInfo, "")
 	check(10)
-	LogAttrs(LevelInfo, "")
+	LogAttrs(nil, LevelInfo, "")
 	check(11)
 }
 
@@ -230,7 +230,7 @@
 		wantAllocs(t, 0, func() { dl.Info("hello") })
 	})
 	t.Run("logger.Log", func(t *testing.T) {
-		wantAllocs(t, 0, func() { dl.Log(LevelDebug, "hello") })
+		wantAllocs(t, 0, func() { dl.Log(nil, LevelDebug, "hello") })
 	})
 	t.Run("2 pairs", func(t *testing.T) {
 		s := "abc"
@@ -247,7 +247,7 @@
 		s := "abc"
 		i := 2000
 		wantAllocs(t, 2, func() {
-			l.Log(LevelInfo, "hello",
+			l.Log(nil, LevelInfo, "hello",
 				"n", i,
 				"s", s,
 			)
@@ -258,8 +258,8 @@
 		s := "abc"
 		i := 2000
 		wantAllocs(t, 0, func() {
-			if l.Enabled(LevelInfo) {
-				l.Log(LevelInfo, "hello",
+			if l.Enabled(nil, LevelInfo) {
+				l.Log(nil, LevelInfo, "hello",
 					"n", i,
 					"s", s,
 				)
@@ -281,30 +281,30 @@
 		wantAllocs(t, 0, func() { dl.Info("", "error", io.EOF) })
 	})
 	t.Run("attrs1", func(t *testing.T) {
-		wantAllocs(t, 0, func() { dl.LogAttrs(LevelInfo, "", Int("a", 1)) })
-		wantAllocs(t, 0, func() { dl.LogAttrs(LevelInfo, "", Any("error", io.EOF)) })
+		wantAllocs(t, 0, func() { dl.LogAttrs(nil, LevelInfo, "", Int("a", 1)) })
+		wantAllocs(t, 0, func() { dl.LogAttrs(nil, LevelInfo, "", Any("error", io.EOF)) })
 	})
 	t.Run("attrs3", func(t *testing.T) {
 		wantAllocs(t, 0, func() {
-			dl.LogAttrs(LevelInfo, "hello", Int("a", 1), String("b", "two"), Duration("c", time.Second))
+			dl.LogAttrs(nil, LevelInfo, "hello", Int("a", 1), String("b", "two"), Duration("c", time.Second))
 		})
 	})
 	t.Run("attrs3 disabled", func(t *testing.T) {
 		logger := New(discardHandler{disabled: true})
 		wantAllocs(t, 0, func() {
-			logger.LogAttrs(LevelInfo, "hello", Int("a", 1), String("b", "two"), Duration("c", time.Second))
+			logger.LogAttrs(nil, LevelInfo, "hello", Int("a", 1), String("b", "two"), Duration("c", time.Second))
 		})
 	})
 	t.Run("attrs6", func(t *testing.T) {
 		wantAllocs(t, 1, func() {
-			dl.LogAttrs(LevelInfo, "hello",
+			dl.LogAttrs(nil, LevelInfo, "hello",
 				Int("a", 1), String("b", "two"), Duration("c", time.Second),
 				Int("d", 1), String("e", "two"), Duration("f", time.Second))
 		})
 	})
 	t.Run("attrs9", func(t *testing.T) {
 		wantAllocs(t, 1, func() {
-			dl.LogAttrs(LevelInfo, "hello",
+			dl.LogAttrs(nil, LevelInfo, "hello",
 				Int("a", 1), String("b", "two"), Duration("c", time.Second),
 				Int("d", 1), String("e", "two"), Duration("f", time.Second),
 				Int("d", 1), String("e", "two"), Duration("f", time.Second))
@@ -325,7 +325,7 @@
 		{[]any{"a", 1, 2, 3}, []Attr{Int("a", 1), Int(badKey, 2), Int(badKey, 3)}},
 	} {
 		r := NewRecord(time.Time{}, 0, "", 0)
-		r.setAttrsFromArgs(test.args)
+		r.Add(test.args...)
 		got := attrsSlice(r)
 		if !attrsEqual(got, test.want) {
 			t.Errorf("%v:\ngot  %v\nwant %v", test.args, got, test.want)
@@ -368,46 +368,6 @@
 	checkLogOutput(t, buf.String(), `level=ERROR msg=msg err=EOF !BADKEY=a`)
 }
 
-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 TestNewLogLogger(t *testing.T) {
 	var buf bytes.Buffer
 	h := NewTextHandler(&buf)
@@ -493,40 +453,40 @@
 	b.Run("no attrs", func(b *testing.B) {
 		b.ReportAllocs()
 		for i := 0; i < b.N; i++ {
-			l.LogAttrs(LevelInfo, "msg")
+			l.LogAttrs(nil, LevelInfo, "msg")
 		}
 	})
 	b.Run("attrs", func(b *testing.B) {
 		b.ReportAllocs()
 		for i := 0; i < b.N; i++ {
-			l.LogAttrs(LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true))
+			l.LogAttrs(nil, LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true))
 		}
 	})
 	b.Run("attrs-parallel", func(b *testing.B) {
 		b.ReportAllocs()
 		b.RunParallel(func(pb *testing.PB) {
 			for pb.Next() {
-				l.LogAttrs(LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true))
+				l.LogAttrs(nil, LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true))
 			}
 		})
 	})
 	b.Run("keys-values", func(b *testing.B) {
 		b.ReportAllocs()
 		for i := 0; i < b.N; i++ {
-			l.Log(LevelInfo, "msg", "a", 1, "b", "two", "c", true)
+			l.Log(nil, LevelInfo, "msg", "a", 1, "b", "two", "c", true)
 		}
 	})
 	b.Run("WithContext", func(b *testing.B) {
 		b.ReportAllocs()
 		for i := 0; i < b.N; i++ {
-			l.WithContext(ctx).LogAttrs(LevelInfo, "msg2", Int("a", 1), String("b", "two"), Bool("c", true))
+			l.LogAttrs(ctx, LevelInfo, "msg2", Int("a", 1), String("b", "two"), Bool("c", true))
 		}
 	})
 	b.Run("WithContext-parallel", func(b *testing.B) {
 		b.ReportAllocs()
 		b.RunParallel(func(pb *testing.PB) {
 			for pb.Next() {
-				l.WithContext(ctx).LogAttrs(LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true))
+				l.LogAttrs(ctx, LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true))
 			}
 		})
 	})
diff --git a/slog/pc.go b/slog/pc.go
index 2d794a1..6e74bd3 100644
--- a/slog/pc.go
+++ b/slog/pc.go
@@ -7,6 +7,7 @@
 package slog
 
 import (
+	"context"
 	"runtime"
 	"time"
 )
@@ -17,26 +18,26 @@
 // LogDepth is like [Logger.Log], but accepts a call depth to adjust the
 // file and line number in the log record. 1 refers to the caller
 // of LogDepth; 2 refers to the caller's caller; and so on.
-func (l *Logger) LogDepth(calldepth int, level Level, msg string, args ...any) {
-	if !l.Enabled(level) {
+func (l *Logger) logDepth(ctx context.Context, calldepth int, level Level, msg string, args ...any) {
+	if !l.Enabled(ctx, level) {
 		return
 	}
 	var pcs [1]uintptr
 	runtime.Callers(calldepth+2, pcs[:])
-	l.logPC(nil, pcs[0], level, msg, args...)
+	l.logPC(ctx, nil, pcs[0], level, msg, args...)
 }
 
 // LogAttrsDepth is like [Logger.LogAttrs], but accepts a call depth argument
 // which it interprets like [Logger.LogDepth].
-func (l *Logger) LogAttrsDepth(calldepth int, level Level, msg string, attrs ...Attr) {
-	if !l.Enabled(level) {
+func (l *Logger) logAttrsDepth(ctx context.Context, calldepth int, level Level, msg string, attrs ...Attr) {
+	if !l.Enabled(ctx, level) {
 		return
 	}
 	var pcs [1]uintptr
 	runtime.Callers(calldepth+2, pcs[:])
 	r := NewRecord(time.Now(), level, msg, pcs[0])
 	r.AddAttrs(attrs...)
-	_ = l.Handler().Handle(l.ctx, r)
+	_ = l.Handler().Handle(ctx, r)
 }
 
 // logDepthErr is a trivial wrapper around logDepth, just to make the call
@@ -45,13 +46,13 @@
 // TODO: When slog moves to the standard library, replace the fixed call depth
 // with logic based on the Record's pc, and remove this function. See the
 // comment on TestConnections/wrap_default_handler.
-func (l *Logger) logDepthErr(err error, calldepth int, level Level, msg string, args ...any) {
-	if !l.Enabled(level) {
+func (l *Logger) logDepthErr(ctx context.Context, err error, calldepth int, level Level, msg string, args ...any) {
+	if !l.Enabled(ctx, level) {
 		return
 	}
 	var pcs [1]uintptr
 	runtime.Callers(calldepth+2, pcs[:])
-	l.logPC(err, pcs[0], level, msg, args...)
+	l.logPC(ctx, err, pcs[0], level, msg, args...)
 }
 
 // callerPC returns the program counter at the given stack depth.
diff --git a/slog/record.go b/slog/record.go
index 895e505..6911c6c 100644
--- a/slog/record.go
+++ b/slog/record.go
@@ -114,7 +114,10 @@
 	r.back = append(r.back, attrs[n:]...)
 }
 
-func (r *Record) setAttrsFromArgs(args []any) {
+// Add converts the args to Attrs as described in [Logger.Log],
+// then appends the Attrs to the Record's list of Attrs.
+// It resolves the Attrs before doing so.
+func (r *Record) Add(args ...any) {
 	var a Attr
 	for len(args) > 0 {
 		a, args = argsToAttr(args)