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