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.
14 loginternal "log/internal"
25 const timeRE = `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{1,9}(Z|[+-]\d{2}:\d{2})`
27 func TestLogTextHandler(t *testing.T) {
28 ctx := context.Background()
31 l := New(NewTextHandler(&buf, nil))
33 check := func(want string) {
36 want = "time=" + timeRE + " " + want
38 checkLogOutput(t, buf.String(), want)
42 l.Info("msg", "a", 1, "b", 2)
43 check(`level=INFO msg=msg a=1 b=2`)
45 // By default, debug messages are not printed.
46 l.Debug("bg", Int("a", 1), "b", 2)
49 l.Warn("w", Duration("dur", 3*time.Second))
50 check(`level=WARN msg=w dur=3s`)
52 l.Error("bad", "a", 1)
53 check(`level=ERROR msg=bad a=1`)
55 l.Log(ctx, LevelWarn+1, "w", Int("a", 1), String("b", "two"))
56 check(`level=WARN\+1 msg=w a=1 b=two`)
58 l.LogAttrs(ctx, LevelInfo+1, "a b c", Int("a", 1), String("b", "two"))
59 check(`level=INFO\+1 msg="a b c" a=1 b=two`)
61 l.Info("info", "a", []Attr{Int("i", 1)})
62 check(`level=INFO msg=info a.i=1`)
64 l.Info("info", "a", GroupValue(Int("i", 1)))
65 check(`level=INFO msg=info a.i=1`)
68 func TestConnections(t *testing.T) {
69 var logbuf, slogbuf bytes.Buffer
71 // Revert any changes to the default logger. This is important because other
72 // tests might change the default logger using SetDefault. Also ensure we
73 // restore the default logger at the end of the test.
74 currentLogger := Default()
75 currentLogWriter := log.Writer()
76 currentLogFlags := log.Flags()
77 SetDefault(New(newDefaultHandler(loginternal.DefaultOutput)))
79 SetDefault(currentLogger)
80 log.SetOutput(currentLogWriter)
81 log.SetFlags(currentLogFlags)
84 // The default slog.Logger's handler uses the log package's default output.
85 log.SetOutput(&logbuf)
86 log.SetFlags(log.Lshortfile &^ log.LstdFlags)
88 checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: INFO msg a=1`)
91 checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: INFO msg p=<nil>`)
95 checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: INFO msg r=<nil>`)
98 checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: WARN msg b=2`)
100 Error("msg", "err", io.EOF, "c", 3)
101 checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: ERROR msg err=EOF c=3`)
103 // Levels below Info are not printed.
106 checkLogOutput(t, logbuf.String(), "")
108 t.Run("wrap default handler", func(t *testing.T) {
109 // It should be possible to wrap the default handler and get the right output.
110 // This works because the default handler uses the pc in the Record
111 // to get the source line, rather than a call depth.
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`)
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})))
121 checkLogOutput(t, slogbuf.String(), "time="+timeRE+` level=INFO source=.*logger_test.go:\d{3}"? msg=msg2`)
123 // The default log.Logger always outputs at Info level.
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)
131 // Setting log's output again breaks the connection.
134 log.SetOutput(&logbuf)
135 log.SetFlags(log.Lshortfile &^ log.LstdFlags)
137 checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: msg3`)
138 if got := slogbuf.String(); got != "" {
139 t.Errorf("got %q, want empty", got)
143 type wrappingHandler struct {
147 func (h wrappingHandler) Enabled(ctx context.Context, level Level) bool {
148 return h.h.Enabled(ctx, level)
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) }
154 func TestAttrs(t *testing.T) {
155 check := func(got []Attr, want ...Attr) {
157 if !attrsEqual(got, want) {
158 t.Errorf("got %v, want %v", got, want)
162 l1 := New(&captureHandler{}).With("a", 1)
163 l2 := New(l1.Handler()).With("b", 2)
165 h := l2.Handler().(*captureHandler)
166 check(h.attrs, Int("a", 1), Int("b", 2))
167 check(attrsSlice(h.r), Int("c", 3))
170 func TestCallDepth(t *testing.T) {
171 ctx := context.Background()
172 h := &captureHandler{}
175 check := func(count int) {
177 const wantFunc = "log/slog.TestCallDepth"
178 const wantFile = "logger_test.go"
179 wantLine := startLine + count*2
181 gotFile := filepath.Base(got.File)
182 if got.Function != wantFunc || gotFile != wantFile || got.Line != wantLine {
183 t.Errorf("got (%s, %s, %d), want (%s, %s, %d)",
184 got.Function, gotFile, got.Line, wantFunc, wantFile, wantLine)
191 // Calls to check must be one line apart.
192 // Determine line where calls start.
193 f, _ := runtime.CallersFrames([]uintptr{callerPC(2)}).Next()
194 startLine = f.Line + 4
195 // Do not change the number of lines between here and the call to check(0).
197 logger.Log(ctx, LevelInfo, "")
199 logger.LogAttrs(ctx, LevelInfo, "")
217 Log(ctx, LevelInfo, "")
219 LogAttrs(ctx, LevelInfo, "")
223 func TestAlloc(t *testing.T) {
224 ctx := context.Background()
225 dl := New(discardHandler{})
226 defer SetDefault(Default()) // restore
229 t.Run("Info", func(t *testing.T) {
230 wantAllocs(t, 0, func() { Info("hello") })
232 t.Run("Error", func(t *testing.T) {
233 wantAllocs(t, 0, func() { Error("hello") })
235 t.Run("logger.Info", func(t *testing.T) {
236 wantAllocs(t, 0, func() { dl.Info("hello") })
238 t.Run("logger.Log", func(t *testing.T) {
239 wantAllocs(t, 0, func() { dl.Log(ctx, LevelDebug, "hello") })
241 t.Run("2 pairs", func(t *testing.T) {
244 wantAllocs(t, 2, func() {
251 t.Run("2 pairs disabled inline", func(t *testing.T) {
252 l := New(discardHandler{disabled: true})
255 wantAllocs(t, 2, func() {
256 l.Log(ctx, LevelInfo, "hello",
262 t.Run("2 pairs disabled", func(t *testing.T) {
263 l := New(discardHandler{disabled: true})
266 wantAllocs(t, 0, func() {
267 if l.Enabled(ctx, LevelInfo) {
268 l.Log(ctx, LevelInfo, "hello",
275 t.Run("9 kvs", func(t *testing.T) {
279 wantAllocs(t, 10, func() {
281 "n", i, "s", s, "d", d,
282 "n", i, "s", s, "d", d,
283 "n", i, "s", s, "d", d)
286 t.Run("pairs", func(t *testing.T) {
287 wantAllocs(t, 0, func() { dl.Info("", "error", io.EOF) })
289 t.Run("attrs1", func(t *testing.T) {
290 wantAllocs(t, 0, func() { dl.LogAttrs(ctx, LevelInfo, "", Int("a", 1)) })
291 wantAllocs(t, 0, func() { dl.LogAttrs(ctx, LevelInfo, "", Any("error", io.EOF)) })
293 t.Run("attrs3", func(t *testing.T) {
294 wantAllocs(t, 0, func() {
295 dl.LogAttrs(ctx, LevelInfo, "hello", Int("a", 1), String("b", "two"), Duration("c", time.Second))
298 t.Run("attrs3 disabled", func(t *testing.T) {
299 logger := New(discardHandler{disabled: true})
300 wantAllocs(t, 0, func() {
301 logger.LogAttrs(ctx, LevelInfo, "hello", Int("a", 1), String("b", "two"), Duration("c", time.Second))
304 t.Run("attrs6", func(t *testing.T) {
305 wantAllocs(t, 1, func() {
306 dl.LogAttrs(ctx, LevelInfo, "hello",
307 Int("a", 1), String("b", "two"), Duration("c", time.Second),
308 Int("d", 1), String("e", "two"), Duration("f", time.Second))
311 t.Run("attrs9", func(t *testing.T) {
312 wantAllocs(t, 1, func() {
313 dl.LogAttrs(ctx, LevelInfo, "hello",
314 Int("a", 1), String("b", "two"), Duration("c", time.Second),
315 Int("d", 1), String("e", "two"), Duration("f", time.Second),
316 Int("d", 1), String("e", "two"), Duration("f", time.Second))
321 func TestSetAttrs(t *testing.T) {
322 for _, test := range []struct {
327 {[]any{"a", 1}, []Attr{Int("a", 1)}},
328 {[]any{"a", 1, "b", "two"}, []Attr{Int("a", 1), String("b", "two")}},
329 {[]any{"a"}, []Attr{String(badKey, "a")}},
330 {[]any{"a", 1, "b"}, []Attr{Int("a", 1), String(badKey, "b")}},
331 {[]any{"a", 1, 2, 3}, []Attr{Int("a", 1), Int(badKey, 2), Int(badKey, 3)}},
333 r := NewRecord(time.Time{}, 0, "", 0)
336 if !attrsEqual(got, test.want) {
337 t.Errorf("%v:\ngot %v\nwant %v", test.args, got, test.want)
342 func TestSetDefault(t *testing.T) {
343 // Verify that setting the default to itself does not result in deadlock.
344 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
346 defer func(w io.Writer) { log.SetOutput(w) }(log.Writer())
347 log.SetOutput(io.Discard)
350 SetDefault(Default())
355 if err := ctx.Err(); err != context.Canceled {
356 t.Errorf("wanted canceled, got %v", err)
360 func TestLoggerError(t *testing.T) {
363 removeTime := func(_ []string, a Attr) Attr {
364 if a.Key == TimeKey {
369 l := New(NewTextHandler(&buf, &HandlerOptions{ReplaceAttr: removeTime}))
370 l.Error("msg", "err", io.EOF, "a", 1)
371 checkLogOutput(t, buf.String(), `level=ERROR msg=msg err=EOF a=1`)
373 // use local var 'args' to defeat vet check
374 args := []any{"err", io.EOF, "a"}
375 l.Error("msg", args...)
376 checkLogOutput(t, buf.String(), `level=ERROR msg=msg err=EOF !BADKEY=a`)
379 func TestNewLogLogger(t *testing.T) {
381 h := NewTextHandler(&buf, nil)
382 ll := NewLogLogger(h, LevelWarn)
384 checkLogOutput(t, buf.String(), "time="+timeRE+` level=WARN msg=hello`)
387 func TestLoggerNoOps(t *testing.T) {
390 t.Error("wanted receiver, didn't get it")
393 t.Error("wanted receiver, didn't get it")
395 if l.WithGroup("") != l {
396 t.Error("wanted receiver, didn't get it")
400 func TestContext(t *testing.T) {
401 // Verify that the context argument to log output methods is passed to the handler.
402 // Also check the level.
403 h := &captureHandler{}
405 defer SetDefault(Default()) // restore
408 for _, test := range []struct {
409 f func(context.Context, string, ...any)
412 {l.DebugContext, LevelDebug},
413 {l.InfoContext, LevelInfo},
414 {l.WarnContext, LevelWarn},
415 {l.ErrorContext, LevelError},
416 {DebugContext, LevelDebug},
417 {InfoContext, LevelInfo},
418 {WarnContext, LevelWarn},
419 {ErrorContext, LevelError},
422 ctx := context.WithValue(context.Background(), "L", test.wantLevel)
425 if gv := h.ctx.Value("L"); gv != test.wantLevel || h.r.Level != test.wantLevel {
426 t.Errorf("got context value %v, level %s; want %s for both", gv, h.r.Level, test.wantLevel)
431 func checkLogOutput(t *testing.T, got, wantRegexp string) {
434 wantRegexp = "^" + wantRegexp + "$"
435 matched, err := regexp.MatchString(wantRegexp, got)
440 t.Errorf("\ngot %s\nwant %s", got, wantRegexp)
444 // clean prepares log output for comparison.
445 func clean(s string) string {
446 if len(s) > 0 && s[len(s)-1] == '\n' {
449 return strings.ReplaceAll(s, "\n", "~")
452 type captureHandler struct {
460 func (h *captureHandler) Handle(ctx context.Context, r Record) error {
468 func (*captureHandler) Enabled(context.Context, Level) bool { return true }
470 func (c *captureHandler) WithAttrs(as []Attr) Handler {
473 var c2 captureHandler
476 c2.attrs = concat(c.attrs, as)
480 func (c *captureHandler) WithGroup(name string) Handler {
483 var c2 captureHandler
486 c2.groups = append(slices.Clip(c.groups), name)
490 func (c *captureHandler) clear() {
497 type discardHandler struct {
502 func (d discardHandler) Enabled(context.Context, Level) bool { return !d.disabled }
503 func (discardHandler) Handle(context.Context, Record) error { return nil }
504 func (d discardHandler) WithAttrs(as []Attr) Handler {
505 d.attrs = concat(d.attrs, as)
508 func (h discardHandler) WithGroup(name string) Handler {
512 // concat returns a new slice with the elements of s1 followed
513 // by those of s2. The slice has no additional capacity.
514 func concat[T any](s1, s2 []T) []T {
515 s := make([]T, len(s1)+len(s2))
517 copy(s[len(s1):], s2)
521 // This is a simple benchmark. See the benchmarks subdirectory for more extensive ones.
522 func BenchmarkNopLog(b *testing.B) {
523 ctx := context.Background()
524 l := New(&captureHandler{})
525 b.Run("no attrs", func(b *testing.B) {
527 for i := 0; i < b.N; i++ {
528 l.LogAttrs(ctx, LevelInfo, "msg")
531 b.Run("attrs", func(b *testing.B) {
533 for i := 0; i < b.N; i++ {
534 l.LogAttrs(ctx, LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true))
537 b.Run("attrs-parallel", func(b *testing.B) {
539 b.RunParallel(func(pb *testing.PB) {
541 l.LogAttrs(ctx, LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true))
545 b.Run("keys-values", func(b *testing.B) {
547 for i := 0; i < b.N; i++ {
548 l.Log(ctx, LevelInfo, "msg", "a", 1, "b", "two", "c", true)
551 b.Run("WithContext", func(b *testing.B) {
553 for i := 0; i < b.N; i++ {
554 l.LogAttrs(ctx, LevelInfo, "msg2", Int("a", 1), String("b", "two"), Bool("c", true))
557 b.Run("WithContext-parallel", func(b *testing.B) {
559 b.RunParallel(func(pb *testing.PB) {
561 l.LogAttrs(ctx, LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true))
567 // callerPC returns the program counter at the given stack depth.
568 func callerPC(depth int) uintptr {
570 runtime.Callers(depth, pcs[:])
574 func wantAllocs(t *testing.T, want int, f func()) {
576 t.Skip("skipping test in race mode")
578 testenv.SkipIfOptimizationOff(t)
580 got := int(testing.AllocsPerRun(5, f))
582 t.Errorf("got %d allocs, want %d", got, want)
586 // panicTextAndJsonMarshaler is a type that panics in MarshalText and MarshalJSON.
587 type panicTextAndJsonMarshaler struct {
591 func (p panicTextAndJsonMarshaler) MarshalText() ([]byte, error) {
595 func (p panicTextAndJsonMarshaler) MarshalJSON() ([]byte, error) {
599 func TestPanics(t *testing.T) {
600 // Revert any changes to the default logger. This is important because other
601 // tests might change the default logger using SetDefault. Also ensure we
602 // restore the default logger at the end of the test.
603 currentLogger := Default()
604 currentLogWriter := log.Writer()
605 currentLogFlags := log.Flags()
607 SetDefault(currentLogger)
608 log.SetOutput(currentLogWriter)
609 log.SetFlags(currentLogFlags)
612 var logBuf bytes.Buffer
613 log.SetOutput(&logBuf)
614 log.SetFlags(log.Lshortfile &^ log.LstdFlags)
616 SetDefault(New(newDefaultHandler(loginternal.DefaultOutput)))
617 for _, pt := range []struct {
621 {(*panicTextAndJsonMarshaler)(nil), `logger_test.go:\d+: INFO msg p=<nil>`},
622 {panicTextAndJsonMarshaler{io.ErrUnexpectedEOF}, `logger_test.go:\d+: INFO msg p="!PANIC: unexpected EOF"`},
623 {panicTextAndJsonMarshaler{"panicking"}, `logger_test.go:\d+: INFO msg p="!PANIC: panicking"`},
624 {panicTextAndJsonMarshaler{42}, `logger_test.go:\d+: INFO msg p="!PANIC: 42"`},
626 Info("msg", "p", pt.in)
627 checkLogOutput(t, logBuf.String(), pt.out)
631 SetDefault(New(NewJSONHandler(&logBuf, nil)))
632 for _, pt := range []struct {
636 {(*panicTextAndJsonMarshaler)(nil), `{"time":"` + timeRE + `","level":"INFO","msg":"msg","p":null}`},
637 {panicTextAndJsonMarshaler{io.ErrUnexpectedEOF}, `{"time":"` + timeRE + `","level":"INFO","msg":"msg","p":"!PANIC: unexpected EOF"}`},
638 {panicTextAndJsonMarshaler{"panicking"}, `{"time":"` + timeRE + `","level":"INFO","msg":"msg","p":"!PANIC: panicking"}`},
639 {panicTextAndJsonMarshaler{42}, `{"time":"` + timeRE + `","level":"INFO","msg":"msg","p":"!PANIC: 42"}`},
641 Info("msg", "p", pt.in)
642 checkLogOutput(t, logBuf.String(), pt.out)