github.com/AndrienkoAleksandr/go@v0.0.19/src/log/slog/logger_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 "internal/race" 11 "internal/testenv" 12 "io" 13 "log" 14 loginternal "log/internal" 15 "path/filepath" 16 "regexp" 17 "runtime" 18 "slices" 19 "strings" 20 "sync" 21 "testing" 22 "time" 23 ) 24 25 const timeRE = `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}(Z|[+-]\d{2}:\d{2})` 26 27 func TestLogTextHandler(t *testing.T) { 28 var buf bytes.Buffer 29 30 l := New(NewTextHandler(&buf, nil)) 31 32 check := func(want string) { 33 t.Helper() 34 if want != "" { 35 want = "time=" + timeRE + " " + want 36 } 37 checkLogOutput(t, buf.String(), want) 38 buf.Reset() 39 } 40 41 l.Info("msg", "a", 1, "b", 2) 42 check(`level=INFO msg=msg a=1 b=2`) 43 44 // By default, debug messages are not printed. 45 l.Debug("bg", Int("a", 1), "b", 2) 46 check("") 47 48 l.Warn("w", Duration("dur", 3*time.Second)) 49 check(`level=WARN msg=w dur=3s`) 50 51 l.Error("bad", "a", 1) 52 check(`level=ERROR msg=bad a=1`) 53 54 l.Log(nil, LevelWarn+1, "w", Int("a", 1), String("b", "two")) 55 check(`level=WARN\+1 msg=w a=1 b=two`) 56 57 l.LogAttrs(nil, LevelInfo+1, "a b c", Int("a", 1), String("b", "two")) 58 check(`level=INFO\+1 msg="a b c" a=1 b=two`) 59 60 l.Info("info", "a", []Attr{Int("i", 1)}) 61 check(`level=INFO msg=info a.i=1`) 62 63 l.Info("info", "a", GroupValue(Int("i", 1))) 64 check(`level=INFO msg=info a.i=1`) 65 } 66 67 func TestConnections(t *testing.T) { 68 var logbuf, slogbuf bytes.Buffer 69 70 // Revert any changes to the default logger. This is important because other 71 // tests might change the default logger using SetDefault. Also ensure we 72 // restore the default logger at the end of the test. 73 currentLogger := Default() 74 SetDefault(New(newDefaultHandler(loginternal.DefaultOutput))) 75 t.Cleanup(func() { 76 SetDefault(currentLogger) 77 }) 78 79 // The default slog.Logger's handler uses the log package's default output. 80 log.SetOutput(&logbuf) 81 log.SetFlags(log.Lshortfile &^ log.LstdFlags) 82 Info("msg", "a", 1) 83 checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: INFO msg a=1`) 84 logbuf.Reset() 85 Warn("msg", "b", 2) 86 checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: WARN msg b=2`) 87 logbuf.Reset() 88 Error("msg", "err", io.EOF, "c", 3) 89 checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: ERROR msg err=EOF c=3`) 90 91 // Levels below Info are not printed. 92 logbuf.Reset() 93 Debug("msg", "c", 3) 94 checkLogOutput(t, logbuf.String(), "") 95 96 t.Run("wrap default handler", func(t *testing.T) { 97 // It should be possible to wrap the default handler and get the right output. 98 // This works because the default handler uses the pc in the Record 99 // to get the source line, rather than a call depth. 100 logger := New(wrappingHandler{Default().Handler()}) 101 logger.Info("msg", "d", 4) 102 checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: INFO msg d=4`) 103 }) 104 105 // Once slog.SetDefault is called, the direction is reversed: the default 106 // log.Logger's output goes through the handler. 107 SetDefault(New(NewTextHandler(&slogbuf, &HandlerOptions{AddSource: true}))) 108 log.Print("msg2") 109 checkLogOutput(t, slogbuf.String(), "time="+timeRE+` level=INFO source=.*logger_test.go:\d{3} msg=msg2`) 110 111 // The default log.Logger always outputs at Info level. 112 slogbuf.Reset() 113 SetDefault(New(NewTextHandler(&slogbuf, &HandlerOptions{Level: LevelWarn}))) 114 log.Print("should not appear") 115 if got := slogbuf.String(); got != "" { 116 t.Errorf("got %q, want empty", got) 117 } 118 119 // Setting log's output again breaks the connection. 120 logbuf.Reset() 121 slogbuf.Reset() 122 log.SetOutput(&logbuf) 123 log.SetFlags(log.Lshortfile &^ log.LstdFlags) 124 log.Print("msg3") 125 checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: msg3`) 126 if got := slogbuf.String(); got != "" { 127 t.Errorf("got %q, want empty", got) 128 } 129 } 130 131 type wrappingHandler struct { 132 h Handler 133 } 134 135 func (h wrappingHandler) Enabled(ctx context.Context, level Level) bool { 136 return h.h.Enabled(ctx, level) 137 } 138 func (h wrappingHandler) WithGroup(name string) Handler { return h.h.WithGroup(name) } 139 func (h wrappingHandler) WithAttrs(as []Attr) Handler { return h.h.WithAttrs(as) } 140 func (h wrappingHandler) Handle(ctx context.Context, r Record) error { return h.h.Handle(ctx, r) } 141 142 func TestAttrs(t *testing.T) { 143 check := func(got []Attr, want ...Attr) { 144 t.Helper() 145 if !attrsEqual(got, want) { 146 t.Errorf("got %v, want %v", got, want) 147 } 148 } 149 150 l1 := New(&captureHandler{}).With("a", 1) 151 l2 := New(l1.Handler()).With("b", 2) 152 l2.Info("m", "c", 3) 153 h := l2.Handler().(*captureHandler) 154 check(h.attrs, Int("a", 1), Int("b", 2)) 155 check(attrsSlice(h.r), Int("c", 3)) 156 } 157 158 func TestCallDepth(t *testing.T) { 159 h := &captureHandler{} 160 var startLine int 161 162 check := func(count int) { 163 t.Helper() 164 const wantFunc = "log/slog.TestCallDepth" 165 const wantFile = "logger_test.go" 166 wantLine := startLine + count*2 167 got := h.r.source() 168 gotFile := filepath.Base(got.File) 169 if got.Function != wantFunc || gotFile != wantFile || got.Line != wantLine { 170 t.Errorf("got (%s, %s, %d), want (%s, %s, %d)", 171 got.Function, gotFile, got.Line, wantFunc, wantFile, wantLine) 172 } 173 } 174 175 logger := New(h) 176 SetDefault(logger) 177 178 // Calls to check must be one line apart. 179 // Determine line where calls start. 180 f, _ := runtime.CallersFrames([]uintptr{callerPC(2)}).Next() 181 startLine = f.Line + 4 182 // Do not change the number of lines between here and the call to check(0). 183 184 logger.Log(nil, LevelInfo, "") 185 check(0) 186 logger.LogAttrs(nil, LevelInfo, "") 187 check(1) 188 logger.Debug("") 189 check(2) 190 logger.Info("") 191 check(3) 192 logger.Warn("") 193 check(4) 194 logger.Error("") 195 check(5) 196 Debug("") 197 check(6) 198 Info("") 199 check(7) 200 Warn("") 201 check(8) 202 Error("") 203 check(9) 204 Log(nil, LevelInfo, "") 205 check(10) 206 LogAttrs(nil, LevelInfo, "") 207 check(11) 208 } 209 210 func TestAlloc(t *testing.T) { 211 dl := New(discardHandler{}) 212 defer SetDefault(Default()) // restore 213 SetDefault(dl) 214 215 t.Run("Info", func(t *testing.T) { 216 wantAllocs(t, 0, func() { Info("hello") }) 217 }) 218 t.Run("Error", func(t *testing.T) { 219 wantAllocs(t, 0, func() { Error("hello") }) 220 }) 221 t.Run("logger.Info", func(t *testing.T) { 222 wantAllocs(t, 0, func() { dl.Info("hello") }) 223 }) 224 t.Run("logger.Log", func(t *testing.T) { 225 wantAllocs(t, 0, func() { dl.Log(nil, LevelDebug, "hello") }) 226 }) 227 t.Run("2 pairs", func(t *testing.T) { 228 s := "abc" 229 i := 2000 230 wantAllocs(t, 2, func() { 231 dl.Info("hello", 232 "n", i, 233 "s", s, 234 ) 235 }) 236 }) 237 t.Run("2 pairs disabled inline", func(t *testing.T) { 238 l := New(discardHandler{disabled: true}) 239 s := "abc" 240 i := 2000 241 wantAllocs(t, 2, func() { 242 l.Log(nil, LevelInfo, "hello", 243 "n", i, 244 "s", s, 245 ) 246 }) 247 }) 248 t.Run("2 pairs disabled", func(t *testing.T) { 249 l := New(discardHandler{disabled: true}) 250 s := "abc" 251 i := 2000 252 wantAllocs(t, 0, func() { 253 if l.Enabled(nil, LevelInfo) { 254 l.Log(nil, LevelInfo, "hello", 255 "n", i, 256 "s", s, 257 ) 258 } 259 }) 260 }) 261 t.Run("9 kvs", func(t *testing.T) { 262 s := "abc" 263 i := 2000 264 d := time.Second 265 wantAllocs(t, 11, func() { 266 dl.Info("hello", 267 "n", i, "s", s, "d", d, 268 "n", i, "s", s, "d", d, 269 "n", i, "s", s, "d", d) 270 }) 271 }) 272 t.Run("pairs", func(t *testing.T) { 273 wantAllocs(t, 0, func() { dl.Info("", "error", io.EOF) }) 274 }) 275 t.Run("attrs1", func(t *testing.T) { 276 wantAllocs(t, 0, func() { dl.LogAttrs(nil, LevelInfo, "", Int("a", 1)) }) 277 wantAllocs(t, 0, func() { dl.LogAttrs(nil, LevelInfo, "", Any("error", io.EOF)) }) 278 }) 279 t.Run("attrs3", func(t *testing.T) { 280 wantAllocs(t, 0, func() { 281 dl.LogAttrs(nil, LevelInfo, "hello", Int("a", 1), String("b", "two"), Duration("c", time.Second)) 282 }) 283 }) 284 t.Run("attrs3 disabled", func(t *testing.T) { 285 logger := New(discardHandler{disabled: true}) 286 wantAllocs(t, 0, func() { 287 logger.LogAttrs(nil, LevelInfo, "hello", Int("a", 1), String("b", "two"), Duration("c", time.Second)) 288 }) 289 }) 290 t.Run("attrs6", func(t *testing.T) { 291 wantAllocs(t, 1, func() { 292 dl.LogAttrs(nil, LevelInfo, "hello", 293 Int("a", 1), String("b", "two"), Duration("c", time.Second), 294 Int("d", 1), String("e", "two"), Duration("f", time.Second)) 295 }) 296 }) 297 t.Run("attrs9", func(t *testing.T) { 298 wantAllocs(t, 1, func() { 299 dl.LogAttrs(nil, LevelInfo, "hello", 300 Int("a", 1), String("b", "two"), Duration("c", time.Second), 301 Int("d", 1), String("e", "two"), Duration("f", time.Second), 302 Int("d", 1), String("e", "two"), Duration("f", time.Second)) 303 }) 304 }) 305 } 306 307 func TestSetAttrs(t *testing.T) { 308 for _, test := range []struct { 309 args []any 310 want []Attr 311 }{ 312 {nil, nil}, 313 {[]any{"a", 1}, []Attr{Int("a", 1)}}, 314 {[]any{"a", 1, "b", "two"}, []Attr{Int("a", 1), String("b", "two")}}, 315 {[]any{"a"}, []Attr{String(badKey, "a")}}, 316 {[]any{"a", 1, "b"}, []Attr{Int("a", 1), String(badKey, "b")}}, 317 {[]any{"a", 1, 2, 3}, []Attr{Int("a", 1), Int(badKey, 2), Int(badKey, 3)}}, 318 } { 319 r := NewRecord(time.Time{}, 0, "", 0) 320 r.Add(test.args...) 321 got := attrsSlice(r) 322 if !attrsEqual(got, test.want) { 323 t.Errorf("%v:\ngot %v\nwant %v", test.args, got, test.want) 324 } 325 } 326 } 327 328 func TestSetDefault(t *testing.T) { 329 // Verify that setting the default to itself does not result in deadlock. 330 ctx, cancel := context.WithTimeout(context.Background(), time.Second) 331 defer cancel() 332 defer func(w io.Writer) { log.SetOutput(w) }(log.Writer()) 333 log.SetOutput(io.Discard) 334 go func() { 335 Info("A") 336 SetDefault(Default()) 337 Info("B") 338 cancel() 339 }() 340 <-ctx.Done() 341 if err := ctx.Err(); err != context.Canceled { 342 t.Errorf("wanted canceled, got %v", err) 343 } 344 } 345 346 func TestLoggerError(t *testing.T) { 347 var buf bytes.Buffer 348 349 removeTime := func(_ []string, a Attr) Attr { 350 if a.Key == TimeKey { 351 return Attr{} 352 } 353 return a 354 } 355 l := New(NewTextHandler(&buf, &HandlerOptions{ReplaceAttr: removeTime})) 356 l.Error("msg", "err", io.EOF, "a", 1) 357 checkLogOutput(t, buf.String(), `level=ERROR msg=msg err=EOF a=1`) 358 buf.Reset() 359 // use local var 'args' to defeat vet check 360 args := []any{"err", io.EOF, "a"} 361 l.Error("msg", args...) 362 checkLogOutput(t, buf.String(), `level=ERROR msg=msg err=EOF !BADKEY=a`) 363 } 364 365 func TestNewLogLogger(t *testing.T) { 366 var buf bytes.Buffer 367 h := NewTextHandler(&buf, nil) 368 ll := NewLogLogger(h, LevelWarn) 369 ll.Print("hello") 370 checkLogOutput(t, buf.String(), "time="+timeRE+` level=WARN msg=hello`) 371 } 372 373 func TestLoggerNoOps(t *testing.T) { 374 l := Default() 375 if l.With() != l { 376 t.Error("wanted receiver, didn't get it") 377 } 378 if With() != l { 379 t.Error("wanted receiver, didn't get it") 380 } 381 if l.WithGroup("") != l { 382 t.Error("wanted receiver, didn't get it") 383 } 384 } 385 386 func TestContext(t *testing.T) { 387 // Verify that the context argument to log output methods is passed to the handler. 388 // Also check the level. 389 h := &captureHandler{} 390 l := New(h) 391 defer SetDefault(Default()) // restore 392 SetDefault(l) 393 394 for _, test := range []struct { 395 f func(context.Context, string, ...any) 396 wantLevel Level 397 }{ 398 {l.DebugCtx, LevelDebug}, 399 {l.InfoCtx, LevelInfo}, 400 {l.WarnCtx, LevelWarn}, 401 {l.ErrorCtx, LevelError}, 402 {DebugCtx, LevelDebug}, 403 {InfoCtx, LevelInfo}, 404 {WarnCtx, LevelWarn}, 405 {ErrorCtx, LevelError}, 406 } { 407 h.clear() 408 ctx := context.WithValue(context.Background(), "L", test.wantLevel) 409 410 test.f(ctx, "msg") 411 if gv := h.ctx.Value("L"); gv != test.wantLevel || h.r.Level != test.wantLevel { 412 t.Errorf("got context value %v, level %s; want %s for both", gv, h.r.Level, test.wantLevel) 413 } 414 } 415 } 416 417 func checkLogOutput(t *testing.T, got, wantRegexp string) { 418 t.Helper() 419 got = clean(got) 420 wantRegexp = "^" + wantRegexp + "$" 421 matched, err := regexp.MatchString(wantRegexp, got) 422 if err != nil { 423 t.Fatal(err) 424 } 425 if !matched { 426 t.Errorf("\ngot %s\nwant %s", got, wantRegexp) 427 } 428 } 429 430 // clean prepares log output for comparison. 431 func clean(s string) string { 432 if len(s) > 0 && s[len(s)-1] == '\n' { 433 s = s[:len(s)-1] 434 } 435 return strings.ReplaceAll(s, "\n", "~") 436 } 437 438 type captureHandler struct { 439 mu sync.Mutex 440 ctx context.Context 441 r Record 442 attrs []Attr 443 groups []string 444 } 445 446 func (h *captureHandler) Handle(ctx context.Context, r Record) error { 447 h.mu.Lock() 448 defer h.mu.Unlock() 449 h.ctx = ctx 450 h.r = r 451 return nil 452 } 453 454 func (*captureHandler) Enabled(context.Context, Level) bool { return true } 455 456 func (c *captureHandler) WithAttrs(as []Attr) Handler { 457 c.mu.Lock() 458 defer c.mu.Unlock() 459 var c2 captureHandler 460 c2.r = c.r 461 c2.groups = c.groups 462 c2.attrs = concat(c.attrs, as) 463 return &c2 464 } 465 466 func (c *captureHandler) WithGroup(name string) Handler { 467 c.mu.Lock() 468 defer c.mu.Unlock() 469 var c2 captureHandler 470 c2.r = c.r 471 c2.attrs = c.attrs 472 c2.groups = append(slices.Clip(c.groups), name) 473 return &c2 474 } 475 476 func (c *captureHandler) clear() { 477 c.mu.Lock() 478 defer c.mu.Unlock() 479 c.ctx = nil 480 c.r = Record{} 481 } 482 483 type discardHandler struct { 484 disabled bool 485 attrs []Attr 486 } 487 488 func (d discardHandler) Enabled(context.Context, Level) bool { return !d.disabled } 489 func (discardHandler) Handle(context.Context, Record) error { return nil } 490 func (d discardHandler) WithAttrs(as []Attr) Handler { 491 d.attrs = concat(d.attrs, as) 492 return d 493 } 494 func (h discardHandler) WithGroup(name string) Handler { 495 return h 496 } 497 498 // concat returns a new slice with the elements of s1 followed 499 // by those of s2. The slice has no additional capacity. 500 func concat[T any](s1, s2 []T) []T { 501 s := make([]T, len(s1)+len(s2)) 502 copy(s, s1) 503 copy(s[len(s1):], s2) 504 return s 505 } 506 507 // This is a simple benchmark. See the benchmarks subdirectory for more extensive ones. 508 func BenchmarkNopLog(b *testing.B) { 509 ctx := context.Background() 510 l := New(&captureHandler{}) 511 b.Run("no attrs", func(b *testing.B) { 512 b.ReportAllocs() 513 for i := 0; i < b.N; i++ { 514 l.LogAttrs(nil, LevelInfo, "msg") 515 } 516 }) 517 b.Run("attrs", func(b *testing.B) { 518 b.ReportAllocs() 519 for i := 0; i < b.N; i++ { 520 l.LogAttrs(nil, LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true)) 521 } 522 }) 523 b.Run("attrs-parallel", func(b *testing.B) { 524 b.ReportAllocs() 525 b.RunParallel(func(pb *testing.PB) { 526 for pb.Next() { 527 l.LogAttrs(nil, LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true)) 528 } 529 }) 530 }) 531 b.Run("keys-values", func(b *testing.B) { 532 b.ReportAllocs() 533 for i := 0; i < b.N; i++ { 534 l.Log(nil, LevelInfo, "msg", "a", 1, "b", "two", "c", true) 535 } 536 }) 537 b.Run("WithContext", func(b *testing.B) { 538 b.ReportAllocs() 539 for i := 0; i < b.N; i++ { 540 l.LogAttrs(ctx, LevelInfo, "msg2", Int("a", 1), String("b", "two"), Bool("c", true)) 541 } 542 }) 543 b.Run("WithContext-parallel", func(b *testing.B) { 544 b.ReportAllocs() 545 b.RunParallel(func(pb *testing.PB) { 546 for pb.Next() { 547 l.LogAttrs(ctx, LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true)) 548 } 549 }) 550 }) 551 } 552 553 // callerPC returns the program counter at the given stack depth. 554 func callerPC(depth int) uintptr { 555 var pcs [1]uintptr 556 runtime.Callers(depth, pcs[:]) 557 return pcs[0] 558 } 559 560 func wantAllocs(t *testing.T, want int, f func()) { 561 if race.Enabled { 562 t.Skip("skipping test in race mode") 563 } 564 testenv.SkipIfOptimizationOff(t) 565 t.Helper() 566 got := int(testing.AllocsPerRun(5, f)) 567 if got != want { 568 t.Errorf("got %d allocs, want %d", got, want) 569 } 570 }