Skip to content

Commit d3d78b4

Browse files
committed
log/slog: handle recursively empty groups
Handlers should not display empty groups. A group with no attributes is certainly empty. But we also want to consider a group to be empty if all its attributes are empty groups. The built-in handlers did not handle this second case properly. This CL fixes that. There are two places in the implementation that we need to consider. For Values of KindGroup, we change the GroupValue constructor to omit Attrs that are empty groups. A Group is then empty if and only if it has no Attrs. This avoids a recursive check for emptiness. It does require allocation, but that doesn't worry us because Group values should be relatively rare. For groups established by WithGroup, we avoid opening such groups unless the Record contains non-empty groups. As we did for values, we avoid adding empty groups to records in the first place, so we only need to check that the record has at least one Attr. We are doing extra work, so we need to make sure we aren't slowing things down unduly. Benchmarks before and after this change show minimal differences. Fixes #61067. Change-Id: I684c7ca834bbf69210516faecae04ee548846fb7 Reviewed-on: https://go-review.googlesource.com/c/go/+/508436 Run-TryBot: Jonathan Amsterdam <[email protected]> Reviewed-by: Alan Donovan <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent 39c5070 commit d3d78b4

File tree

5 files changed

+134
-10
lines changed

5 files changed

+134
-10
lines changed

src/log/slog/handler.go

+17-7
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,11 @@ func (h *commonHandler) enabled(l Level) bool {
221221
}
222222

