Compare commits
2 Commits
0ebae4b70a
...
430bd76230
| Author | SHA1 | Date | |
|---|---|---|---|
| 430bd76230 | |||
| 3dbe6954d7 |
@@ -91,8 +91,11 @@ it.
|
|||||||
The record's own fields are named `Time`, `Level`, `Message` and `PC`, and
|
The record's own fields are named `Time`, `Level`, `Message` and `PC`, and
|
||||||
they own those names: an attribute keyed after one of them is dropped from
|
they own those names: an attribute keyed after one of them is dropped from
|
||||||
the json and webhook output. A key logged more than once keeps its last
|
the json and webhook output. A key logged more than once keeps its last
|
||||||
value there for the same reason. Neither applies to the console output,
|
value there for the same reason, with one exception: when both are
|
||||||
which is a line of text: both pairs appear, in order. If you need a field
|
`slog.Group` values sharing a key, the two groups are merged into one
|
||||||
|
object holding the members of both, rather than the second replacing the
|
||||||
|
first. Neither the collision nor the merge applies to the console output,
|
||||||
|
which is a line of text: every pair appears, in order. If you need a field
|
||||||
called `message`, pick a key that does not collide - the collision is
|
called `message`, pick a key that does not collide - the collision is
|
||||||
silent.
|
silent.
|
||||||
|
|
||||||
|
|||||||
22
attrs.go
22
attrs.go
@@ -229,23 +229,27 @@ func appendAttrText(out *strings.Builder, prefix string, attr slog.Attr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
out.WriteString(" ")
|
out.WriteString(" ")
|
||||||
out.WriteString(prefix)
|
// The key is quoted on the same terms as the value, and as a whole
|
||||||
out.WriteString(attr.Key)
|
// including its group prefix, because that is the token a reader has to
|
||||||
|
// find the "=" in. slog.NewTextHandler quotes "prefix+key" the same way,
|
||||||
|
// so a key like "a=b" reads as "a=b"=v2 rather than the ambiguous
|
||||||
|
// a=b=v2, which parses as the key "a" with the value "b=v2".
|
||||||
|
out.WriteString(quoteIfNeeded(prefix + attr.Key))
|
||||||
out.WriteString("=")
|
out.WriteString("=")
|
||||||
out.WriteString(quoteIfNeeded(value.String()))
|
out.WriteString(quoteIfNeeded(value.String()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// quoteIfNeeded quotes a value only when leaving it bare would make the
|
// quoteIfNeeded quotes a key or a value only when leaving it bare would make
|
||||||
// key=value pairs ambiguous, matching how the stdlib text handler reads.
|
// the key=value pairs ambiguous, matching how the stdlib text handler reads.
|
||||||
func quoteIfNeeded(value string) string {
|
func quoteIfNeeded(text string) string {
|
||||||
if value == "" {
|
if text == "" {
|
||||||
return `""`
|
return `""`
|
||||||
}
|
}
|
||||||
for _, r := range value {
|
for _, r := range text {
|
||||||
if unicode.IsSpace(r) || !unicode.IsPrint(r) ||
|
if unicode.IsSpace(r) || !unicode.IsPrint(r) ||
|
||||||
r == '"' || r == '=' {
|
r == '"' || r == '=' {
|
||||||
return strconv.Quote(value)
|
return strconv.Quote(text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return value
|
return text
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -297,6 +297,97 @@ func TestConsoleHandlerQuotesValuesNeedingIt(t *testing.T) {
|
|||||||
wantContains(t, output, `file="The Movie.mp4"`)
|
wantContains(t, output, `file="The Movie.mp4"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestConsoleHandlerQuotesKeysNeedingIt pins the key side of the same rule.
|
||||||
|
// A key is quoted on the same terms as a value, and as one token including
|
||||||
|
// its group prefix, so that the "=" separating the pair is always the first
|
||||||
|
// one outside quotes. Every case here was compared against
|
||||||
|
// slog.NewTextHandler, which renders each of them identically.
|
||||||
|
func TestConsoleHandlerQuotesKeysNeedingIt(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
build func() slog.Handler
|
||||||
|
attrs []slog.Attr
|
||||||
|
want string
|
||||||
|
notWant string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "an ordinary key is left bare",
|
||||||
|
build: func() slog.Handler { return NewConsoleHandler() },
|
||||||
|
attrs: []slog.Attr{slog.String("device", "livingroom")},
|
||||||
|
want: " device=livingroom",
|
||||||
|
notWant: `"device"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "a key containing an equals sign is quoted",
|
||||||
|
build: func() slog.Handler { return NewConsoleHandler() },
|
||||||
|
attrs: []slog.Attr{slog.String("a=b", "v2")},
|
||||||
|
want: ` "a=b"=v2`,
|
||||||
|
notWant: " a=b=v2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "a key containing a space is quoted",
|
||||||
|
build: func() slog.Handler { return NewConsoleHandler() },
|
||||||
|
attrs: []slog.Attr{slog.String("my key", "v")},
|
||||||
|
want: ` "my key"=v`,
|
||||||
|
notWant: " my key=v",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "a key containing a quote is escaped",
|
||||||
|
build: func() slog.Handler { return NewConsoleHandler() },
|
||||||
|
attrs: []slog.Attr{slog.String(`he"llo`, "v")},
|
||||||
|
want: ` "he\"llo"=v`,
|
||||||
|
notWant: ` he"llo=v`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "a printable non-ascii key is left bare",
|
||||||
|
build: func() slog.Handler { return NewConsoleHandler() },
|
||||||
|
attrs: []slog.Attr{slog.String("キー", "v")},
|
||||||
|
want: " キー=v",
|
||||||
|
notWant: `"キー"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "a group prefix is quoted together with its key",
|
||||||
|
build: func() slog.Handler { return NewConsoleHandler() },
|
||||||
|
attrs: []slog.Attr{
|
||||||
|
slog.Group("grp", slog.String("a=b", "v")),
|
||||||
|
},
|
||||||
|
want: ` "grp.a=b"=v`,
|
||||||
|
notWant: " grp.a=b=v",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "a WithGroup prefix needing quotes quotes the whole key",
|
||||||
|
build: func() slog.Handler {
|
||||||
|
return NewConsoleHandler().WithGroup("my grp")
|
||||||
|
},
|
||||||
|
attrs: []slog.Attr{slog.String("k", "v")},
|
||||||
|
want: ` "my grp.k"=v`,
|
||||||
|
notWant: " my grp.k=v",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "an empty key is quoted rather than left as a gap",
|
||||||
|
build: func() slog.Handler { return NewConsoleHandler() },
|
||||||
|
attrs: []slog.Attr{slog.String("", "v")},
|
||||||
|
want: ` ""=v`,
|
||||||
|
notWant: " =v",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
output := captureStdout(t, func() {
|
||||||
|
handler := test.build()
|
||||||
|
record := testRecord("casting", test.attrs...)
|
||||||
|
if err := handler.Handle(context.Background(), record); err != nil {
|
||||||
|
t.Fatalf("Handle: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
wantContains(t, output, test.want)
|
||||||
|
wantNotContains(t, output, test.notWant)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestConsoleHandlerWithAttrsAccumulates(t *testing.T) {
|
func TestConsoleHandlerWithAttrsAccumulates(t *testing.T) {
|
||||||
output := captureStdout(t, func() {
|
output := captureStdout(t, func() {
|
||||||
handler := NewConsoleHandler().
|
handler := NewConsoleHandler().
|
||||||
|
|||||||
Reference in New Issue
Block a user