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