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{3}(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 SetDefault(New(newDefaultHandler(loginternal.DefaultOutput)))
77 SetDefault(currentLogger)
80 // The default slog.Logger's handler uses the log package's default output.
81 log.SetOutput(&logbuf)
82 log.SetFlags(log.Lshortfile &^ log.LstdFlags)
84 checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: INFO msg a=1`)
87 checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: WARN msg b=2`)
89 Error("msg", "err", io.EOF, "c", 3)
90 checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: ERROR msg err=EOF c=3`)
92 // Levels below Info are not printed.
95 checkLogOutput(t, logbuf.String(), "")
97 t.Run("wrap default handler", func(t *testing.T) {
98 // It should be possible to wrap the default handler and get the right output.
99 // This works because the default handler uses the pc in the Record
100 // to get the source line, rather than a call depth.
101 logger := New(wrappingHandler{Default().Handler()})
102 logger.Info("msg", "d", 4)
103 checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: INFO msg d=4`)
106 // Once slog.SetDefault is called, the direction is reversed: the default
107 // log.Logger's output goes through the handler.
108 SetDefault(New(NewTextHandler(&slogbuf, &HandlerOptions{AddSource: true})))
110 checkLogOutput(t, slogbuf.String(), "time="+timeRE+` level=INFO source=.*logger_test.go:\d{3}"? msg=msg2`)
112 // The default log.Logger always outputs at Info level.
114 SetDefault(New(NewTextHandler(&slogbuf, &HandlerOptions{Level: LevelWarn})))
115 log.Print("should not appear")
116 if got := slogbuf.String(); got != "" {
117 t.Errorf("got %q, want empty", got)
120 // Setting log's output again breaks the connection.
123 log.SetOutput(&logbuf)
124 log.SetFlags(log.Lshortfile &^ log.LstdFlags)
126 checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: msg3`)
127 if got := slogbuf.String(); got != "" {
128 t.Errorf("got %q, want empty", got)
132 type wrappingHandler struct {
136 func (h wrappingHandler) Enabled(ctx context.Context, level Level) bool {
137 return h.h.Enabled(ctx, level)
139 func (h wrappingHandler) WithGroup(name string) Handler { return h.h.WithGroup(name) }
140 func (h wrappingHandler) WithAttrs(as []Attr) Handler { return h.h.WithAttrs(as) }
141 func (h wrappingHandler) Handle(ctx context.Context, r Record) error { return h.h.Handle(ctx, r) }
143 func TestAttrs(t *testing.T) {
144 check := func(got []Attr, want ...Attr) {
146 if !attrsEqual(got, want) {
147 t.Errorf("got %v, want %v", got, want)
151 l1 := New(&captureHandler{}).With("a", 1)
152 l2 := New(l1.Handler()).With("b", 2)
154 h := l2.Handler().(*captureHandler)
155 check(h.attrs, Int("a", 1), Int("b", 2))
156 check(attrsSlice(h.r), Int("c", 3))
159 func TestCallDepth(t *testing.T) {
160 ctx := context.Background()
161 h := &captureHandler{}
164 check := func(count int) {
166 const wantFunc = "log/slog.TestCallDepth"
167 const wantFile = "logger_test.go"
168 wantLine := startLine + count*2
170 gotFile := filepath.Base(got.File)
171 if got.Function != wantFunc || gotFile != wantFile || got.Line != wantLine {
172 t.Errorf("got (%s, %s, %d), want (%s, %s, %d)",
173 got.Function, gotFile, got.Line, wantFunc, wantFile, wantLine)
180 // Calls to check must be one line apart.
181 // Determine line where calls start.
182 f, _ := runtime.CallersFrames([]uintptr{callerPC(2)}).Next()
183 startLine = f.Line + 4
184 // Do not change the number of lines between here and the call to check(0).
186 logger.Log(ctx, LevelInfo, "")
188 logger.LogAttrs(ctx, LevelInfo, "")
206 Log(ctx, LevelInfo, "")
208 LogAttrs(ctx, LevelInfo, "")
212 func TestAlloc(t *testing.T) {
213 ctx := context.Background()
214 dl := New(discardHandler{})
215 defer SetDefault(Default()) // restore
218 t.Run("Info", func(t *testing.T) {
219 wantAllocs(t, 0, func() { Info("hello") })
221 t.Run("Error", func(t *testing.T) {
222 wantAllocs(t, 0, func() { Error("hello") })
224 t.Run("logger.Info", func(t *testing.T) {
225 wantAllocs(t, 0, func() { dl.Info("hello") })
227 t.Run("logger.Log", func(t *testing.T) {
228 wantAllocs(t, 0, func() { dl.Log(ctx, LevelDebug, "hello") })
230 t.Run("2 pairs", func(t *testing.T) {
233 wantAllocs(t, 2, func() {
240 t.Run("2 pairs disabled inline", func(t *testing.T) {
241 l := New(discardHandler{disabled: true})
244 wantAllocs(t, 2, func() {
245 l.Log(ctx, LevelInfo, "hello",
251 t.Run("2 pairs disabled", func(t *testing.T) {
252 l := New(discardHandler{disabled: true})
255 wantAllocs(t, 0, func() {
256 if l.Enabled(ctx, LevelInfo) {
257 l.Log(ctx, LevelInfo, "hello",
264 t.Run("9 kvs", func(t *testing.T) {
268 wantAllocs(t, 11, func() {
270 "n", i, "s", s, "d", d,
271 "n", i, "s", s, "d", d,
272 "n", i, "s", s, "d", d)
275 t.Run("pairs", func(t *testing.T) {
276 wantAllocs(t, 0, func() { dl.Info("", "error", io.EOF) })
278 t.Run("attrs1", func(t *testing.T) {
279 wantAllocs(t, 0, func() { dl.LogAttrs(ctx, LevelInfo, "", Int("a", 1)) })
280 wantAllocs(t, 0, func() { dl.LogAttrs(ctx, LevelInfo, "", Any("error", io.EOF)) })
282 t.Run("attrs3", func(t *testing.T) {
283 wantAllocs(t, 0, func() {
284 dl.LogAttrs(ctx, LevelInfo, "hello", Int("a", 1), String("b", "two"), Duration("c", time.Second))
287 t.Run("attrs3 disabled", func(t *testing.T) {
288 logger := New(discardHandler{disabled: true})
289 wantAllocs(t, 0, func() {
290 logger.LogAttrs(ctx, LevelInfo, "hello", Int("a", 1), String("b", "two"), Duration("c", time.Second))
293 t.Run("attrs6", func(t *testing.T) {
294 wantAllocs(t, 1, func() {
295 dl.LogAttrs(ctx, LevelInfo, "hello",
296 Int("a", 1), String("b", "two"), Duration("c", time.Second),
297 Int("d", 1), String("e", "two"), Duration("f", time.Second))
300 t.Run("attrs9", func(t *testing.T) {
301 wantAllocs(t, 1, func() {
302 dl.LogAttrs(ctx, LevelInfo, "hello",
303 Int("a", 1), String("b", "two"), Duration("c", time.Second),
304 Int("d", 1), String("e", "two"), Duration("f", time.Second),
305 Int("d", 1), String("e", "two"), Duration("f", time.Second))
310 func TestSetAttrs(t *testing.T) {
311 for _, test := range []struct {
316 {[]any{"a", 1}, []Attr{Int("a", 1)}},
317 {[]any{"a", 1, "b", "two"}, []Attr{Int("a", 1), String("b", "two")}},
318 {[]any{"a"}, []Attr{String(badKey, "a")}},
319 {[]any{"a", 1, "b"}, []Attr{Int("a", 1), String(badKey, "b")}},
320 {[]any{"a", 1, 2, 3}, []Attr{Int("a", 1), Int(badKey, 2), Int(badKey, 3)}},
322 r := NewRecord(time.Time{}, 0, "", 0)
325 if !attrsEqual(got, test.want) {
326 t.Errorf("%v:\ngot %v\nwant %v", test.args, got, test.want)
331 func TestSetDefault(t *testing.T) {
332 // Verify that setting the default to itself does not result in deadlock.
333 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
335 defer func(w io.Writer) { log.SetOutput(w) }(log.Writer())
336 log.SetOutput(io.Discard)
339 SetDefault(Default())
344 if err := ctx.Err(); err != context.Canceled {
345 t.Errorf("wanted canceled, got %v", err)
349 func TestLoggerError(t *testing.T) {
352 removeTime := func(_ []string, a Attr) Attr {
353 if a.Key == TimeKey {
358 l := New(NewTextHandler(&buf, &HandlerOptions{ReplaceAttr: removeTime}))
359 l.Error("msg", "err", io.EOF, "a", 1)
360 checkLogOutput(t, buf.String(), `level=ERROR msg=msg err=EOF a=1`)
362 // use local var 'args' to defeat vet check
363 args := []any{"err", io.EOF, "a"}
364 l.Error("msg", args...)
365 checkLogOutput(t, buf.String(), `level=ERROR msg=msg err=EOF !BADKEY=a`)
368 func TestNewLogLogger(t *testing.T) {
370 h := NewTextHandler(&buf, nil)
371 ll := NewLogLogger(h, LevelWarn)
373 checkLogOutput(t, buf.String(), "time="+timeRE+` level=WARN msg=hello`)
376 func TestLoggerNoOps(t *testing.T) {
379 t.Error("wanted receiver, didn't get it")
382 t.Error("wanted receiver, didn't get it")
384 if l.WithGroup("") != l {
385 t.Error("wanted receiver, didn't get it")
389 func TestContext(t *testing.T) {
390 // Verify that the context argument to log output methods is passed to the handler.
391 // Also check the level.
392 h := &captureHandler{}
394 defer SetDefault(Default()) // restore
397 for _, test := range []struct {
398 f func(context.Context, string, ...any)
401 {l.DebugContext, LevelDebug},
402 {l.InfoContext, LevelInfo},
403 {l.WarnContext, LevelWarn},
404 {l.ErrorContext, LevelError},
405 {DebugContext, LevelDebug},
406 {InfoContext, LevelInfo},
407 {WarnContext, LevelWarn},
408 {ErrorContext, LevelError},
411 ctx := context.WithValue(context.Background(), "L", test.wantLevel)
414 if gv := h.ctx.Value("L"); gv != test.wantLevel || h.r.Level != test.wantLevel {
415 t.Errorf("got context value %v, level %s; want %s for both", gv, h.r.Level, test.wantLevel)
420 func checkLogOutput(t *testing.T, got, wantRegexp string) {
423 wantRegexp = "^" + wantRegexp + "$"
424 matched, err := regexp.MatchString(wantRegexp, got)
429 t.Errorf("\ngot %s\nwant %s", got, wantRegexp)
433 // clean prepares log output for comparison.
434 func clean(s string) string {
435 if len(s) > 0 && s[len(s)-1] == '\n' {
438 return strings.ReplaceAll(s, "\n", "~")
441 type captureHandler struct {
449 func (h *captureHandler) Handle(ctx context.Context, r Record) error {
457 func (*captureHandler) Enabled(context.Context, Level) bool { return true }
459 func (c *captureHandler) WithAttrs(as []Attr) Handler {
462 var c2 captureHandler
465 c2.attrs = concat(c.attrs, as)
469 func (c *captureHandler) WithGroup(name string) Handler {
472 var c2 captureHandler
475 c2.groups = append(slices.Clip(c.groups), name)
479 func (c *captureHandler) clear() {
486 type discardHandler struct {
491 func (d discardHandler) Enabled(context.Context, Level) bool { return !d.disabled }
492 func (discardHandler) Handle(context.Context, Record) error { return nil }
493 func (d discardHandler) WithAttrs(as []Attr) Handler {
494 d.attrs = concat(d.attrs, as)
497 func (h discardHandler) WithGroup(name string) Handler {
501 // concat returns a new slice with the elements of s1 followed
502 // by those of s2. The slice has no additional capacity.
503 func concat[T any](s1, s2 []T) []T {
504 s := make([]T, len(s1)+len(s2))
506 copy(s[len(s1):], s2)
510 // This is a simple benchmark. See the benchmarks subdirectory for more extensive ones.
511 func BenchmarkNopLog(b *testing.B) {
512 ctx := context.Background()
513 l := New(&captureHandler{})
514 b.Run("no attrs", func(b *testing.B) {
516 for i := 0; i < b.N; i++ {
517 l.LogAttrs(ctx, LevelInfo, "msg")
520 b.Run("attrs", func(b *testing.B) {
522 for i := 0; i < b.N; i++ {
523 l.LogAttrs(ctx, LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true))
526 b.Run("attrs-parallel", func(b *testing.B) {
528 b.RunParallel(func(pb *testing.PB) {
530 l.LogAttrs(ctx, LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true))
534 b.Run("keys-values", func(b *testing.B) {
536 for i := 0; i < b.N; i++ {
537 l.Log(ctx, LevelInfo, "msg", "a", 1, "b", "two", "c", true)
540 b.Run("WithContext", func(b *testing.B) {
542 for i := 0; i < b.N; i++ {
543 l.LogAttrs(ctx, LevelInfo, "msg2", Int("a", 1), String("b", "two"), Bool("c", true))
546 b.Run("WithContext-parallel", func(b *testing.B) {
548 b.RunParallel(func(pb *testing.PB) {
550 l.LogAttrs(ctx, LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true))
556 // callerPC returns the program counter at the given stack depth.
557 func callerPC(depth int) uintptr {
559 runtime.Callers(depth, pcs[:])
563 func wantAllocs(t *testing.T, want int, f func()) {
565 t.Skip("skipping test in race mode")
567 testenv.SkipIfOptimizationOff(t)
569 got := int(testing.AllocsPerRun(5, f))
571 t.Errorf("got %d allocs, want %d", got, want)