golang.org/x/exp@v0.0.0-20240506185415-9bf2ced13842/slog/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 // TODO: verify that the output of Marshal{Text,JSON} is suitably escaped. 6 7 package slog 8 9 import ( 10 "bytes" 11 "context" 12 "encoding/json" 13 "io" 14 "path/filepath" 15 "strconv" 16 "strings" 17 "testing" 18 "time" 19 20 "golang.org/x/exp/slices" 21 "golang.org/x/exp/slog/internal/buffer" 22 ) 23 24 func TestDefaultHandle(t *testing.T) { 25 ctx := context.Background() 26 preAttrs := []Attr{Int("pre", 0)} 27 attrs := []Attr{Int("a", 1), String("b", "two")} 28 for _, test := range []struct { 29 name string 30 with func(Handler) Handler 31 attrs []Attr 32 want string 33 }{ 34 { 35 name: "no attrs", 36 want: "INFO message", 37 }, 38 { 39 name: "attrs", 40 attrs: attrs, 41 want: "INFO message a=1 b=two", 42 }, 43 { 44 name: "preformatted", 45 with: func(h Handler) Handler { return h.WithAttrs(preAttrs) }, 46 attrs: attrs, 47 want: "INFO message pre=0 a=1 b=two", 48 }, 49 { 50 name: "groups", 51 attrs: []Attr{ 52 Int("a", 1), 53 Group("g", 54 Int("b", 2), 55 Group("h", Int("c", 3)), 56 Int("d", 4)), 57 Int("e", 5), 58 }, 59 want: "INFO message a=1 g.b=2 g.h.c=3 g.d=4 e=5", 60 }, 61 { 62 name: "group", 63 with: func(h Handler) Handler { return h.WithAttrs(preAttrs).WithGroup("s") }, 64 attrs: attrs, 65 want: "INFO message pre=0 s.a=1 s.b=two", 66 }, 67 { 68 name: "preformatted groups", 69 with: func(h Handler) Handler { 70 return h.WithAttrs([]Attr{Int("p1", 1)}). 71 WithGroup("s1"). 72 WithAttrs([]Attr{Int("p2", 2)}). 73 WithGroup("s2") 74 }, 75 attrs: attrs, 76 want: "INFO message p1=1 s1.p2=2 s1.s2.a=1 s1.s2.b=two", 77 }, 78 { 79 name: "two with-groups", 80 with: func(h Handler) Handler { 81 return h.WithAttrs([]Attr{Int("p1", 1)}). 82 WithGroup("s1"). 83 WithGroup("s2") 84 }, 85 attrs: attrs, 86 want: "INFO message p1=1 s1.s2.a=1 s1.s2.b=two", 87 }, 88 } { 89 t.Run(test.name, func(t *testing.T) { 90 var got string 91 var h Handler = newDefaultHandler(func(_ int, s string) error { 92 got = s 93 return nil 94 }) 95 if test.with != nil { 96 h = test.with(h) 97 } 98 r := NewRecord(time.Time{}, LevelInfo, "message", 0) 99 r.AddAttrs(test.attrs...) 100 if err := h.Handle(ctx, r); err != nil { 101 t.Fatal(err) 102 } 103 if got != test.want { 104 t.Errorf("\ngot %s\nwant %s", got, test.want) 105 } 106 }) 107 } 108 } 109 110 // Verify the common parts of TextHandler and JSONHandler. 111 func TestJSONAndTextHandlers(t *testing.T) { 112 ctx := context.Background() 113 114 // remove all Attrs 115 removeAll := func(_ []string, a Attr) Attr { return Attr{} } 116 117 attrs := []Attr{String("a", "one"), Int("b", 2), Any("", nil)} 118 preAttrs := []Attr{Int("pre", 3), String("x", "y")} 119 120 for _, test := range []struct { 121 name string 122 replace func([]string, Attr) Attr 123 addSource bool 124 with func(Handler) Handler 125 preAttrs []Attr 126 attrs []Attr 127 wantText string 128 wantJSON string 129 }{ 130 { 131 name: "basic", 132 attrs: attrs, 133 wantText: "time=2000-01-02T03:04:05.000Z level=INFO msg=message a=one b=2", 134 wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","a":"one","b":2}`, 135 }, 136 { 137 name: "empty key", 138 attrs: append(slices.Clip(attrs), Any("", "v")), 139 wantText: `time=2000-01-02T03:04:05.000Z level=INFO msg=message a=one b=2 ""=v`, 140 wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","a":"one","b":2,"":"v"}`, 141 }, 142 { 143 name: "cap keys", 144 replace: upperCaseKey, 145 attrs: attrs, 146 wantText: "TIME=2000-01-02T03:04:05.000Z LEVEL=INFO MSG=message A=one B=2", 147 wantJSON: `{"TIME":"2000-01-02T03:04:05Z","LEVEL":"INFO","MSG":"message","A":"one","B":2}`, 148 }, 149 { 150 name: "remove all", 151 replace: removeAll, 152 attrs: attrs, 153 wantText: "", 154 wantJSON: `{}`, 155 }, 156 { 157 name: "preformatted", 158 with: func(h Handler) Handler { return h.WithAttrs(preAttrs) }, 159 preAttrs: preAttrs, 160 attrs: attrs, 161 wantText: "time=2000-01-02T03:04:05.000Z level=INFO msg=message pre=3 x=y a=one b=2", 162 wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","pre":3,"x":"y","a":"one","b":2}`, 163 }, 164 { 165 name: "preformatted cap keys", 166 replace: upperCaseKey, 167 with: func(h Handler) Handler { return h.WithAttrs(preAttrs) }, 168 preAttrs: preAttrs, 169 attrs: attrs, 170 wantText: "TIME=2000-01-02T03:04:05.000Z LEVEL=INFO MSG=message PRE=3 X=y A=one B=2", 171 wantJSON: `{"TIME":"2000-01-02T03:04:05Z","LEVEL":"INFO","MSG":"message","PRE":3,"X":"y","A":"one","B":2}`, 172 }, 173 { 174 name: "preformatted remove all", 175 replace: removeAll, 176 with: func(h Handler) Handler { return h.WithAttrs(preAttrs) }, 177 preAttrs: preAttrs, 178 attrs: attrs, 179 wantText: "", 180 wantJSON: "{}", 181 }, 182 { 183 name: "remove built-in", 184 replace: removeKeys(TimeKey, LevelKey, MessageKey), 185 attrs: attrs, 186 wantText: "a=one b=2", 187 wantJSON: `{"a":"one","b":2}`, 188 }, 189 { 190 name: "preformatted remove built-in", 191 replace: removeKeys(TimeKey, LevelKey, MessageKey), 192 with: func(h Handler) Handler { return h.WithAttrs(preAttrs) }, 193 attrs: attrs, 194 wantText: "pre=3 x=y a=one b=2", 195 wantJSON: `{"pre":3,"x":"y","a":"one","b":2}`, 196 }, 197 { 198 name: "groups", 199 replace: removeKeys(TimeKey, LevelKey), // to simplify the result 200 attrs: []Attr{ 201 Int("a", 1), 202 Group("g", 203 Int("b", 2), 204 Group("h", Int("c", 3)), 205 Int("d", 4)), 206 Int("e", 5), 207 }, 208 wantText: "msg=message a=1 g.b=2 g.h.c=3 g.d=4 e=5", 209 wantJSON: `{"msg":"message","a":1,"g":{"b":2,"h":{"c":3},"d":4},"e":5}`, 210 }, 211 { 212 name: "empty group", 213 replace: removeKeys(TimeKey, LevelKey), 214 attrs: []Attr{Group("g"), Group("h", Int("a", 1))}, 215 wantText: "msg=message h.a=1", 216 wantJSON: `{"msg":"message","h":{"a":1}}`, 217 }, 218 { 219 name: "escapes", 220 replace: removeKeys(TimeKey, LevelKey), 221 attrs: []Attr{ 222 String("a b", "x\t\n\000y"), 223 Group(" b.c=\"\\x2E\t", 224 String("d=e", "f.g\""), 225 Int("m.d", 1)), // dot is not escaped 226 }, 227 wantText: `msg=message "a b"="x\t\n\x00y" " b.c=\"\\x2E\t.d=e"="f.g\"" " b.c=\"\\x2E\t.m.d"=1`, 228 wantJSON: `{"msg":"message","a b":"x\t\n\u0000y"," b.c=\"\\x2E\t":{"d=e":"f.g\"","m.d":1}}`, 229 }, 230 { 231 name: "LogValuer", 232 replace: removeKeys(TimeKey, LevelKey), 233 attrs: []Attr{ 234 Int("a", 1), 235 Any("name", logValueName{"Ren", "Hoek"}), 236 Int("b", 2), 237 }, 238 wantText: "msg=message a=1 name.first=Ren name.last=Hoek b=2", 239 wantJSON: `{"msg":"message","a":1,"name":{"first":"Ren","last":"Hoek"},"b":2}`, 240 }, 241 { 242 // Test resolution when there is no ReplaceAttr function. 243 name: "resolve", 244 attrs: []Attr{ 245 Any("", &replace{Value{}}), // should be elided 246 Any("name", logValueName{"Ren", "Hoek"}), 247 }, 248 wantText: "time=2000-01-02T03:04:05.000Z level=INFO msg=message name.first=Ren name.last=Hoek", 249 wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","name":{"first":"Ren","last":"Hoek"}}`, 250 }, 251 { 252 name: "with-group", 253 replace: removeKeys(TimeKey, LevelKey), 254 with: func(h Handler) Handler { return h.WithAttrs(preAttrs).WithGroup("s") }, 255 attrs: attrs, 256 wantText: "msg=message pre=3 x=y s.a=one s.b=2", 257 wantJSON: `{"msg":"message","pre":3,"x":"y","s":{"a":"one","b":2}}`, 258 }, 259 { 260 name: "preformatted with-groups", 261 replace: removeKeys(TimeKey, LevelKey), 262 with: func(h Handler) Handler { 263 return h.WithAttrs([]Attr{Int("p1", 1)}). 264 WithGroup("s1"). 265 WithAttrs([]Attr{Int("p2", 2)}). 266 WithGroup("s2") 267 }, 268 attrs: attrs, 269 wantText: "msg=message p1=1 s1.p2=2 s1.s2.a=one s1.s2.b=2", 270 wantJSON: `{"msg":"message","p1":1,"s1":{"p2":2,"s2":{"a":"one","b":2}}}`, 271 }, 272 { 273 name: "two with-groups", 274 replace: removeKeys(TimeKey, LevelKey), 275 with: func(h Handler) Handler { 276 return h.WithAttrs([]Attr{Int("p1", 1)}). 277 WithGroup("s1"). 278 WithGroup("s2") 279 }, 280 attrs: attrs, 281 wantText: "msg=message p1=1 s1.s2.a=one s1.s2.b=2", 282 wantJSON: `{"msg":"message","p1":1,"s1":{"s2":{"a":"one","b":2}}}`, 283 }, 284 { 285 name: "GroupValue as Attr value", 286 replace: removeKeys(TimeKey, LevelKey), 287 attrs: []Attr{{"v", AnyValue(IntValue(3))}}, 288 wantText: "msg=message v=3", 289 wantJSON: `{"msg":"message","v":3}`, 290 }, 291 { 292 name: "byte slice", 293 replace: removeKeys(TimeKey, LevelKey), 294 attrs: []Attr{Any("bs", []byte{1, 2, 3, 4})}, 295 wantText: `msg=message bs="\x01\x02\x03\x04"`, 296 wantJSON: `{"msg":"message","bs":"AQIDBA=="}`, 297 }, 298 { 299 name: "json.RawMessage", 300 replace: removeKeys(TimeKey, LevelKey), 301 attrs: []Attr{Any("bs", json.RawMessage([]byte("1234")))}, 302 wantText: `msg=message bs="1234"`, 303 wantJSON: `{"msg":"message","bs":1234}`, 304 }, 305 { 306 name: "inline group", 307 replace: removeKeys(TimeKey, LevelKey), 308 attrs: []Attr{ 309 Int("a", 1), 310 Group("", Int("b", 2), Int("c", 3)), 311 Int("d", 4), 312 }, 313 wantText: `msg=message a=1 b=2 c=3 d=4`, 314 wantJSON: `{"msg":"message","a":1,"b":2,"c":3,"d":4}`, 315 }, 316 { 317 name: "Source", 318 replace: func(gs []string, a Attr) Attr { 319 if a.Key == SourceKey { 320 s := a.Value.Any().(*Source) 321 s.File = filepath.Base(s.File) 322 return Any(a.Key, s) 323 } 324 return removeKeys(TimeKey, LevelKey)(gs, a) 325 }, 326 addSource: true, 327 wantText: `source=handler_test.go:$LINE msg=message`, 328 wantJSON: `{"source":{"function":"golang.org/x/exp/slog.TestJSONAndTextHandlers","file":"handler_test.go","line":$LINE},"msg":"message"}`, 329 }, 330 } { 331 r := NewRecord(testTime, LevelInfo, "message", callerPC(2)) 332 line := strconv.Itoa(r.source().Line) 333 r.AddAttrs(test.attrs...) 334 var buf bytes.Buffer 335 opts := HandlerOptions{ReplaceAttr: test.replace, AddSource: test.addSource} 336 t.Run(test.name, func(t *testing.T) { 337 for _, handler := range []struct { 338 name string 339 h Handler 340 want string 341 }{ 342 {"text", NewTextHandler(&buf, &opts), test.wantText}, 343 {"json", NewJSONHandler(&buf, &opts), test.wantJSON}, 344 } { 345 t.Run(handler.name, func(t *testing.T) { 346 h := handler.h 347 if test.with != nil { 348 h = test.with(h) 349 } 350 buf.Reset() 351 if err := h.Handle(ctx, r); err != nil { 352 t.Fatal(err) 353 } 354 want := strings.ReplaceAll(handler.want, "$LINE", line) 355 got := strings.TrimSuffix(buf.String(), "\n") 356 if got != want { 357 t.Errorf("\ngot %s\nwant %s\n", got, want) 358 } 359 }) 360 } 361 }) 362 } 363 } 364 365 // removeKeys returns a function suitable for HandlerOptions.ReplaceAttr 366 // that removes all Attrs with the given keys. 367 func removeKeys(keys ...string) func([]string, Attr) Attr { 368 return func(_ []string, a Attr) Attr { 369 for _, k := range keys { 370 if a.Key == k { 371 return Attr{} 372 } 373 } 374 return a 375 } 376 } 377 378 func upperCaseKey(_ []string, a Attr) Attr { 379 a.Key = strings.ToUpper(a.Key) 380 return a 381 } 382 383 type logValueName struct { 384 first, last string 385 } 386 387 func (n logValueName) LogValue() Value { 388 return GroupValue( 389 String("first", n.first), 390 String("last", n.last)) 391 } 392 393 func TestHandlerEnabled(t *testing.T) { 394 levelVar := func(l Level) *LevelVar { 395 var al LevelVar 396 al.Set(l) 397 return &al 398 } 399 400 for _, test := range []struct { 401 leveler Leveler 402 want bool 403 }{ 404 {nil, true}, 405 {LevelWarn, false}, 406 {&LevelVar{}, true}, // defaults to Info 407 {levelVar(LevelWarn), false}, 408 {LevelDebug, true}, 409 {levelVar(LevelDebug), true}, 410 } { 411 h := &commonHandler{opts: HandlerOptions{Level: test.leveler}} 412 got := h.enabled(LevelInfo) 413 if got != test.want { 414 t.Errorf("%v: got %t, want %t", test.leveler, got, test.want) 415 } 416 } 417 } 418 419 func TestSecondWith(t *testing.T) { 420 // Verify that a second call to Logger.With does not corrupt 421 // the original. 422 var buf bytes.Buffer 423 h := NewTextHandler(&buf, &HandlerOptions{ReplaceAttr: removeKeys(TimeKey)}) 424 logger := New(h).With( 425 String("app", "playground"), 426 String("role", "tester"), 427 Int("data_version", 2), 428 ) 429 appLogger := logger.With("type", "log") // this becomes type=met 430 _ = logger.With("type", "metric") 431 appLogger.Info("foo") 432 got := strings.TrimSpace(buf.String()) 433 want := `level=INFO msg=foo app=playground role=tester data_version=2 type=log` 434 if got != want { 435 t.Errorf("\ngot %s\nwant %s", got, want) 436 } 437 } 438 439 func TestReplaceAttrGroups(t *testing.T) { 440 // Verify that ReplaceAttr is called with the correct groups. 441 type ga struct { 442 groups string 443 key string 444 val string 445 } 446 447 var got []ga 448 449 h := NewTextHandler(io.Discard, &HandlerOptions{ReplaceAttr: func(gs []string, a Attr) Attr { 450 v := a.Value.String() 451 if a.Key == TimeKey { 452 v = "<now>" 453 } 454 got = append(got, ga{strings.Join(gs, ","), a.Key, v}) 455 return a 456 }}) 457 New(h). 458 With(Int("a", 1)). 459 WithGroup("g1"). 460 With(Int("b", 2)). 461 WithGroup("g2"). 462 With( 463 Int("c", 3), 464 Group("g3", Int("d", 4)), 465 Int("e", 5)). 466 Info("m", 467 Int("f", 6), 468 Group("g4", Int("h", 7)), 469 Int("i", 8)) 470 471 want := []ga{ 472 {"", "a", "1"}, 473 {"g1", "b", "2"}, 474 {"g1,g2", "c", "3"}, 475 {"g1,g2,g3", "d", "4"}, 476 {"g1,g2", "e", "5"}, 477 {"", "time", "<now>"}, 478 {"", "level", "INFO"}, 479 {"", "msg", "m"}, 480 {"g1,g2", "f", "6"}, 481 {"g1,g2,g4", "h", "7"}, 482 {"g1,g2", "i", "8"}, 483 } 484 if !slices.Equal(got, want) { 485 t.Errorf("\ngot %v\nwant %v", got, want) 486 } 487 } 488 489 const rfc3339Millis = "2006-01-02T15:04:05.000Z07:00" 490 491 func TestWriteTimeRFC3339(t *testing.T) { 492 for _, tm := range []time.Time{ 493 time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC), 494 time.Date(2000, 1, 2, 3, 4, 5, 400, time.Local), 495 time.Date(2000, 11, 12, 3, 4, 500, 5e7, time.UTC), 496 } { 497 want := tm.Format(rfc3339Millis) 498 buf := buffer.New() 499 defer buf.Free() 500 writeTimeRFC3339Millis(buf, tm) 501 got := buf.String() 502 if got != want { 503 t.Errorf("got %s, want %s", got, want) 504 } 505 } 506 } 507 508 func BenchmarkWriteTime(b *testing.B) { 509 buf := buffer.New() 510 defer buf.Free() 511 tm := time.Date(2022, 3, 4, 5, 6, 7, 823456789, time.Local) 512 b.ResetTimer() 513 for i := 0; i < b.N; i++ { 514 writeTimeRFC3339Millis(buf, tm) 515 buf.Reset() 516 } 517 }