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.
22 const timeRE = `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}(Z|[+-]\d{2}:\d{2})`
24 func TestLogTextHandler(t *testing.T) {
27 l := New(NewTextHandler(&buf))
29 check := func(want string) {
32 want = "time=" + timeRE + " " + want
34 checkLogOutput(t, buf.String(), want)
38 l.Info("msg", "a", 1, "b", 2)
39 check(`level=INFO msg=msg a=1 b=2`)
41 // By default, debug messages are not printed.
42 l.Debug("bg", Int("a", 1), "b", 2)
45 l.Warn("w", Duration("dur", 3*time.Second))
46 check(`level=WARN msg=w dur=3s`)
48 l.Error("bad", "a", 1)
49 check(`level=ERROR msg=bad a=1`)
51 l.Log(nil, LevelWarn+1, "w", Int("a", 1), String("b", "two"))
52 check(`level=WARN\+1 msg=w a=1 b=two`)
54 l.LogAttrs(nil, LevelInfo+1, "a b c", Int("a", 1), String("b", "two"))
55 check(`level=INFO\+1 msg="a b c" a=1 b=two`)
57 l.Info("info", "a", []Attr{Int("i", 1)})
58 check(`level=INFO msg=info a.i=1`)
60 l.Info("info", "a", GroupValue(Int("i", 1)))
61 check(`level=INFO msg=info a.i=1`)
64 func TestConnections(t *testing.T) {
65 var logbuf, slogbuf bytes.Buffer
67 // Revert any changes to the default logger. This is important because other
68 // tests might change the default logger using SetDefault. Also ensure we
69 // restore the default logger at the end of the test.
70 currentLogger := Default()
71 SetDefault(New(newDefaultHandler(log.Output)))
73 SetDefault(currentLogger)
76 // The default slog.Logger's handler uses the log package's default output.
77 log.SetOutput(&logbuf)
78 log.SetFlags(log.Lshortfile &^ log.LstdFlags)
80 checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: INFO msg a=1`)
83 checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: WARN msg b=2`)
85 Error("msg", "err", io.EOF, "c", 3)
86 checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: ERROR msg err=EOF c=3`)
88 // Levels below Info are not printed.
91 checkLogOutput(t, logbuf.String(), "")
93 t.Run("wrap default handler", func(t *testing.T) {
94 // It should be possible to wrap the default handler and get the right output.
95 // But because the call depth to log.Output is hard-coded, the source line is wrong.
96 // We want to use the pc inside the Record, but there is no way to give that to
99 // TODO(jba): when slog lives under log in the standard library, we can
100 // move the bulk of log.Logger.Output to a function in an internal
101 // package, so both log and slog can call it.
103 // While slog lives in exp, we punt.
104 t.Skip("skip until this package is in the standard library")
105 logger := New(wrappingHandler{Default().Handler()})
106 logger.Info("msg", "d", 4)
107 checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: INFO msg d=4`)
110 // Once slog.SetDefault is called, the direction is reversed: the default
111 // log.Logger's output goes through the handler.
112 SetDefault(New(HandlerOptions{AddSource: true}.NewTextHandler(&slogbuf)))
114 checkLogOutput(t, slogbuf.String(), "time="+timeRE+` level=INFO source=.*logger_test.go:\d{3} msg=msg2`)
116 // The default log.Logger always outputs at Info level.
118 SetDefault(New(HandlerOptions{Level: LevelWarn}.NewTextHandler(&slogbuf)))
119 log.Print("should not appear")
120 if got := slogbuf.String(); got != "" {
121 t.Errorf("got %q, want empty", got)
124 // Setting log's output again breaks the connection.
127 log.SetOutput(&logbuf)
128 log.SetFlags(log.Lshortfile &^ log.LstdFlags)
130 checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: msg3`)
131 if got := slogbuf.String(); got != "" {
132 t.Errorf("got %q, want empty", got)
136 type wrappingHandler struct {
140 func (h wrappingHandler) Enabled(ctx context.Context, level Level) bool {
141 return h.h.Enabled(ctx, level)
143 func (h wrappingHandler) WithGroup(name string) Handler { return h.h.WithGroup(name) }
144 func (h wrappingHandler) WithAttrs(as []Attr) Handler { return h.h.WithAttrs(as) }
145 func (h wrappingHandler) Handle(ctx context.Context, r Record) error { return h.h.Handle(ctx, r) }
147 func TestAttrs(t *testing.T) {
148 check := func(got []Attr, want ...Attr) {
150 if !attrsEqual(got, want) {
151 t.Errorf("got %v, want %v", got, want)
155 l1 := New(&captureHandler{}).With("a", 1)
156 l2 := New(l1.Handler()).With("b", 2)
158 h := l2.Handler().(*captureHandler)
159 check(h.attrs, Int("a", 1), Int("b", 2))
160 check(attrsSlice(h.r), Int("c", 3))
163 func sourceLine(r Record) (string, int) {
165 return f.File, f.Line
168 func TestCallDepth(t *testing.T) {
169 h := &captureHandler{}
172 check := func(count int) {
174 const wantFile = "logger_test.go"
175 wantLine := startLine + count*2
176 gotFile, gotLine := sourceLine(h.r)
177 gotFile = filepath.Base(gotFile)
178 if gotFile != wantFile || gotLine != wantLine {
179 t.Errorf("got (%s, %d), want (%s, %d)", gotFile, gotLine, wantFile, wantLine)
186 // Calls to check must be one line apart.
187 // Determine line where calls start.
188 f, _ := runtime.CallersFrames([]uintptr{callerPC(2)}).Next()
189 startLine = f.Line + 4
190 // Do not change the number of lines between here and the call to check(0).
192 logger.Log(nil, LevelInfo, "")
194 logger.LogAttrs(nil, LevelInfo, "")
212 Log(nil, LevelInfo, "")
214 LogAttrs(nil, LevelInfo, "")
218 func TestAlloc(t *testing.T) {
219 dl := New(discardHandler{})
220 defer func(d *Logger) { SetDefault(d) }(Default())
223 t.Run("Info", func(t *testing.T) {
224 wantAllocs(t, 0, func() { Info("hello") })
226 t.Run("Error", func(t *testing.T) {
227 wantAllocs(t, 0, func() { Error("hello") })
229 t.Run("logger.Info", func(t *testing.T) {
230 wantAllocs(t, 0, func() { dl.Info("hello") })
232 t.Run("logger.Log", func(t *testing.T) {
233 wantAllocs(t, 0, func() { dl.Log(nil, LevelDebug, "hello") })
235 t.Run("2 pairs", func(t *testing.T) {
238 wantAllocs(t, 2, func() {
245 t.Run("2 pairs disabled inline", func(t *testing.T) {
246 l := New(discardHandler{disabled: true})
249 wantAllocs(t, 2, func() {
250 l.Log(nil, LevelInfo, "hello",
256 t.Run("2 pairs disabled", func(t *testing.T) {
257 l := New(discardHandler{disabled: true})
260 wantAllocs(t, 0, func() {
261 if l.Enabled(nil, LevelInfo) {
262 l.Log(nil, LevelInfo, "hello",
269 t.Run("9 kvs", func(t *testing.T) {
273 wantAllocs(t, 11, func() {
275 "n", i, "s", s, "d", d,
276 "n", i, "s", s, "d", d,
277 "n", i, "s", s, "d", d)
280 t.Run("pairs", func(t *testing.T) {
281 wantAllocs(t, 0, func() { dl.Info("", "error", io.EOF) })
283 t.Run("attrs1", func(t *testing.T) {
284 wantAllocs(t, 0, func() { dl.LogAttrs(nil, LevelInfo, "", Int("a", 1)) })
285 wantAllocs(t, 0, func() { dl.LogAttrs(nil, LevelInfo, "", Any("error", io.EOF)) })
287 t.Run("attrs3", func(t *testing.T) {
288 wantAllocs(t, 0, func() {
289 dl.LogAttrs(nil, LevelInfo, "hello", Int("a", 1), String("b", "two"), Duration("c", time.Second))
292 t.Run("attrs3 disabled", func(t *testing.T) {
293 logger := New(discardHandler{disabled: true})
294 wantAllocs(t, 0, func() {
295 logger.LogAttrs(nil, LevelInfo, "hello", Int("a", 1), String("b", "two"), Duration("c", time.Second))
298 t.Run("attrs6", func(t *testing.T) {
299 wantAllocs(t, 1, func() {
300 dl.LogAttrs(nil, LevelInfo, "hello",
301 Int("a", 1), String("b", "two"), Duration("c", time.Second),
302 Int("d", 1), String("e", "two"), Duration("f", time.Second))
305 t.Run("attrs9", func(t *testing.T) {
306 wantAllocs(t, 1, func() {
307 dl.LogAttrs(nil, LevelInfo, "hello",
308 Int("a", 1), String("b", "two"), Duration("c", time.Second),
309 Int("d", 1), String("e", "two"), Duration("f", time.Second),
310 Int("d", 1), String("e", "two"), Duration("f", time.Second))
315 func TestSetAttrs(t *testing.T) {
316 for _, test := range []struct {
321 {[]any{"a", 1}, []Attr{Int("a", 1)}},
322 {[]any{"a", 1, "b", "two"}, []Attr{Int("a", 1), String("b", "two")}},
323 {[]any{"a"}, []Attr{String(badKey, "a")}},
324 {[]any{"a", 1, "b"}, []Attr{Int("a", 1), String(badKey, "b")}},
325 {[]any{"a", 1, 2, 3}, []Attr{Int("a", 1), Int(badKey, 2), Int(badKey, 3)}},
327 r := NewRecord(time.Time{}, 0, "", 0)
330 if !attrsEqual(got, test.want) {
331 t.Errorf("%v:\ngot %v\nwant %v", test.args, got, test.want)
336 func TestSetDefault(t *testing.T) {
337 // Verify that setting the default to itself does not result in deadlock.
338 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
340 defer func(w io.Writer) { log.SetOutput(w) }(log.Writer())
341 log.SetOutput(io.Discard)
344 SetDefault(Default())
349 if err := ctx.Err(); err != context.Canceled {
350 t.Errorf("wanted canceled, got %v", err)
354 func TestLoggerError(t *testing.T) {
357 removeTime := func(_ []string, a Attr) Attr {
358 if a.Key == TimeKey {
363 l := New(HandlerOptions{ReplaceAttr: removeTime}.NewTextHandler(&buf))
364 l.Error("msg", "err", io.EOF, "a", 1)
365 checkLogOutput(t, buf.String(), `level=ERROR msg=msg err=EOF a=1`)
367 l.Error("msg", "err", io.EOF, "a")
368 checkLogOutput(t, buf.String(), `level=ERROR msg=msg err=EOF !BADKEY=a`)
371 func TestNewLogLogger(t *testing.T) {
373 h := NewTextHandler(&buf)
374 ll := NewLogLogger(h, LevelWarn)
376 checkLogOutput(t, buf.String(), "time="+timeRE+` level=WARN msg=hello`)
379 func checkLogOutput(t *testing.T, got, wantRegexp string) {
382 wantRegexp = "^" + wantRegexp + "$"
383 matched, err := regexp.MatchString(wantRegexp, got)
388 t.Errorf("\ngot %s\nwant %s", got, wantRegexp)
392 // clean prepares log output for comparison.
393 func clean(s string) string {
394 if len(s) > 0 && s[len(s)-1] == '\n' {
397 return strings.ReplaceAll(s, "\n", "~")
400 type captureHandler struct {
407 func (h *captureHandler) Handle(ctx context.Context, r Record) error {
414 func (*captureHandler) Enabled(context.Context, Level) bool { return true }
416 func (c *captureHandler) WithAttrs(as []Attr) Handler {
419 var c2 captureHandler
422 c2.attrs = concat(c.attrs, as)
426 func (c *captureHandler) WithGroup(name string) Handler {
429 var c2 captureHandler
432 c2.groups = append(slices.Clip(c.groups), name)
436 type discardHandler struct {
441 func (d discardHandler) Enabled(context.Context, Level) bool { return !d.disabled }
442 func (discardHandler) Handle(context.Context, Record) error { return nil }
443 func (d discardHandler) WithAttrs(as []Attr) Handler {
444 d.attrs = concat(d.attrs, as)
447 func (h discardHandler) WithGroup(name string) Handler {
451 // concat returns a new slice with the elements of s1 followed
452 // by those of s2. The slice has no additional capacity.
453 func concat[T any](s1, s2 []T) []T {
454 s := make([]T, len(s1)+len(s2))
456 copy(s[len(s1):], s2)
460 // This is a simple benchmark. See the benchmarks subdirectory for more extensive ones.
461 func BenchmarkNopLog(b *testing.B) {
462 ctx := context.Background()
463 l := New(&captureHandler{})
464 b.Run("no attrs", func(b *testing.B) {
466 for i := 0; i < b.N; i++ {
467 l.LogAttrs(nil, LevelInfo, "msg")
470 b.Run("attrs", func(b *testing.B) {
472 for i := 0; i < b.N; i++ {
473 l.LogAttrs(nil, LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true))
476 b.Run("attrs-parallel", func(b *testing.B) {
478 b.RunParallel(func(pb *testing.PB) {
480 l.LogAttrs(nil, LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true))
484 b.Run("keys-values", func(b *testing.B) {
486 for i := 0; i < b.N; i++ {
487 l.Log(nil, LevelInfo, "msg", "a", 1, "b", "two", "c", true)
490 b.Run("WithContext", func(b *testing.B) {
492 for i := 0; i < b.N; i++ {
493 l.LogAttrs(ctx, LevelInfo, "msg2", Int("a", 1), String("b", "two"), Bool("c", true))
496 b.Run("WithContext-parallel", func(b *testing.B) {
498 b.RunParallel(func(pb *testing.PB) {
500 l.LogAttrs(ctx, LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true))
506 // callerPC returns the program counter at the given stack depth.
507 func callerPC(depth int) uintptr {
509 runtime.Callers(depth, pcs[:])