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))
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(HandlerOptions{AddSource: true}.NewTextHandler(&slogbuf)))
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(HandlerOptions{Level: LevelWarn}.NewTextHandler(&slogbuf)))
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 sourceLine(r Record) (string, int) {
160 return f.File, f.Line
163 func TestCallDepth(t *testing.T) {
164 h := &captureHandler{}
167 check := func(count int) {
169 const wantFile = "logger_test.go"
170 wantLine := startLine + count*2
171 gotFile, gotLine := sourceLine(h.r)
172 gotFile = filepath.Base(gotFile)
173 if gotFile != wantFile || gotLine != wantLine {
174 t.Errorf("got (%s, %d), want (%s, %d)", gotFile, gotLine, wantFile, wantLine)
181 // Calls to check must be one line apart.
182 // Determine line where calls start.
183 f, _ := runtime.CallersFrames([]uintptr{callerPC(2)}).Next()
184 startLine = f.Line + 4
185 // Do not change the number of lines between here and the call to check(0).
187 logger.Log(nil, LevelInfo, "")
189 logger.LogAttrs(nil, LevelInfo, "")
207 Log(nil, LevelInfo, "")
209 LogAttrs(nil, LevelInfo, "")
213 func TestAlloc(t *testing.T) {
214 dl := New(discardHandler{})
215 defer func(d *Logger) { SetDefault(d) }(Default())
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(nil, 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(nil, 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(nil, LevelInfo) {
257 l.Log(nil, 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(nil, LevelInfo, "", Int("a", 1)) })
280 wantAllocs(t, 0, func() { dl.LogAttrs(nil, LevelInfo, "", Any("error", io.EOF)) })
282 t.Run("attrs3", func(t *testing.T) {
283 wantAllocs(t, 0, func() {
284 dl.LogAttrs(nil, 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(nil, 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(nil, 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(nil, 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(HandlerOptions{ReplaceAttr: removeTime}.NewTextHandler(&buf))
359 l.Error("msg", "err", io.EOF, "a", 1)
360 checkLogOutput(t, buf.String(), `level=ERROR msg=msg err=EOF a=1`)
362 l.Error("msg", "err", io.EOF, "a")
363 checkLogOutput(t, buf.String(), `level=ERROR msg=msg err=EOF !BADKEY=a`)
366 func TestNewLogLogger(t *testing.T) {
368 h := NewTextHandler(&buf)
369 ll := NewLogLogger(h, LevelWarn)
371 checkLogOutput(t, buf.String(), "time="+timeRE+` level=WARN msg=hello`)
374 func checkLogOutput(t *testing.T, got, wantRegexp string) {
377 wantRegexp = "^" + wantRegexp + "$"
378 matched, err := regexp.MatchString(wantRegexp, got)
383 t.Errorf("\ngot %s\nwant %s", got, wantRegexp)
387 // clean prepares log output for comparison.
388 func clean(s string) string {
389 if len(s) > 0 && s[len(s)-1] == '\n' {
392 return strings.ReplaceAll(s, "\n", "~")
395 type captureHandler struct {
402 func (h *captureHandler) Handle(ctx context.Context, r Record) error {
409 func (*captureHandler) Enabled(context.Context, Level) bool { return true }
411 func (c *captureHandler) WithAttrs(as []Attr) Handler {
414 var c2 captureHandler
417 c2.attrs = concat(c.attrs, as)
421 func (c *captureHandler) WithGroup(name string) Handler {
424 var c2 captureHandler
427 c2.groups = append(slices.Clip(c.groups), name)
431 type discardHandler struct {
436 func (d discardHandler) Enabled(context.Context, Level) bool { return !d.disabled }
437 func (discardHandler) Handle(context.Context, Record) error { return nil }
438 func (d discardHandler) WithAttrs(as []Attr) Handler {
439 d.attrs = concat(d.attrs, as)
442 func (h discardHandler) WithGroup(name string) Handler {
446 // concat returns a new slice with the elements of s1 followed
447 // by those of s2. The slice has no additional capacity.
448 func concat[T any](s1, s2 []T) []T {
449 s := make([]T, len(s1)+len(s2))
451 copy(s[len(s1):], s2)
455 // This is a simple benchmark. See the benchmarks subdirectory for more extensive ones.
456 func BenchmarkNopLog(b *testing.B) {
457 ctx := context.Background()
458 l := New(&captureHandler{})
459 b.Run("no attrs", func(b *testing.B) {
461 for i := 0; i < b.N; i++ {
462 l.LogAttrs(nil, LevelInfo, "msg")
465 b.Run("attrs", func(b *testing.B) {
467 for i := 0; i < b.N; i++ {
468 l.LogAttrs(nil, LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true))
471 b.Run("attrs-parallel", func(b *testing.B) {
473 b.RunParallel(func(pb *testing.PB) {
475 l.LogAttrs(nil, LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true))
479 b.Run("keys-values", func(b *testing.B) {
481 for i := 0; i < b.N; i++ {
482 l.Log(nil, LevelInfo, "msg", "a", 1, "b", "two", "c", true)
485 b.Run("WithContext", func(b *testing.B) {
487 for i := 0; i < b.N; i++ {
488 l.LogAttrs(ctx, LevelInfo, "msg2", Int("a", 1), String("b", "two"), Bool("c", true))
491 b.Run("WithContext-parallel", func(b *testing.B) {
493 b.RunParallel(func(pb *testing.PB) {
495 l.LogAttrs(ctx, LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true))
501 // callerPC returns the program counter at the given stack depth.
502 func callerPC(depth int) uintptr {
504 runtime.Callers(depth, pcs[:])
508 func wantAllocs(t *testing.T, want int, f func()) {
510 t.Skip("skipping test in race mode")
512 testenv.SkipIfOptimizationOff(t)
514 got := int(testing.AllocsPerRun(5, f))
516 t.Errorf("got %d allocs, want %d", got, want)