1
0
mirror of https://github.com/golang/go synced 2024-11-17 06:24:48 -07:00

log/slog: require entire Attr to be empty to elide

Specify that Handlers should ignore zero-valued Attrs.

Implement that policy in the built-in handlers.

Fixes #59282.

Change-Id: I4430686b61f49bdac849ee300daaabfac9895849
Reviewed-on: https://go-review.googlesource.com/c/go/+/484095
Run-TryBot: Jonathan Amsterdam <jba@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Alan Donovan <adonovan@google.com>
This commit is contained in:
Jonathan Amsterdam 2023-03-27 09:27:43 -04:00
parent b3a194aad6
commit d528f72d4c
8 changed files with 28 additions and 24 deletions

View File

@ -83,6 +83,8 @@ func (a Attr) String() string {
return fmt.Sprintf("%s=%s", a.Key, a.Value) return fmt.Sprintf("%s=%s", a.Key, a.Value)
} }
// isEmpty reports whether a has an empty key and a nil value.
// That can be written as Attr{} or Any("", nil).
func (a Attr) isEmpty() bool { func (a Attr) isEmpty() bool {
return a.Key == "" && a.Value.num == 0 && a.Value.any == nil return a.Key == "" && a.Value.num == 0 && a.Value.any == nil
} }

View File

