Skip to content

Commit be6415c

Browse files
committed
slog: backport changes from stdlib
The standard library version of slog has diverged from this version somewhat; bring them back into sync, as much as possible. Since moving to the standard library, a Handler rule has changed: a Handler should ignore the zero slog.Attr, but not an Attr whose key is empty but whose value is non-zero. Change the check in slogtest to reflect that. Fixes golang/go#59727. Change-Id: Idd2084b84a3faf9b1796051af1578aee6558ed02 Reviewed-on: https://go-review.googlesource.com/c/exp/+/486795 TryBot-Result: Gopher Robot <gobot@golang.org> Run-TryBot: Jonathan Amsterdam <jba@google.com> Reviewed-by: Alan Donovan <adonovan@google.com>
1 parent 864b3d6 commit be6415c

18 files changed

+186
-111
lines changed

slog/attr.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,9 @@ func (a Attr) Equal(b Attr) bool {
8282
func (a Attr) String() string {
8383
return fmt.Sprintf("%s=%s", a.Key, a.Value)
8484
}
85+
86+
// isEmpty reports whether a has an empty key and a nil value.
87+
// That can be written as Attr{} or Any("", nil).
88+
func (a Attr) isEmpty() bool {
89+
return a.Key == "" && a.Value.num == 0 && a.Value.any == nil
90+
}

slog/benchmarks/handlers.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,12 @@ func (h *fastTextHandler) Handle(_ context.Context, r slog.Record) error {
4848
buf.WriteByte(' ')
4949
buf.WriteString("msg=")
5050
buf.WriteString(r.Message)
51-
r.Attrs(func(a slog.Attr) {
51+
r.Attrs(func(a slog.Attr) bool {
5252
buf.WriteByte(' ')
5353
buf.WriteString(a.Key)
5454
buf.WriteByte('=')
5555
h.appendValue(buf, a.Value)
56+
return true
5657
})
5758
buf.WriteByte('\n')
5859
_, err := h.w.Write(*buf)

slog/benchmarks/handlers_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,6 @@ func TestHandlers(t *testing.T) {
3838

3939
func attrSlice(r slog.Record) []slog.Attr {
4040
var as []slog.Attr
41-
r.Attrs(func(a slog.Attr) { as = append(as, a) })
41+
r.Attrs(func(a slog.Attr) bool { as = append(as, a); return true })
4242
return as
4343
}

slog/example_depth_test.go renamed to slog/example_wrap_test.go

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,21 @@ import (
1717

1818
// Infof is an example of a user-defined logging function that wraps slog.
1919
// The log record contains the source position of the caller of Infof.
20-
func Infof(format string, args ...any) {
21-
l := slog.Default()
22-
if !l.Enabled(context.Background(), slog.LevelInfo) {
20+
func Infof(logger *slog.Logger, format string, args ...any) {
21+
if !logger.Enabled(context.Background(), slog.LevelInfo) {
2322
return
2423
}
2524
var pcs [1]uintptr
2625
runtime.Callers(2, pcs[:]) // skip [Callers, Infof]
2726
r := slog.NewRecord(time.Now(), slog.LevelInfo, fmt.Sprintf(format, args...), pcs[0])
28-
_ = l.Handler().Handle(context.Background(), r)
27+
_ = logger.Handler().Handle(context.Background(), r)
2928
}
3029

3130
func Example_wrapping() {
32-
defer func(l *slog.Logger) { slog.SetDefault(l) }(slog.Default())
33-
3431
replace := func(groups []string, a slog.Attr) slog.Attr {
3532
// Remove time.
3633
if a.Key == slog.TimeKey && len(groups) == 0 {
37-
a.Key = ""
34+
return slog.Attr{}
3835
}
3936
// Remove the directory from the source's filename.
4037
if a.Key == slog.SourceKey {
@@ -43,9 +40,8 @@ func Example_wrapping() {
4340
return a
4441
}
4542
logger := slog.New(slog.HandlerOptions{AddSource: true, ReplaceAttr: replace}.NewTextHandler(os.Stdout))
46-
slog.SetDefault(logger)
47-
Infof("message, %s", "formatted")
43+
Infof(logger, "message, %s", "formatted")
4844

4945
// Output:
50-
// level=INFO source=example_depth_test.go:47 msg="message, formatted"
46+
// level=INFO source=example_wrap_test.go:43 msg="message, formatted"
5147
}

slog/handler.go

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,9 @@ type Handler interface {
5151
// Handle methods that produce output should observe the following rules:
5252
// - If r.Time is the zero time, ignore the time.
5353
// - If r.PC is zero, ignore it.
54-
// - If an Attr's key is the empty string and the value is not a group,
55-
// ignore the Attr.
54+
// - Attr's values should be resolved.
55+
// - If an Attr's key and value are both the zero value, ignore the Attr.
56+
// This can be tested with attr.Equal(Attr{}).
5657
// - If a group's key is empty, inline the group's Attrs.
5758
// - If a group has no Attrs (even if it has a non-empty key),
5859
// ignore it.
@@ -61,7 +62,6 @@ type Handler interface {
6162
// WithAttrs returns a new Handler whose attributes consist of
6263
// both the receiver's attributes and the arguments.
6364
// The Handler owns the slice: it may retain, modify or discard it.
64-
// [Logger.With] will resolve the Attrs.
6565
WithAttrs(attrs []Attr) Handler
6666

6767
// WithGroup returns a new Handler with the given group appended to
@@ -333,8 +333,9 @@ func (s *handleState) appendNonBuiltIns(r Record) {
333333
defer s.prefix.Free()
334334
s.prefix.WriteString(s.h.groupPrefix)
335335
s.openGroups()
336-
r.Attrs(func(a Attr) {
336+
r.Attrs(func(a Attr) bool {
337337
s.appendAttr(a)
338+
return true
338339
})
339340
if s.h.json {
340341
// Close all open groups.
@@ -440,26 +441,22 @@ func (s *handleState) closeGroup(name string) {
440441
// It handles replacement and checking for an empty key.
441442
// after replacement).
442443
func (s *handleState) appendAttr(a Attr) {
443-
v := a.Value
444-
// Elide a non-group with an empty key.
445-
if a.Key == "" && v.Kind() != KindGroup {
446-
return
447-
}
448-
if rep := s.h.opts.ReplaceAttr; rep != nil && v.Kind() != KindGroup {
444+
if rep := s.h.opts.ReplaceAttr; rep != nil && a.Value.Kind() != KindGroup {
449445
var gs []string
450446
if s.groups != nil {
451447
gs = *s.groups
452448
}
453-
a = rep(gs, Attr{a.Key, v})
454-
if a.Key == "" {
455-
return
456-
}
457-
// Although all attributes in the Record are already resolved,
458-
// This one came from the user, so it may not have been.
459-
v = a.Value.Resolve()
449+
// Resolve before calling ReplaceAttr, so the user doesn't have to.
450+
a.Value = a.Value.Resolve()
451+
a = rep(gs, a)
452+
}
453+
a.Value = a.Value.Resolve()
454+
// Elide empty Attrs.
455+
if a.isEmpty() {
456+
return
460457
}
461-
if v.Kind() == KindGroup {
462-
attrs := v.Group()
458+
if a.Value.Kind() == KindGroup {
459+
attrs := a.Value.Group()
463460
// Output only non-empty groups.
464461
if len(attrs) > 0 {
465462
// Inline a group with an empty key.
@@ -475,7 +472,7 @@ func (s *handleState) appendAttr(a Attr) {
475472
}
476473
} else {
477474
s.appendKey(a.Key)
478-
s.appendValue(v)
475+
s.appendValue(a.Value)
479476
}
480477
}
481478

slog/handler_test.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,12 +109,10 @@ func TestDefaultHandle(t *testing.T) {
109109
func TestJSONAndTextHandlers(t *testing.T) {
110110
ctx := context.Background()
111111

112-
// ReplaceAttr functions
113-
114112
// remove all Attrs
115113
removeAll := func(_ []string, a Attr) Attr { return Attr{} }
116114

117-
attrs := []Attr{String("a", "one"), Int("b", 2), Any("", "ignore me")}
115+
attrs := []Attr{String("a", "one"), Int("b", 2), Any("", nil)}
118116
preAttrs := []Attr{Int("pre", 3), String("x", "y")}
119117

120118
for _, test := range []struct {
@@ -132,6 +130,12 @@ func TestJSONAndTextHandlers(t *testing.T) {
132130
wantText: "time=2000-01-02T03:04:05.000Z level=INFO msg=message a=one b=2",
133131
wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","a":"one","b":2}`,
134132
},
133+
{
134+
name: "empty key",
135+
attrs: append(slices.Clip(attrs), Any("", "v")),
136+
wantText: `time=2000-01-02T03:04:05.000Z level=INFO msg=message a=one b=2 ""=v`,
137+
wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","a":"one","b":2,"":"v"}`,
138+
},
135139
{
136140
name: "cap keys",
137141
replace: upperCaseKey,
@@ -231,6 +235,16 @@ func TestJSONAndTextHandlers(t *testing.T) {
231235
wantText: "msg=message a=1 name.first=Ren name.last=Hoek b=2",
232236
wantJSON: `{"msg":"message","a":1,"name":{"first":"Ren","last":"Hoek"},"b":2}`,
233237
},
238+
{
239+
// Test resolution when there is no ReplaceAttr function.
240+
name: "resolve",
241+
attrs: []Attr{
242+
Any("", &replace{Value{}}), // should be elided
243+
Any("name", logValueName{"Ren", "Hoek"}),
244+
},
245+
wantText: "time=2000-01-02T03:04:05.000Z level=INFO msg=message name.first=Ren name.last=Hoek",
246+
wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","name":{"first":"Ren","last":"Hoek"}}`,
247+
},
234248
{
235249
name: "with-group",
236250
replace: removeKeys(TimeKey, LevelKey),
@@ -297,7 +311,7 @@ func TestJSONAndTextHandlers(t *testing.T) {
297311
wantJSON: `{"msg":"message","a":1,"b":2,"c":3,"d":4}`,
298312
},
299313
} {
300-
r := NewRecord(testTime, LevelInfo, "message", 1)
314+
r := NewRecord(testTime, LevelInfo, "message", 0)
301315
r.AddAttrs(test.attrs...)
302316
var buf bytes.Buffer
303317
opts := HandlerOptions{ReplaceAttr: test.replace}

slog/internal/testutil/testutil.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import "golang.org/x/exp/slog"
1212
// to make example output deterministic.
1313
func RemoveTime(groups []string, a slog.Attr) slog.Attr {
1414
if a.Key == slog.TimeKey && len(groups) == 0 {
15-
a.Key = ""
15+
return slog.Attr{}
1616
}
1717
return a
1818
}

slog/json_handler.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ func (h *JSONHandler) WithGroup(name string) Handler {
8080
// Values are formatted as with encoding/json.Marshal, with the following
8181
// exceptions:
8282
// - Floating-point NaNs and infinities are formatted as one of the strings
83-
// "NaN", "+Inf" or "-Inf".
83+
// "NaN", "Infinity" or "-Infinity".
8484
// - Levels are formatted as with Level.String.
8585
// - HTML characters are not escaped.
8686
//
@@ -114,9 +114,9 @@ func appendJSONValue(s *handleState, v Value) error {
114114
// json.Marshal fails on special floats, so handle them here.
115115
switch {
116116
case math.IsInf(f, 1):
117-
s.buf.WriteString(`"+Inf"`)
117+
s.buf.WriteString(`"Infinity"`)
118118
case math.IsInf(f, -1):
119-
s.buf.WriteString(`"-Inf"`)
119+
s.buf.WriteString(`"-Infinity"`)
120120
case math.IsNaN(f):
121121
s.buf.WriteString(`"NaN"`)
122122
default:
@@ -136,13 +136,14 @@ func appendJSONValue(s *handleState, v Value) error {
136136
s.appendTime(v.Time())
137137
case KindAny:
138138
a := v.Any()
139-
if err, ok := a.(error); ok {
139+
_, jm := a.(json.Marshaler)
140+
if err, ok := a.(error); ok && !jm {
140141
s.appendString(err.Error())
141142
} else {
142143
return appendJSONMarshal(s.buf, a)
143144
}
144145
default:
145-
panic(fmt.Sprintf("bad kind: %d", v.Kind()))
146+
panic(fmt.Sprintf("bad kind: %s", v.Kind()))
146147
}
147148
return nil
148149
}

slog/json_handler_test.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"io"
1414
"math"
1515
"os"
16+
"path/filepath"
1617
"strings"
1718
"testing"
1819
"time"
@@ -67,6 +68,12 @@ func (j jsonMarshaler) MarshalJSON() ([]byte, error) {
6768
return []byte(fmt.Sprintf(`[%q]`, j.s)), nil
6869
}
6970

71+
type jsonMarshalerError struct {
72+
jsonMarshaler
73+
}
74+
75+
func (jsonMarshalerError) Error() string { return "oops" }
76+
7077
func TestAppendJSONValue(t *testing.T) {
7178
// On most values, jsonAppendAttrValue should agree with json.Marshal.
7279
for _, value := range []any{
@@ -82,6 +89,7 @@ func TestAppendJSONValue(t *testing.T) {
8289
time.Minute,
8390
testTime,
8491
jsonMarshaler{"xyz"},
92+
jsonMarshalerError{jsonMarshaler{"pqr"}},
8593
} {
8694
got := jsonValueString(t, AnyValue(value))
8795
want, err := marshalJSON(value)
@@ -111,8 +119,8 @@ func TestJSONAppendAttrValueSpecial(t *testing.T) {
111119
want string
112120
}{
113121
{math.NaN(), `"NaN"`},
114-
{math.Inf(+1), `"+Inf"`},
115-
{math.Inf(-1), `"-Inf"`},
122+
{math.Inf(+1), `"Infinity"`},
123+
{math.Inf(-1), `"-Infinity"`},
116124
{LevelWarn, `"WARN"`},
117125
} {
118126
got := jsonValueString(t, AnyValue(test.value))
@@ -202,7 +210,7 @@ func BenchmarkPreformatting(b *testing.B) {
202210
}),
203211
}
204212

205-
outFile, err := os.Create("/tmp/bench.log")
213+
outFile, err := os.Create(filepath.Join(b.TempDir(), "bench.log"))
206214
if err != nil {
207215
b.Fatal(err)
208216
}

slog/logger.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ func (l *Logger) clone() *Logger {
8989
func (l *Logger) Handler() Handler { return l.handler }
9090

9191
// With returns a new Logger that includes the given arguments, converted to
92-
// Attrs as in [Logger.Log] and resolved.
92+
// Attrs as in [Logger.Log].
9393
// The Attrs will be added to each output from the Logger.
9494
// The new Logger shares the old Logger's context.
9595
// The new Logger's handler is the result of calling WithAttrs on the receiver's
@@ -110,6 +110,8 @@ func (l *Logger) With(args ...any) *Logger {
110110

111111
// WithGroup returns a new Logger that starts a group. The keys of all
112112
// attributes added to the Logger will be qualified by the given name.
113+
// (How that qualification happens depends on the [Handler.WithGroup]
114+
// method of the Logger's Handler.)
113115
// The new Logger shares the old Logger's context.
114116
//
115117
// The new Logger's handler is the result of calling WithGroup on the receiver's

slog/logger_test.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"regexp"
1414
"runtime"
1515
"strings"
16+
"sync"
1617
"testing"
1718
"time"
1819

@@ -398,27 +399,38 @@ func clean(s string) string {
398399
}
399400

400401
type captureHandler struct {
402+
mu sync.Mutex
401403
r Record
402404
attrs []Attr
403405
groups []string
404406
}
405407

406408
func (h *captureHandler) Handle(ctx context.Context, r Record) error {
409+
h.mu.Lock()
410+
defer h.mu.Unlock()
407411
h.r = r
408412
return nil
409413
}
410414

411415
func (*captureHandler) Enabled(context.Context, Level) bool { return true }
412416

413417
func (c *captureHandler) WithAttrs(as []Attr) Handler {
414-
c2 := *c
415-
c2.attrs = concat(c2.attrs, as)
418+
c.mu.Lock()
419+
defer c.mu.Unlock()
420+
var c2 captureHandler
421+
c2.r = c.r
422+
c2.groups = c.groups
423+
c2.attrs = concat(c.attrs, as)
416424
return &c2
417425
}
418426

419427
func (c *captureHandler) WithGroup(name string) Handler {
420-
c2 := *c
421-
c2.groups = append(slices.Clip(c2.groups), name)
428+
c.mu.Lock()
429+
defer c.mu.Unlock()
430+
var c2 captureHandler
431+
c2.r = c.r
432+
c2.attrs = c.attrs
433+
c2.groups = append(slices.Clip(c.groups), name)
422434
return &c2
423435
}
424436

0 commit comments

Comments
 (0)