github.com/twelsh-aw/go/src@v0.0.0-20230516233729-a56fe86a7c81/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", 80 `"[{escape}]"`, 81 "<escapeHTML&>", 82 `-123`, 83 int64(-9_200_123_456_789_123_456), 84 uint64(9_200_123_456_789_123_456), 85 -12.75, 86 1.23e-9, 87 false, 88 time.Minute, 89 testTime, 90 jsonMarshaler{"xyz"}, 91 jsonMarshalerError{jsonMarshaler{"pqr"}}, 92 LevelWarn, 93 } { 94 got := jsonValueString(AnyValue(value)) 95 want, err := marshalJSON(value) 96 if err != nil { 97 t.Fatal(err) 98 } 99 if got != want { 100 t.Errorf("%v: got %s, want %s", value, got, want) 101 } 102 } 103 } 104 105 func marshalJSON(x any) (string, error) { 106 var buf bytes.Buffer 107 enc := json.NewEncoder(&buf) 108 enc.SetEscapeHTML(false) 109 if err := enc.Encode(x); err != nil { 110 return "", err 111 } 112 return strings.TrimSpace(buf.String()), nil 113 } 114 115 func TestJSONAppendAttrValueSpecial(t *testing.T) { 116 // Attr values that render differently from json.Marshal. 117 for _, test := range []struct { 118 value any 119 want string 120 }{ 121 {math.NaN(), `"!ERROR:json: unsupported value: NaN"`}, 122 {math.Inf(+1), `"!ERROR:json: unsupported value: +Inf"`}, 123 {math.Inf(-1), `"!ERROR:json: unsupported value: -Inf"`}, 124 {io.EOF, `"EOF"`}, 125 } { 126 got := jsonValueString(AnyValue(test.value)) 127 if got != test.want { 128 t.Errorf("%v: got %s, want %s", test.value, got, test.want) 129 } 130 } 131 } 132 133 func jsonValueString(v Value) string { 134 var buf []byte 135 s := &handleState{h: &commonHandler{json: true}, buf: (*buffer.Buffer)(&buf)} 136 if err := appendJSONValue(s, v); err != nil { 137 s.appendError(err) 138 } 139 return string(buf) 140 } 141 142 func BenchmarkJSONHandler(b *testing.B) { 143 for _, bench := range []struct { 144 name string 145 opts HandlerOptions 146 }{ 147 {"defaults", HandlerOptions{}}, 148 {"time format", HandlerOptions{ 149 ReplaceAttr: func(_ []string, a Attr) Attr { 150 v := a.Value 151 if v.Kind() == KindTime { 152 return String(a.Key, v.Time().Format(rfc3339Millis)) 153 } 154 if a.Key == "level" { 155 return Attr{"severity", a.Value} 156 } 157 return a 158 }, 159 }}, 160 {"time unix", HandlerOptions{ 161 ReplaceAttr: func(_ []string, a Attr) Attr { 162 v := a.Value 163 if v.Kind() == KindTime { 164 return Int64(a.Key, v.Time().UnixNano()) 165 } 166 if a.Key == "level" { 167 return Attr{"severity", a.Value} 168 } 169 return a 170 }, 171 }}, 172 } { 173 b.Run(bench.name, func(b *testing.B) { 174 l := New(NewJSONHandler(io.Discard, &bench.opts)).With( 175 String("program", "my-test-program"), 176 String("package", "log/slog"), 177 String("traceID", "2039232309232309"), 178 String("URL", "https://pkg.go.dev/golang.org/x/log/slog")) 179 b.ReportAllocs() 180 b.ResetTimer() 181 for i := 0; i < b.N; i++ { 182 l.LogAttrs(nil, LevelInfo, "this is a typical log message", 183 String("module", "github.com/google/go-cmp"), 184 String("version", "v1.23.4"), 185 Int("count", 23), 186 Int("number", 123456), 187 ) 188 } 189 }) 190 } 191 } 192 193 func BenchmarkPreformatting(b *testing.B) { 194 type req struct { 195 Method string 196 URL string 197 TraceID string 198 Addr string 199 } 200 201 structAttrs := []any{ 202 String("program", "my-test-program"), 203 String("package", "log/slog"), 204 Any("request", &req{ 205 Method: "GET", 206 URL: "https://pkg.go.dev/golang.org/x/log/slog", 207 TraceID: "2039232309232309", 208 Addr: "127.0.0.1:8080", 209 }), 210 } 211 212 outFile, err := os.Create(filepath.Join(b.TempDir(), "bench.log")) 213 if err != nil { 214 b.Fatal(err) 215 } 216 defer func() { 217 if err := outFile.Close(); err != nil { 218 b.Fatal(err) 219 } 220 }() 221 222 for _, bench := range []struct { 223 name string 224 wc io.Writer 225 attrs []any 226 }{ 227 {"separate", io.Discard, []any{ 228 String("program", "my-test-program"), 229 String("package", "log/slog"), 230 String("method", "GET"), 231 String("URL", "https://pkg.go.dev/golang.org/x/log/slog"), 232 String("traceID", "2039232309232309"), 233 String("addr", "127.0.0.1:8080"), 234 }}, 235 {"struct", io.Discard, structAttrs}, 236 {"struct file", outFile, structAttrs}, 237 } { 238 b.Run(bench.name, func(b *testing.B) { 239 l := New(NewJSONHandler(bench.wc, nil)).With(bench.attrs...) 240 b.ReportAllocs() 241 b.ResetTimer() 242 for i := 0; i < b.N; i++ { 243 l.LogAttrs(nil, LevelInfo, "this is a typical log message", 244 String("module", "github.com/google/go-cmp"), 245 String("version", "v1.23.4"), 246 Int("count", 23), 247 Int("number", 123456), 248 ) 249 } 250 }) 251 } 252 } 253 254 func BenchmarkJSONEncoding(b *testing.B) { 255 value := 3.14 256 buf := buffer.New() 257 defer buf.Free() 258 b.Run("json.Marshal", func(b *testing.B) { 259 b.ReportAllocs() 260 for i := 0; i < b.N; i++ { 261 by, err := json.Marshal(value) 262 if err != nil { 263 b.Fatal(err) 264 } 265 buf.Write(by) 266 *buf = (*buf)[:0] 267 } 268 }) 269 b.Run("Encoder.Encode", func(b *testing.B) { 270 b.ReportAllocs() 271 for i := 0; i < b.N; i++ { 272 if err := json.NewEncoder(buf).Encode(value); err != nil { 273 b.Fatal(err) 274 } 275 *buf = (*buf)[:0] 276 } 277 }) 278 _ = buf 279 }