Source file
src/log/slog/json_handler_test.go
1
2
3
4
5 package slog
6
7 import (
8 "bytes"
9 "context"
10 "encoding/json"
11 "errors"
12 "fmt"
13 "io"
14 "log/slog/internal/buffer"
15 "math"
16 "os"
17 "path/filepath"
18 "strings"
19 "testing"
20 "time"
21 )
22
23 func TestJSONHandler(t *testing.T) {
24 for _, test := range []struct {
25 name string
26 opts HandlerOptions
27 want string
28 }{
29 {
30 "none",
31 HandlerOptions{},
32 `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"m","a":1,"m":{"b":2}}`,
33 },
34 {
35 "replace",
36 HandlerOptions{ReplaceAttr: upperCaseKey},
37 `{"TIME":"2000-01-02T03:04:05Z","LEVEL":"INFO","MSG":"m","A":1,"M":{"b":2}}`,
38 },
39 } {
40 t.Run(test.name, func(t *testing.T) {
41 var buf bytes.Buffer
42 h := NewJSONHandler(&buf, &test.opts)
43 r := NewRecord(testTime, LevelInfo, "m", 0)
44 r.AddAttrs(Int("a", 1), Any("m", map[string]int{"b": 2}))
45 if err := h.Handle(context.Background(), r); err != nil {
46 t.Fatal(err)
47 }
48 got := strings.TrimSuffix(buf.String(), "\n")
49 if got != test.want {
50 t.Errorf("\ngot %s\nwant %s", got, test.want)
51 }
52 })
53 }
54 }
55
56
57 type jsonMarshaler struct {
58 s string
59 }
60
61 func (j jsonMarshaler) String() string { return j.s }
62
63 func (j jsonMarshaler) MarshalJSON() ([]byte, error) {
64 if j.s == "" {
65 return nil, errors.New("json: empty string")
66 }
67 return []byte(fmt.Sprintf(`[%q]`, j.s)), nil
68 }
69
70 type jsonMarshalerError struct {
71 jsonMarshaler
72 }
73
74 func (jsonMarshalerError) Error() string { return "oops" }
75
76 func TestAppendJSONValue(t *testing.T) {
77
78 for _, value := range []any{
79 "hello\r\n\t\a",
80 `"[{escape}]"`,
81 "<escapeHTML&>",
82
83
84 "\u03B8\u2028\u2029\uFFFF\xF6",
85 `-123`,
86 int64(-9_200_123_456_789_123_456),
87 uint64(9_200_123_456_789_123_456),
88 -12.75,
89 1.23e-9,
90 false,
91 time.Minute,
92 testTime,
93 jsonMarshaler{"xyz"},
94 jsonMarshalerError{jsonMarshaler{"pqr"}},
95 LevelWarn,
96 } {
97 got := jsonValueString(AnyValue(value))
98 want, err := marshalJSON(value)
99 if err != nil {
100 t.Fatal(err)
101 }
102 if got != want {
103 t.Errorf("%v: got %s, want %s", value, got, want)
104 }
105 }
106 }
107
108 func marshalJSON(x any) (string, error) {
109 var buf bytes.Buffer
110 enc := json.NewEncoder(&buf)
111 enc.SetEscapeHTML(false)
112 if err := enc.Encode(x); err != nil {
113 return "", err
114 }
115 return strings.TrimSpace(buf.String()), nil
116 }
117
118 func TestJSONAppendAttrValueSpecial(t *testing.T) {
119
120 for _, test := range []struct {
121 value any
122 want string
123 }{
124 {math.NaN(), `"!ERROR:json: unsupported value: NaN"`},
125 {math.Inf(+1), `"!ERROR:json: unsupported value: +Inf"`},
126 {math.Inf(-1), `"!ERROR:json: unsupported value: -Inf"`},
127 {io.EOF, `"EOF"`},
128 } {
129 got := jsonValueString(AnyValue(test.value))
130 if got != test.want {
131 t.Errorf("%v: got %s, want %s", test.value, got, test.want)
132 }
133 }
134 }
135
136 func jsonValueString(v Value) string {
137 var buf []byte
138 s := &handleState{h: &commonHandler{json: true}, buf: (*buffer.Buffer)(&buf)}
139 if err := appendJSONValue(s, v); err != nil {
140 s.appendError(err)
141 }
142 return string(buf)
143 }
144
145 func TestJSONAllocs(t *testing.T) {
146 ctx := t.Context()
147 l := New(NewJSONHandler(io.Discard, &HandlerOptions{}))
148 testErr := errors.New("an error occurred")
149 testEvent := struct {
150 ID int
151 Scope string
152 Enabled bool
153 }{
154 123456, "abcdefgh", true,
155 }
156
157 t.Run("message", func(t *testing.T) {
158 wantAllocs(t, 0, func() {
159 l.LogAttrs(ctx, LevelInfo,
160 "hello world",
161 )
162 })
163 })
164 t.Run("attrs", func(t *testing.T) {
165 wantAllocs(t, 1, func() {
166 l.LogAttrs(ctx, LevelInfo,
167 "hello world",
168 String("component", "subtest"),
169 Int("id", 67890),
170 Bool("flag", true),
171 Any("error", testErr),
172 Any("event", testEvent),
173 )
174 })
175 })
176 }
177
178 func BenchmarkJSONHandler(b *testing.B) {
179 for _, bench := range []struct {
180 name string
181 opts HandlerOptions
182 }{
183 {"defaults", HandlerOptions{}},
184 {"time format", HandlerOptions{
185 ReplaceAttr: func(_ []string, a Attr) Attr {
186 v := a.Value
187 if v.Kind() == KindTime {
188 return String(a.Key, v.Time().Format(rfc3339Millis))
189 }
190 if a.Key == "level" {
191 return Attr{"severity", a.Value}
192 }
193 return a
194 },
195 }},
196 {"time unix", HandlerOptions{
197 ReplaceAttr: func(_ []string, a Attr) Attr {
198 v := a.Value
199 if v.Kind() == KindTime {
200 return Int64(a.Key, v.Time().UnixNano())
201 }
202 if a.Key == "level" {
203 return Attr{"severity", a.Value}
204 }
205 return a
206 },
207 }},
208 } {
209 b.Run(bench.name, func(b *testing.B) {
210 ctx := context.Background()
211 l := New(NewJSONHandler(io.Discard, &bench.opts)).With(
212 String("program", "my-test-program"),
213 String("package", "log/slog"),
214 String("traceID", "2039232309232309"),
215 String("URL", "https://pkg.go.dev/golang.org/x/log/slog"))
216 b.ReportAllocs()
217 for b.Loop() {
218 l.LogAttrs(ctx, LevelInfo, "this is a typical log message",
219 String("module", "github.com/google/go-cmp"),
220 String("version", "v1.23.4"),
221 Int("count", 23),
222 Int("number", 123456),
223 )
224 }
225 })
226 }
227 }
228
229 func BenchmarkPreformatting(b *testing.B) {
230 type req struct {
231 Method string
232 URL string
233 TraceID string
234 Addr string
235 }
236
237 structAttrs := []any{
238 String("program", "my-test-program"),
239 String("package", "log/slog"),
240 Any("request", &req{
241 Method: "GET",
242 URL: "https://pkg.go.dev/golang.org/x/log/slog",
243 TraceID: "2039232309232309",
244 Addr: "127.0.0.1:8080",
245 }),
246 }
247
248 outFile, err := os.Create(filepath.Join(b.TempDir(), "bench.log"))
249 if err != nil {
250 b.Fatal(err)
251 }
252 defer func() {
253 if err := outFile.Close(); err != nil {
254 b.Fatal(err)
255 }
256 }()
257
258 for _, bench := range []struct {
259 name string
260 wc io.Writer
261 attrs []any
262 }{
263 {"separate", io.Discard, []any{
264 String("program", "my-test-program"),
265 String("package", "log/slog"),
266 String("method", "GET"),
267 String("URL", "https://pkg.go.dev/golang.org/x/log/slog"),
268 String("traceID", "2039232309232309"),
269 String("addr", "127.0.0.1:8080"),
270 }},
271 {"struct", io.Discard, structAttrs},
272 {"struct file", outFile, structAttrs},
273 } {
274 ctx := context.Background()
275 b.Run(bench.name, func(b *testing.B) {
276 l := New(NewJSONHandler(bench.wc, nil)).With(bench.attrs...)
277 b.ReportAllocs()
278 for b.Loop() {
279 l.LogAttrs(ctx, LevelInfo, "this is a typical log message",
280 String("module", "github.com/google/go-cmp"),
281 String("version", "v1.23.4"),
282 Int("count", 23),
283 Int("number", 123456),
284 )
285 }
286 })
287 }
288 }
289
290 func BenchmarkJSONEncoding(b *testing.B) {
291 value := 3.14
292 buf := buffer.New()
293 defer buf.Free()
294 b.Run("json.Marshal", func(b *testing.B) {
295 b.ReportAllocs()
296 for i := 0; i < b.N; i++ {
297 by, err := json.Marshal(value)
298 if err != nil {
299 b.Fatal(err)
300 }
301 buf.Write(by)
302 *buf = (*buf)[:0]
303 }
304 })
305 b.Run("Encoder.Encode", func(b *testing.B) {
306 b.ReportAllocs()
307 for i := 0; i < b.N; i++ {
308 if err := json.NewEncoder(buf).Encode(value); err != nil {
309 b.Fatal(err)
310 }
311 *buf = (*buf)[:0]
312 }
313 })
314 _ = buf
315 }
316
View as plain text