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) {
30 l := New(NewTextHandler(&buf, nil))
32 check := func(want string) {
35 want = "time=" + timeRE + " " + want
37 checkLogOutput(t, buf.String(), want)
41 l.Info("msg", "a", 1, "b", 2)
42 check(`level=INFO msg=msg a=1 b=2`)
44 // By default, debug messages are not printed.
45 l.Debug("bg", Int("a", 1), "b", 2)
48 l.Warn("w", Duration("dur", 3*time.Second))
49 check(`level=WARN msg=w dur=3s`)
51 l.Error("bad", "a", 1)
52 check(`level=ERROR msg=bad a=1`)
54 l.Log(nil, LevelWarn+1, "w", Int("a", 1), String("b", "two"))
55 check(`level=WARN\+1 msg=w a=1 b=two`)
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`)
60 l.Info("info", "a", []Attr{Int("i", 1)})
61 check(`level=INFO msg=info a.i=1`)
63 l.Info("info", "a", GroupValue(Int("i", 1)))
64 check(`level=INFO msg=info a.i=1`)
67 func TestConnections(t *testing.T) {
68 var logbuf, slogbuf bytes.Buffer
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)))
76 SetDefault(currentLogger)
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)
83 checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: INFO msg a=1`)
86 checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: WARN msg b=2`)
88 Error("msg", "err", io.EOF, "c", 3)
89 checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: ERROR msg err=EOF c=3`)
91 // Levels below Info are not printed.
94 checkLogOutput(t, logbuf.String(), "")
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`)
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})))
109 checkLogOutput(t, slogbuf.String(), "time="+timeRE+` level=INFO source=.*logger_test.go:\d{3} msg=msg2`)
111 // The default log.Logger always outputs at Info level.
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)
119 // Setting log's output again breaks the connection.
122 log.SetOutput(&logbuf)
123 log.SetFlags(log.Lshortfile &^ log.LstdFlags)
125 checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: msg3`)
126 if got := slogbuf.String(); got != "" {
127 t.Errorf("got %q, want empty", got)
131 type wrappingHandler struct {
135 func (h wrappingHandler) Enabled(ctx context.Context, level Level) bool {
136 return h.h.Enabled(ctx, level)
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) }
142 func TestAttrs(t *testing.T) {
143 check := func(got []Attr, want ...Attr) {
145 if !attrsEqual(got, want) {
146 t.Errorf("got %v, want %v", got, want)
150 l1 := New(&captureHandler{}).With("a", 1)
151 l2 := New(l1.Handler()).With("b", 2)
153 h := l2.Handler().(*captureHandler)
154 check(h.attrs, Int("a", 1), Int("b", 2))
155 check(attrsSlice(h.r), Int("c", 3))
158 func TestCallDepth(t *testing.T) {
159 h := &captureHandler{}
162 check := func(count int) {
164 const wantFunc = "log/slog.TestCallDepth"
165 const wantFile = "logger_test.go"
166 wantLine := startLine + count*2
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)
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).
184 logger.Log(nil, LevelInfo, "")
186 logger.LogAttrs(nil, LevelInfo, "")
204 Log(nil, LevelInfo, "")
206 LogAttrs(nil, LevelInfo, "")
210 func TestAlloc(t *testing.T) {
211 dl := New(discardHandler{})
212 defer func(d *Logger) { SetDefault(d) }(Default())
215 t.Run("Info", func(t *testing.T) {
216 wantAllocs(t, 0, func() { Info("hello") })
218 t.Run("Error", func(t *testing.T) {
219 wantAllocs(t, 0, func() { Error("hello") })
221 t.Run("logger.Info", func(t *testing.T) {
222 wantAllocs(t, 0, func() { dl.Info("hello") })
224 t.Run("logger.Log", func(t *testing.T) {
225 wantAllocs(t, 0, func() { dl.Log(nil, LevelDebug, "hello") })
227 t.Run("2 pairs", func(t *testing.T) {
230 wantAllocs(t, 2, func() {
237 t.Run("2 pairs disabled inline", func(t *testing.T) {
238 l := New(discardHandler{disabled: true})
241 wantAllocs(t, 2, func() {
242 l.Log(nil, LevelInfo, "hello",
248 t.Run("2 pairs disabled", func(t *testing.T) {
249 l := New(discardHandler{disabled: true})
252 wantAllocs(t, 0, func() {
253 if l.Enabled(nil, LevelInfo) {
254 l.Log(nil, LevelInfo, "hello",
261 t.Run("9 kvs", func(t *testing.T) {
265 wantAllocs(t, 11, func() {
267 "n", i, "s", s, "d", d,
268 "n", i, "s", s, "d", d,
269 "n", i, "s", s, "d", d)
272 t.Run("pairs", func(t *testing.T) {
273 wantAllocs(t, 0, func() { dl.Info("", "error", io.EOF) })
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)) })
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))
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))
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))
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))
307 func TestSetAttrs(t *testing.T) {
308 for _, test := range []struct {
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)}},
319 r := NewRecord(time.Time{}, 0, "", 0)
322 if !attrsEqual(got, test.want) {
323 t.Errorf("%v:\ngot %v\nwant %v", test.args, got, test.want)
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)
332 defer func(w io.Writer) { log.SetOutput(w) }(log.Writer())
333 log.SetOutput(io.Discard)
336 SetDefault(Default())
341 if err := ctx.Err(); err != context.Canceled {
342 t.Errorf("wanted canceled, got %v", err)
346 func TestLoggerError(t *testing.T) {
349 removeTime := func(_ []string, a Attr) Attr {
350 if a.Key == TimeKey {
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`)
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`)
365 func TestNewLogLogger(t *testing.T) {
367 h := NewTextHandler(&buf, nil)
368 ll := NewLogLogger(h, LevelWarn)
370 checkLogOutput(t, buf.String(), "time="+timeRE+` level=WARN msg=hello`)
373 func checkLogOutput(t *testing.T, got, wantRegexp string) {
376 wantRegexp = "^" + wantRegexp + "$"
377 matched, err := regexp.MatchString(wantRegexp, got)
382 t.Errorf("\ngot %s\nwant %s", got, wantRegexp)
386 // clean prepares log output for comparison.
387 func clean(s string) string {
388 if len(s) > 0 && s[len(s)-1] == '\n' {
391 return strings.ReplaceAll(s, "\n", "~")
394 type captureHandler struct {
401 func (h *captureHandler) Handle(ctx context.Context, r Record) error {
408 func (*captureHandler) Enabled(context.Context, Level) bool { return true }
410 func (c *captureHandler) WithAttrs(as []Attr) Handler {
413 var c2 captureHandler
416 c2.attrs = concat(c.attrs, as)
420 func (c *captureHandler) WithGroup(name string) Handler {
423 var c2 captureHandler
426 c2.groups = append(slices.Clip(c.groups), name)
430 type discardHandler struct {
435 func (d discardHandler) Enabled(context.Context, Level) bool { return !d.disabled }
436 func (discardHandler) Handle(context.Context, Record) error { return nil }
437 func (d discardHandler) WithAttrs(as []Attr) Handler {
438 d.attrs = concat(d.attrs, as)
441 func (h discardHandler) WithGroup(name string) Handler {
445 // concat returns a new slice with the elements of s1 followed
446 // by those of s2. The slice has no additional capacity.
447 func concat[T any](s1, s2 []T) []T {
448 s := make([]T, len(s1)+len(s2))
450 copy(s[len(s1):], s2)
454 // This is a simple benchmark. See the benchmarks subdirectory for more extensive ones.
455 func BenchmarkNopLog(b *testing.B) {
456 ctx := context.Background()
457 l := New(&captureHandler{})
458 b.Run("no attrs", func(b *testing.B) {
460 for i := 0; i < b.N; i++ {
461 l.LogAttrs(nil, LevelInfo, "msg")
464 b.Run("attrs", func(b *testing.B) {
466 for i := 0; i < b.N; i++ {
467 l.LogAttrs(nil, LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true))
470 b.Run("attrs-parallel", func(b *testing.B) {
472 b.RunParallel(func(pb *testing.PB) {
474 l.LogAttrs(nil, LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true))
478 b.Run("keys-values", func(b *testing.B) {
480 for i := 0; i < b.N; i++ {
481 l.Log(nil, LevelInfo, "msg", "a", 1, "b", "two", "c", true)
484 b.Run("WithContext", func(b *testing.B) {
486 for i := 0; i < b.N; i++ {
487 l.LogAttrs(ctx, LevelInfo, "msg2", Int("a", 1), String("b", "two"), Bool("c", true))
490 b.Run("WithContext-parallel", func(b *testing.B) {
492 b.RunParallel(func(pb *testing.PB) {
494 l.LogAttrs(ctx, LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true))
500 // callerPC returns the program counter at the given stack depth.
501 func callerPC(depth int) uintptr {
503 runtime.Callers(depth, pcs[:])
507 func wantAllocs(t *testing.T, want int, f func()) {
509 t.Skip("skipping test in race mode")
511 testenv.SkipIfOptimizationOff(t)
513 got := int(testing.AllocsPerRun(5, f))
515 t.Errorf("got %d allocs, want %d", got, want)