mirror of
https://github.com/golang/go.git
synced 2025-10-19 19:13:18 +00:00
log/slog: export Source method in Record for custom handler support
Currently, the `source` method in `slog.Record` is not accessible to
custom handlers, requiring developers to re-implement logic for
retrieving source location information. This commit exports the `source`
method as `Source`, enabling consistent access for custom logging
handlers and reducing code redundancy.
Fixes #70280
Change-Id: I3eb3bc60658abc5de95697a10bddd11ab54c6e13
GitHub-Last-Rev: bd81afe5a5
GitHub-Pull-Request: golang/go#70281
Reviewed-on: https://go-review.googlesource.com/c/go/+/626976
Reviewed-by: qiu laidongfeng2 <2645477756@qq.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
parent
35b4fd9f37
commit
044ca4e5c8
7 changed files with 75 additions and 14 deletions
1
api/next/70280.txt
Normal file
1
api/next/70280.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pkg log/slog, method (Record) Source() *Source #70280
|
1
doc/next/6-stdlib/99-minor/log/slog/70280.md
Normal file
1
doc/next/6-stdlib/99-minor/log/slog/70280.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[Record] now has a Source() method, returning its source location or nil if unavailable.
|
|
@ -299,7 +299,11 @@ func (h *commonHandler) handle(r Record) error {
|
||||||
}
|
}
|
||||||
// source
|
// source
|
||||||
if h.opts.AddSource {
|
if h.opts.AddSource {
|
||||||
state.appendAttr(Any(SourceKey, r.source()))
|
src := r.Source()
|
||||||
|
if src == nil {
|
||||||
|
src = &Source{}
|
||||||
|
}
|
||||||
|
state.appendAttr(Any(SourceKey, src))
|
||||||
}
|
}
|
||||||
key = MessageKey
|
key = MessageKey
|
||||||
msg := r.Message
|
msg := r.Message
|
||||||
|
|
|
@ -547,7 +547,11 @@ func TestJSONAndTextHandlers(t *testing.T) {
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
r := NewRecord(testTime, LevelInfo, "message", callerPC(2))
|
r := NewRecord(testTime, LevelInfo, "message", callerPC(2))
|
||||||
line := strconv.Itoa(r.source().Line)
|
source := r.Source()
|
||||||
|
if source == nil {
|
||||||
|
t.Fatal("source is nil")
|
||||||
|
}
|
||||||
|
line := strconv.Itoa(source.Line)
|
||||||
r.AddAttrs(test.attrs...)
|
r.AddAttrs(test.attrs...)
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
opts := HandlerOptions{ReplaceAttr: test.replace, AddSource: test.addSource}
|
opts := HandlerOptions{ReplaceAttr: test.replace, AddSource: test.addSource}
|
||||||
|
@ -634,6 +638,40 @@ func TestHandlerEnabled(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestJSONAndTextHandlersWithUnavailableSource(t *testing.T) {
|
||||||
|
// Verify that a nil source does not cause a panic.
|
||||||
|
// and that the source is empty.
|
||||||
|
var buf bytes.Buffer
|
||||||
|
opts := &HandlerOptions{
|
||||||
|
ReplaceAttr: removeKeys(LevelKey),
|
||||||
|
AddSource: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
h Handler
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"text", NewTextHandler(&buf, opts), "source=:0 msg=message"},
|
||||||
|
{"json", NewJSONHandler(&buf, opts), `{"msg":"message"}`},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
buf.Reset()
|
||||||
|
r := NewRecord(time.Time{}, LevelInfo, "message", 0)
|
||||||
|
err := test.h.Handle(t.Context(), r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := strings.TrimSpace(test.want)
|
||||||
|
got := strings.TrimSpace(buf.String())
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("\ngot %s\nwant %s", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSecondWith(t *testing.T) {
|
func TestSecondWith(t *testing.T) {
|
||||||
// Verify that a second call to Logger.With does not corrupt
|
// Verify that a second call to Logger.With does not corrupt
|
||||||
// the original.
|
// the original.
|
||||||
|
|
|
@ -190,7 +190,10 @@ func TestCallDepth(t *testing.T) {
|
||||||
const wantFunc = "log/slog.TestCallDepth"
|
const wantFunc = "log/slog.TestCallDepth"
|
||||||
const wantFile = "logger_test.go"
|
const wantFile = "logger_test.go"
|
||||||
wantLine := startLine + count*2
|
wantLine := startLine + count*2
|
||||||
got := h.r.source()
|
got := h.r.Source()
|
||||||
|
if got == nil {
|
||||||
|
t.Fatal("got nil source")
|
||||||
|
}
|
||||||
gotFile := filepath.Base(got.File)
|
gotFile := filepath.Base(got.File)
|
||||||
if got.Function != wantFunc || gotFile != wantFile || got.Line != wantLine {
|
if got.Function != wantFunc || gotFile != wantFile || got.Line != wantLine {
|
||||||
t.Errorf("got (%s, %s, %d), want (%s, %s, %d)",
|
t.Errorf("got (%s, %s, %d), want (%s, %s, %d)",
|
||||||
|
|
|
@ -211,11 +211,14 @@ func (s *Source) group() Value {
|
||||||
return GroupValue(as...)
|
return GroupValue(as...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// source returns a Source for the log event.
|
// Source returns a new Source for the log event using r's PC.
|
||||||
// If the Record was created without the necessary information,
|
// If the PC field is zero, meaning the Record was created without the necessary information
|
||||||
// or if the location is unavailable, it returns a non-nil *Source
|
// or the location is unavailable, then nil is returned.
|
||||||
// with zero fields.
|
func (r Record) Source() *Source {
|
||||||
func (r Record) source() *Source {
|
if r.PC == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
fs := runtime.CallersFrames([]uintptr{r.PC})
|
fs := runtime.CallersFrames([]uintptr{r.PC})
|
||||||
f, _ := fs.Next()
|
f, _ := fs.Next()
|
||||||
return &Source{
|
return &Source{
|
||||||
|
|
|
@ -39,24 +39,35 @@ func TestRecordAttrs(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRecordSource(t *testing.T) {
|
func TestRecordSource(t *testing.T) {
|
||||||
// Zero call depth => empty *Source.
|
// Zero call depth => nil *Source.
|
||||||
for _, test := range []struct {
|
for _, test := range []struct {
|
||||||
depth int
|
depth int
|
||||||
wantFunction string
|
wantFunction string
|
||||||
wantFile string
|
wantFile string
|
||||||
wantLinePositive bool
|
wantLinePositive bool
|
||||||
|
wantNil bool
|
||||||
}{
|
}{
|
||||||
{0, "", "", false},
|
{0, "", "", false, true},
|
||||||
{-16, "", "", false},
|
{-16, "", "", false, true},
|
||||||
{1, "log/slog.TestRecordSource", "record_test.go", true}, // 1: caller of NewRecord
|
{1, "log/slog.TestRecordSource", "record_test.go", true, false}, // 1: caller of NewRecord
|
||||||
{2, "testing.tRunner", "testing.go", true},
|
{2, "testing.tRunner", "testing.go", true, false},
|
||||||
} {
|
} {
|
||||||
var pc uintptr
|
var pc uintptr
|
||||||
if test.depth > 0 {
|
if test.depth > 0 {
|
||||||
pc = callerPC(test.depth + 1)
|
pc = callerPC(test.depth + 1)
|
||||||
}
|
}
|
||||||
r := NewRecord(time.Time{}, 0, "", pc)
|
r := NewRecord(time.Time{}, 0, "", pc)
|
||||||
got := r.source()
|
got := r.Source()
|
||||||
|
if test.wantNil {
|
||||||
|
if got != nil {
|
||||||
|
t.Errorf("depth %d: got non-nil Source, want nil", test.depth)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if got == nil {
|
||||||
|
t.Errorf("depth %d: got nil Source, want non-nil", test.depth)
|
||||||
|
continue
|
||||||
|
}
|
||||||
if i := strings.LastIndexByte(got.File, '/'); i >= 0 {
|
if i := strings.LastIndexByte(got.File, '/'); i >= 0 {
|
||||||
got.File = got.File[i+1:]
|
got.File = got.File[i+1:]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue