github.com/AndrienkoAleksandr/go@v0.0.19/src/log/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 "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 // for testing json.Marshaler 57 type jsonMarshaler struct { 58 s string 59 } 60 61 func (j jsonMarshaler) String() string { return j.s } // should be ignored 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 // jsonAppendAttrValue should always agree with json.Marshal. 78 for _, value := range []any{ 79 "hello\r\n\t\a", 80 `"[{escape}]"`, 81 "<escapeHTML&>", 82 // \u2028\u2029 is an edge case in JavaScript vs JSON. 83 // \xF6 is an incomplete encoding. 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 // Attr values that render differently from json.Marshal. 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 BenchmarkJSONHandler(b *testing.B) { 146 for _, bench := range []struct { 147 name string 148 opts HandlerOptions 149 }{ 150 {"defaults", HandlerOptions{}}, 151 {"time format", HandlerOptions{ 152 ReplaceAttr: func(_ []string, a Attr) Attr { 153 v := a.Value 154 if v.Kind() == KindTime { 155 return String(a.Key, v.Time().Format(rfc3339Millis)) 156 } 157 if a.Key == "level" { 158 return Attr{"severity", a.Value} 159 } 160 return a 161 }, 162 }}, 163 {"time unix", HandlerOptions{ 164 ReplaceAttr: func(_ []string, a Attr) Attr { 165 v := a.Value 166 if v.Kind() == KindTime { 167 return Int64(a.Key, v.Time().UnixNano()) 168 } 169 if a.Key == "level" { 170 return Attr{"severity", a.Value} 171 } 172 return a 173 }, 174 }}, 175 } { 176 b.Run(bench.name, func(b *testing.B) { 177 l := New(NewJSONHandler(io.Discard, &bench.opts)).With( 178 String("program", "my-test-program"), 179 String("package", "log/slog"), 180 String("traceID", "2039232309232309"), 181 String("URL", "https://pkg.go.dev/golang.org/x/log/slog")) 182 b.ReportAllocs() 183 b.ResetTimer() 184 for i := 0; i < b.N; i++ { 185 l.LogAttrs(nil, LevelInfo, "this is a typical log message", 186 String("module", "github.com/google/go-cmp"), 187 String("version", "v1.23.4"), 188 Int("count", 23), 189 Int("number", 123456), 190 ) 191 } 192 }) 193 } 194 } 195 196 func BenchmarkPreformatting(b *testing.B) { 197 type req struct { 198 Method string 199 URL string 200 TraceID string 201 Addr string 202 } 203 204 structAttrs := []any{ 205 String("program", "my-test-program"), 206 String("package", "log/slog"), 207 Any("request", &req{ 208 Method: "GET", 209 URL: "https://pkg.go.dev/golang.org/x/log/slog", 210 TraceID: "2039232309232309", 211 Addr: "127.0.0.1:8080", 212 }), 213 } 214 215 outFile, err := os.Create(filepath.Join(b.TempDir(), "bench.log")) 216 if err != nil { 217 b.Fatal(err) 218 } 219 defer func() { 220 if err := outFile.Close(); err != nil { 221 b.Fatal(err) 222 } 223 }() 224 225 for _, bench := range []struct { 226 name string 227 wc io.Writer 228 attrs []any 229 }{ 230 {"separate", io.Discard, []any{ 231 String("program", "my-test-program"), 232 String("package", "log/slog"), 233 String("method", "GET"), 234 String("URL", "https://pkg.go.dev/golang.org/x/log/slog"), 235 String("traceID", "2039232309232309"), 236 String("addr", "127.0.0.1:8080"), 237 }}, 238 {"struct", io.Discard, structAttrs}, 239 {"struct file", outFile, structAttrs}, 240 } { 241 b.Run(bench.name, func(b *testing.B) { 242 l := New(NewJSONHandler(bench.wc, nil)).With(bench.attrs...) 243 b.ReportAllocs() 244 b.ResetTimer() 245 for i := 0; i < b.N; i++ { 246 l.LogAttrs(nil, LevelInfo, "this is a typical log message", 247 String("module", "github.com/google/go-cmp"), 248 String("version", "v1.23.4"), 249 Int("count", 23), 250 Int("number", 123456), 251 ) 252 } 253 }) 254 } 255 } 256 257 func BenchmarkJSONEncoding(b *testing.B) { 258 value := 3.14 259 buf := buffer.New() 260 defer buf.Free() 261 b.Run("json.Marshal", func(b *testing.B) { 262 b.ReportAllocs() 263 for i := 0; i < b.N; i++ { 264 by, err := json.Marshal(value) 265 if err != nil { 266 b.Fatal(err) 267 } 268 buf.Write(by) 269 *buf = (*buf)[:0] 270 } 271 }) 272 b.Run("Encoder.Encode", func(b *testing.B) { 273 b.ReportAllocs() 274 for i := 0; i < b.N; i++ { 275 if err := json.NewEncoder(buf).Encode(value); err != nil { 276 b.Fatal(err) 277 } 278 *buf = (*buf)[:0] 279 } 280 }) 281 _ = buf 282 }