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)