Skip to content

Commit 439092d

Browse files
committed
slog: add Level.MarshalJSON
Add a MarshalJSON method to Level so that it marshals as a string. This lets us remove special cases for Level in the handlers. It also ensures that others who write handlers that output JSON will get strings instead of numbers; the numbers are obscure, uninformative, and could be confused with the values of other level-numbering schemes. Also, improve handler tests to check replacement. Change-Id: Iedc7aa797469922fb9cfcc3b5d804806f3deb5d8 Reviewed-on: https://go-review.googlesource.com/c/exp/+/432183 Run-TryBot: Jonathan Amsterdam <[email protected]> Reviewed-by: Alan Donovan <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent 669c812 commit 439092d

File tree

6 files changed

+91
-54
lines changed

6 files changed

+91
-54
lines changed

slog/handler.go

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,9 @@ func (h *commonHandler) handle(r Record) error {
138138
a = rep(a)
139139
if a.Key() != "" {
140140
app.appendKey(a.Key())
141-
app.appendAttrValue(a)
141+
if err := app.appendAttrValue(a); err != nil {
142+
app.appendString(fmt.Sprintf("!ERROR:%v", err))
143+
}
142144
}
143145
}
144146

@@ -163,17 +165,7 @@ func (h *commonHandler) handle(r Record) error {
163165
app.appendKey(key)
164166
app.appendString(val.String())
165167
} else {
166-
// Don't use replace: we want to output Levels as strings
167-
// to match the above.
168-
a := rep(Any(key, val))
169-
if a.Key() != "" {
170-
app.appendKey(a.Key())
171-
if l, ok := a.any.(Level); ok {
172-
app.appendString(l.String())
173-
} else {
174-
app.appendAttrValue(a)
175-
}
176-
}
168+
replace(Any(key, val))
177169
}
178170
app.appendSep()
179171
}

slog/handler_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,7 @@ func TestCommonHandle(t *testing.T) {
5151
{
5252
name: "cap keys",
5353
h: &commonHandler{
54-
opts: HandlerOptions{
55-
ReplaceAttr: func(a Attr) Attr {
56-
return a.WithKey(strings.ToUpper(a.Key()))
57-
},
58-
},
54+
opts: HandlerOptions{ReplaceAttr: upperCaseKey},
5955
},
6056
want: "(TIME=2022-09-18T08:26:33.000Z;LEVEL=INFO;MSG=message;A=one;B=2)",
6157
},
@@ -87,6 +83,10 @@ func TestCommonHandle(t *testing.T) {
8783
}
8884
}
8985

86+
func upperCaseKey(a Attr) Attr {
87+
return a.WithKey(strings.ToUpper(a.Key()))
88+
}
89+
9090
type testAppender struct {
9191
buf *buffer.Buffer
9292
}

slog/json_handler.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,10 +149,7 @@ func (ap *jsonAppender) appendAttrValue(a Attr) error {
149149
return err
150150
}
151151
case AnyKind:
152-
v := a.Value()
153-
if l, ok := v.(Level); ok {
154-
ap.appendString(l.String())
155-
} else if err := ap.appendJSONMarshal(v); err != nil {
152+
if err := ap.appendJSONMarshal(a.Value()); err != nil {
156153
return err
157154
}
158155
default:

slog/json_handler_test.go

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,43 @@ import (
1212
"io"
1313
"math"
1414
"os"
15+
"strings"
1516
"testing"
1617
"time"
1718

1819
"golang.org/x/exp/slog/internal/buffer"
1920
)
2021

2122
func TestJSONHandler(t *testing.T) {
22-
var buf bytes.Buffer
23-
h := NewJSONHandler(&buf)
24-
r := NewRecord(testTime, InfoLevel, "m", 0)
25-
r.AddAttrs(Int("a", 1))
26-
if err := h.Handle(r); err != nil {
27-
t.Fatal(err)
28-
}
29-
got := buf.String()
30-
got = got[:len(got)-1] // Remove final newline.
31-
want := `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"m","a":1}`
32-
if got != want {
33-
t.Errorf("\ngot %s\nwant %s", got, want)
23+
for _, test := range []struct {
24+
name string
25+
opts HandlerOptions
26+
want string
27+
}{
28+
{
29+
"none",
30+
HandlerOptions{},
31+
`{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"m","a":1,"m":{"b":2}}`,
32+
},
33+
{
34+
"replace",
35+
HandlerOptions{ReplaceAttr: upperCaseKey},
36+
`{"TIME":"2000-01-02T03:04:05Z","LEVEL":"INFO","MSG":"m","A":1,"M":{"b":2}}`,
37+
},
38+
} {
39+
t.Run(test.name, func(t *testing.T) {
40+
var buf bytes.Buffer
41+
h := test.opts.NewJSONHandler(&buf)
42+
r := NewRecord(testTime, InfoLevel, "m", 0)
43+
r.AddAttrs(Int("a", 1), Any("m", map[string]int{"b": 2}))
44+
if err := h.Handle(r); err != nil {
45+
t.Fatal(err)
46+
}
47+
got := strings.TrimSuffix(buf.String(), "\n")
48+
if got != test.want {
49+
t.Errorf("\ngot %s\nwant %s", got, test.want)
50+
}
51+
})
3452
}
3553
}
3654

slog/level.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package slog
77
import (
88
"fmt"
99
"math"
10+
"strconv"
1011
"sync/atomic"
1112
)
1213

@@ -78,6 +79,13 @@ func (l Level) String() string {
7879
}
7980
}
8081

82+
func (l Level) MarshalJSON() ([]byte, error) {
83+
// AppendQuote is sufficient for JSON-encoding all Level strings.
84+
// They don't contain any runes that would produce invalid JSON
85+
// when escaped.
86+
return strconv.AppendQuote(nil, l.String()), nil
87+
}
88+
8189
// An AtomicLevel is Level that can be read and written safely by multiple
8290
// goroutines.
8391
// Use NewAtomicLevel to create one.

slog/text_handler_test.go

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,50 +21,72 @@ var testTime = time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC)
2121