223223
func (h *commonHandler) withAttrs(as []Attr) *commonHandler {
224+
// We are going to ignore empty groups, so if the entire slice consists of
225+
// them, there is nothing to do.
226+
if countEmptyGroups(as) == len(as) {
227+
return h
228+
}
224229
h2 := h.clone()
225230
// Pre-format the attributes as an optimization.
226231
state := h2.newHandleState((*buffer.Buffer)(&h2.preformattedAttrs), false, "")
@@ -308,15 +313,20 @@ func (s *handleState) appendNonBuiltIns(r Record) {
308313
}
309314
// Attrs in Record -- unlike the built-in ones, they are in groups started
310315
// from WithGroup.
311-
s.prefix.WriteString(s.h.groupPrefix)
312-
s.openGroups()
313-
r.Attrs(func(a Attr) bool {
314-
s.appendAttr(a)
315-
return true
316-
})
316+
// If the record has no Attrs, don't output any groups.
317+
nOpenGroups := s.h.nOpenGroups
318+
if r.NumAttrs() > 0 {
319+
s.prefix.WriteString(s.h.groupPrefix)
320+
s.openGroups()
321+
nOpenGroups = len(s.h.groups)
322+
r.Attrs(func(a Attr) bool {
323+
s.appendAttr(a)
324+
return true
325+
})
326+
}
317327
if s.h.json {
318328
// Close all open groups.
319-
for range s.h.groups {
329+
for range s.h.groups[:nOpenGroups] {
320330
s.buf.WriteByte('}')
321331
}
322332
// Close the top-level object.

src/log/slog/handler_test.go

+50
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,28 @@ func TestJSONAndTextHandlers(t *testing.T) {
214214
wantText: "msg=message h.a=1",
215215
wantJSON: `{"msg":"message","h":{"a":1}}`,
216216
},
217+
{
218+
name: "nested empty group",
219+
replace: removeKeys(TimeKey, LevelKey),
220+
attrs: []Attr{
221+
Group("g",
222+
Group("h",
223+
Group("i"), Group("j"))),
224+
},
225+
wantText: `msg=message`,
226+
wantJSON: `{"msg":"message"}`,
227+
},
228+
{
229+
name: "nested non-empty group",
230+
replace: removeKeys(TimeKey, LevelKey),
231+
attrs: []Attr{
232+
Group("g",
233+
Group("h",
234+
Group("i"), Group("j", Int("a", 1)))),
235+
},
236+
wantText: `msg=message g.h.j.a=1`,
237+
wantJSON: `{"msg":"message","g":{"h":{"j":{"a":1}}}}`,
238+
},
217239
{
218240
name: "escapes",
219241
replace: removeKeys(TimeKey, LevelKey),
@@ -281,6 +303,34 @@ func TestJSONAndTextHandlers(t *testing.T) {
281303
wantText: "msg=message p1=1 s1.s2.a=one s1.s2.b=2",
282304
wantJSON: `{"msg":"message","p1":1,"s1":{"s2":{"a":"one","b":2}}}`,
283305
},
306+
{
307+
name: "empty with-groups",
308+
replace: removeKeys(TimeKey, LevelKey),
309+
with: func(h Handler) Handler {
310+
return h.WithGroup("x").WithGroup("y")
311+
},
312+
wantText: "msg=message",
313+
wantJSON: `{"msg":"message"}`,
314+
},
315+
{
316+
name: "empty with-groups, no non-empty attrs",
317+
replace: removeKeys(TimeKey, LevelKey),
318+
with: func(h Handler) Handler {
319+
return h.WithGroup("x").WithAttrs([]Attr{Group("g")}).WithGroup("y")
320+
},
321+
wantText: "msg=message",
322+
wantJSON: `{"msg":"message"}`,
323+
},
324+
{
325+
name: "one empty with-group",
326+
replace: removeKeys(TimeKey, LevelKey),
327+
with: func(h Handler) Handler {
328+
return h.WithGroup("x").WithAttrs([]Attr{Int("a", 1)}).WithGroup("y")
329+
},
330+
attrs: []Attr{Group("g", Group("h"))},
331+
wantText: "msg=message x.a=1",
332+
wantJSON: `{"msg":"message","x":{"a":1}}`,
333+
},
284334
{
285335
name: "GroupValue as Attr value",
286336
replace: removeKeys(TimeKey, LevelKey),

src/log/slog/record.go

+21-3
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,17 @@ func (r Record) Attrs(f func(Attr) bool) {
9393
}
9494

9595
// AddAttrs appends the given Attrs to the Record's list of Attrs.
96+
// It omits empty groups.
9697
func (r *Record) AddAttrs(attrs ...Attr) {
97-
n := copy(r.front[r.nFront:], attrs)
98-
r.nFront += n
98+
var i int
99+
for i = 0; i < len(attrs) && r.nFront < len(r.front); i++ {
100+
a := attrs[i]
101+
if a.Value.isEmptyGroup() {
102+
continue
103+
}
104+
r.front[r.nFront] = a
105+
r.nFront++
106+
}
99107
// Check if a copy was modified by slicing past the end
100108
// and seeing if the Attr there is non-zero.
101109
if cap(r.back) > len(r.back) {
@@ -104,15 +112,25 @@ func (r *Record) AddAttrs(attrs ...Attr) {
104112
panic("copies of a slog.Record were both modified")
105113
}
106114
}
107-
r.back = append(r.back, attrs[n:]...)
115+
ne := countEmptyGroups(attrs[i:])
116+
r.back = slices.Grow(r.back, len(attrs[i:])-ne)
117+
for _, a := range attrs[i:] {
118+
if !a.Value.isEmptyGroup() {
119+
r.back = append(r.back, a)
120+
}
121+
}
108122
}
109123

110124
// Add converts the args to Attrs as described in [Logger.Log],
111125
// then appends the Attrs to the Record's list of Attrs.
126+
// It omits empty groups.
112127
func (r *Record) Add(args ...any) {
113128
var a Attr
114129
for len(args) > 0 {
115130
a, args = argsToAttr(args)
131+
if a.Value.isEmptyGroup() {
132+
continue
133+
}
116134
if r.nFront < len(r.front) {
117135
r.front[r.nFront] = a
118136
r.nFront++

src/log/slog/value.go

+34
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,32 @@ func DurationValue(v time.Duration) Value {
164164
// GroupValue returns a new Value for a list of Attrs.
165165
// The caller must not subsequently mutate the argument slice.
166166
func GroupValue(as ...Attr) Value {
167+
// Remove empty groups.
168+
// It is simpler overall to do this at construction than
169+
// to check each Group recursively for emptiness.
170+
if n := countEmptyGroups(as); n > 0 {
171+
as2 := make([]Attr, 0, len(as)-n)
172+
for _, a := range as {
173+
if !a.Value.isEmptyGroup() {
174+
as2 = append(as2, a)
175+
}
176+
}
177+
as = as2
178+
}
167179
return Value{num: uint64(len(as)), any: groupptr(unsafe.SliceData(as))}
168180
}
169181

182+
// countEmptyGroups returns the number of empty group values in its argument.
183+
func countEmptyGroups(as []Attr) int {
184+
n := 0
185+
for _, a := range as {
186+
if a.Value.isEmptyGroup() {
187+
n++
188+
}
189+
}
190+
return n
191+
}
192+
170193
// AnyValue returns a Value for the supplied value.
171194
//
172195
// If the supplied value is of type Value, it is returned
@@ -399,6 +422,17 @@ func (v Value) Equal(w Value) bool {
399422
}
400423
}
401424

425+
// isEmptyGroup reports whether v is a group that has no attributes.
426+
func (v Value) isEmptyGroup() bool {
427+
if v.Kind() != KindGroup {
428+
return false
429+
}
430+
// We do not need to recursively examine the group's Attrs for emptiness,
431+
// because GroupValue removed them when the group was constructed, and
432+
// groups are immutable.
433+
return len(v.group()) == 0
434+
}
435+
402436
// append appends a text representation of v to dst.
403437
// v is formatted as with fmt.Sprint.
404438
func (v Value) append(dst []byte) []byte {

src/log/slog/value_test.go

+12
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,18 @@ func TestZeroTime(t *testing.T) {
229229
}
230230
}
231231

232+
func TestEmptyGroup(t *testing.T) {
233+
g := GroupValue(
234+
Int("a", 1),
235+
Group("g1", Group("g2")),
236+
Group("g3", Group("g4", Int("b", 2))))
237+
got := g.Group()
238+
want := []Attr{Int("a", 1), Group("g3", Group("g4", Int("b", 2)))}
239+
if !attrsEqual(got, want) {
240+
t.Errorf("\ngot %v\nwant %v", got, want)
241+
}
242+
}
243+
232244
type replace struct {
233245
v Value
234246
}

0 commit comments

Comments
 (0)