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