2222
func TestTextHandler(t *testing.T) {
2323
for _, test := range []struct {
24-
name string
25-
attr Attr
26-
want string
24+
name string
25+
attr Attr
26+
wantKey, wantVal string
2727
}{
2828
{
2929
"unquoted",
3030
Int("a", 1),
31-
"a=1",
31+
"a", "1",
3232
},
3333
{
3434
"quoted",
3535
String("x = y", `qu"o`),
36-
`"x = y"="qu\"o"`,
36+
`"x = y"`, `"qu\"o"`,
3737
},
3838
{
3939
"Sprint",
4040
Any("name", name{"Ren", "Hoek"}),
41-
`name="Hoek, Ren"`,
41+
`name`, `"Hoek, Ren"`,
4242
},
4343
{
4444
"TextMarshaler",
4545
Any("t", text{"abc"}),
46-
`t="text{\"abc\"}"`,
46+
`t`, `"text{\"abc\"}"`,
4747
},
4848
{
4949
"TextMarshaler error",
5050
Any("t", text{""}),
51-
`t="!ERROR:text: empty string"`,
51+
`t`, `"!ERROR:text: empty string"`,
5252
},
5353
} {
5454
t.Run(test.name, func(t *testing.T) {
55-
var buf bytes.Buffer
56-
h := NewTextHandler(&buf)
57-
r := NewRecord(testTime, InfoLevel, "a message", 0)
58-
r.AddAttrs(test.attr)
59-
if err := h.Handle(r); err != nil {
60-
t.Fatal(err)
61-
}
62-
got := buf.String()
63-
// Remove final newline.
64-
got = got[:len(got)-1]
65-
want := `time=2000-01-02T03:04:05.000Z level=INFO msg="a message" ` + test.want
66-
if got != want {
67-
t.Errorf("\ngot %s\nwant %s", got, want)
55+
for _, opts := range []struct {
56+
name string
57+
opts HandlerOptions
58+
wantPrefix string
59+
modKey func(string) string
60+
}{
61+
{
62+
"none",
63+
HandlerOptions{},
64+
`time=2000-01-02T03:04:05.000Z level=INFO msg="a message"`,
65+
func(s string) string { return s },
66+
},
67+
{
68+
"replace",
69+
HandlerOptions{ReplaceAttr: upperCaseKey},
70+
`TIME=2000-01-02T03:04:05.000Z LEVEL=INFO MSG="a message"`,
71+
strings.ToUpper,
72+
},
73+
} {
74+
t.Run(opts.name, func(t *testing.T) {
75+
var buf bytes.Buffer
76+
h := opts.opts.NewTextHandler(&buf)
77+
r := NewRecord(testTime, InfoLevel, "a message", 0)
78+
r.AddAttrs(test.attr)
79+
if err := h.Handle(r); err != nil {
80+
t.Fatal(err)
81+
}
82+
got := buf.String()
83+
// Remove final newline.
84+
got = got[:len(got)-1]
85+
want := opts.wantPrefix + " " + opts.modKey(test.wantKey) + "=" + test.wantVal
86+
if got != want {
87+
t.Errorf("\ngot %s\nwant %s", got, want)
88+
}
89+
})
6890
}
6991
})
7092
}

0 commit comments

Comments
 (0)