@ -30,7 +30,7 @@ func Example_wrapping() {
replace := func(groups []string, a slog.Attr) slog.Attr { replace := func(groups []string, a slog.Attr) slog.Attr {
// Remove time. // Remove time.
if a.Key == slog.TimeKey && len(groups) == 0 { if a.Key == slog.TimeKey && len(groups) == 0 {
a.Key = "" return slog.Attr{}
} }
// Remove the directory from the source's filename. // Remove the directory from the source's filename.
if a.Key == slog.SourceKey { if a.Key == slog.SourceKey {

View File

@ -50,8 +50,8 @@ type Handler interface {
// Handle methods that produce output should observe the following rules: // Handle methods that produce output should observe the following rules:
// - If r.Time is the zero time, ignore the time. // - If r.Time is the zero time, ignore the time.
// - If r.PC is zero, ignore it. // - If r.PC is zero, ignore it.
// - If an Attr's key is the empty string and the value is not a group, // - If an Attr's key and value are both the zero value, ignore the Attr.
// ignore the Attr. // This can be tested with attr.Equal(Attr{}).
// - If a group's key is empty, inline the group's Attrs. // - If a group's key is empty, inline the group's Attrs.
// - If a group has no Attrs (even if it has a non-empty key), // - If a group has no Attrs (even if it has a non-empty key),
// ignore it. // ignore it.
@ -437,26 +437,22 @@ func (s *handleState) closeGroup(name string) {
// It handles replacement and checking for an empty key. // It handles replacement and checking for an empty key.
// after replacement). // after replacement).
func (s *handleState) appendAttr(a Attr) { func (s *handleState) appendAttr(a Attr) {
v := a.Value if rep := s.h.opts.ReplaceAttr; rep != nil && a.Value.Kind() != KindGroup {
// Elide a non-group with an empty key.
if a.Key == "" && v.Kind() != KindGroup {
return
}
if rep := s.h.opts.ReplaceAttr; rep != nil && v.Kind() != KindGroup {
var gs []string var gs []string
if s.groups != nil { if s.groups != nil {
gs = *s.groups gs = *s.groups
} }
a = rep(gs, Attr{a.Key, v}) a = rep(gs, a)
if a.Key == "" {
return
}
// Although all attributes in the Record are already resolved, // Although all attributes in the Record are already resolved,
// This one came from the user, so it may not have been. // This one came from the user, so it may not have been.
v = a.Value.Resolve() a.Value = a.Value.Resolve()
} }
if v.Kind() == KindGroup { // Elide empty Attrs.
attrs := v.Group() if a.isEmpty() {
return
}
if a.Value.Kind() == KindGroup {
attrs := a.Value.Group()
// Output only non-empty groups. // Output only non-empty groups.
if len(attrs) > 0 { if len(attrs) > 0 {
// Inline a group with an empty key. // Inline a group with an empty key.
@ -472,7 +468,7 @@ func (s *handleState) appendAttr(a Attr) {
} }
} else { } else {
s.appendKey(a.Key) s.appendKey(a.Key)
s.appendValue(v) s.appendValue(a.Value)
} }
} }

View File

@ -108,12 +108,10 @@ func TestDefaultHandle(t *testing.T) {
func TestJSONAndTextHandlers(t *testing.T) { func TestJSONAndTextHandlers(t *testing.T) {
ctx := context.Background() ctx := context.Background()
// ReplaceAttr functions
// remove all Attrs // remove all Attrs
removeAll := func(_ []string, a Attr) Attr { return Attr{} } removeAll := func(_ []string, a Attr) Attr { return Attr{} }
attrs := []Attr{String("a", "one"), Int("b", 2), Any("", "ignore me")} attrs := []Attr{String("a", "one"), Int("b", 2), Any("", nil)}
preAttrs := []Attr{Int("pre", 3), String("x", "y")} preAttrs := []Attr{Int("pre", 3), String("x", "y")}
for _, test := range []struct { for _, test := range []struct {
@ -131,6 +129,12 @@ func TestJSONAndTextHandlers(t *testing.T) {
wantText: "time=2000-01-02T03:04:05.000Z level=INFO msg=message a=one b=2", wantText: "time=2000-01-02T03:04:05.000Z level=INFO msg=message a=one b=2",
wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","a":"one","b":2}`, wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","a":"one","b":2}`,
}, },
{
name: "empty key",
attrs: append(slices.Clip(attrs), Any("", "v")),
wantText: `time=2000-01-02T03:04:05.000Z level=INFO msg=message a=one b=2 ""=v`,
wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","a":"one","b":2,"":"v"}`,
},
{ {
name: "cap keys", name: "cap keys",
replace: upperCaseKey, replace: upperCaseKey,
@ -296,7 +300,7 @@ func TestJSONAndTextHandlers(t *testing.T) {
wantJSON: `{"msg":"message","a":1,"b":2,"c":3,"d":4}`, wantJSON: `{"msg":"message","a":1,"b":2,"c":3,"d":4}`,
}, },
} { } {
r := NewRecord(testTime, LevelInfo, "message", 1) r := NewRecord(testTime, LevelInfo, "message", 0)
r.AddAttrs(test.attrs...) r.AddAttrs(test.attrs...)
var buf bytes.Buffer var buf bytes.Buffer
opts := HandlerOptions{ReplaceAttr: test.replace} opts := HandlerOptions{ReplaceAttr: test.replace}

View File

@ -12,7 +12,7 @@ import "log/slog"
// to make example output deterministic. // to make example output deterministic.
func RemoveTime(groups []string, a slog.Attr) slog.Attr { func RemoveTime(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey && len(groups) == 0 { if a.Key == slog.TimeKey && len(groups) == 0 {
a.Key = "" return slog.Attr{}
} }
return a return a
} }

View File

@ -138,6 +138,9 @@ func byteSlice(a any) ([]byte, bool) {
} }
func needsQuoting(s string) bool { func needsQuoting(s string) bool {
if len(s) == 0 {
return true
}
for i := 0; i < len(s); { for i := 0; i < len(s); {
b := s[i] b := s[i]
if b < utf8.RuneSelf { if b < utf8.RuneSelf {

View File

@ -185,7 +185,7 @@ func TestNeedsQuoting(t *testing.T) {
in string in string
want bool want bool
}{ }{
{"", false}, {"", true},
{"ab", false}, {"ab", false},
{"a=b", true}, {"a=b", true},
{`"ab"`, true}, {`"ab"`, true},

View File

@ -185,7 +185,6 @@ func TestLogValue(t *testing.T) {
if !attrsEqual(got2, want2) { if !attrsEqual(got2, want2) {
t.Errorf("got %v, want %v", got2, want2) t.Errorf("got %v, want %v", got2, want2)
} }
} }
func TestZeroTime(t *testing.T) { func TestZeroTime(t *testing.T) {