1
2
3
4
5 package slog
6
7 import (
8 "bytes"
9 "context"
10 "io"
11 "log"
12 "path/filepath"
13 "regexp"
14 "runtime"
15 "strings"
16 "sync"
17 "testing"
18 "time"
19
20 "golang.org/x/exp/slices"
21 )
22
23
24
25 const textTimeRE = `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}(Z|[+-]\d{2}:\d{2})`
26
27
28
29 const jsonTimeRE = `\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})`
30
31 func TestLogTextHandler(t *testing.T) {
32 var buf bytes.Buffer
33
34 l := New(NewTextHandler(&buf, nil))
35
36 check := func(want string) {
37 t.Helper()
38 if want != "" {
39 want = "time=" + textTimeRE + " " + want
40 }
41 checkLogOutput(t, buf.String(), want)
42 buf.Reset()
43 }
44
45 l.Info("msg", "a", 1, "b", 2)
46 check(`level=INFO msg=msg a=1 b=2`)
47
48
49 l.Debug("bg", Int("a", 1), "b", 2)
50 check("")
51
52 l.Warn("w", Duration("dur", 3*time.Second))
53 check(`level=WARN msg=w dur=3s`)
54
55 l.Error("bad", "a", 1)
56 check(`level=ERROR msg=bad a=1`)
57
58 l.Log(nil, LevelWarn+1, "w", Int("a", 1), String("b", "two"))
59 check(`level=WARN\+1 msg=w a=1 b=two`)
60
61 l.LogAttrs(nil, LevelInfo+1, "a b c", Int("a", 1), String("b", "two"))
62 check(`level=INFO\+1 msg="a b c" a=1 b=two`)
63
64 l.Info("info", "a", []Attr{Int("i", 1)})
65 check(`level=INFO msg=info a.i=1`)
66
67 l.Info("info", "a", GroupValue(Int("i", 1)))
68 check(`level=INFO msg=info a.i=1`)
69 }
70
71 func TestConnections(t *testing.T) {
72 var logbuf, slogbuf bytes.Buffer
73
74
75
76
77 currentLogger := Default()
78 SetDefault(New(newDefaultHandler(log.Output)))
79 t.Cleanup(func() {
80 SetDefault(currentLogger)
81 })
82
83
84 log.SetOutput(&logbuf)
85 log.SetFlags(log.Lshortfile &^ log.LstdFlags)
86 Info("msg", "a", 1)
87 checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: INFO msg a=1`)
88 logbuf.Reset()
89 Warn("msg", "b", 2)
90 checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: WARN msg b=2`)
91 logbuf.Reset()
92 Error("msg", "err", io.EOF, "c", 3)
93 checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: ERROR msg err=EOF c=3`)
94
95
96 logbuf.Reset()
97 Debug("msg", "c", 3)
98 checkLogOutput(t, logbuf.String(), "")
99
100 t.Run("wrap default handler", func(t *testing.T) {
101
102
103
104
105
106
107
108
109
110
111 t.Skip("skip until this package is in the standard library")
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`)
115 })
116
117
118
119 SetDefault(New(NewTextHandler(&slogbuf, &HandlerOptions{AddSource: true})))
120 log.Print("msg2")
121 checkLogOutput(t, slogbuf.String(), "time="+textTimeRE+` level=INFO source=.*logger_test.go:\d{3} msg=msg2`)
122
123
124 slogbuf.Reset()
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)
129 }
130
131
132 logbuf.Reset()
133 slogbuf.Reset()
134 log.SetOutput(&logbuf)
135 log.SetFlags(log.Lshortfile &^ log.LstdFlags)
136 log.Print("msg3")
137 checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: msg3`)
138 if got := slogbuf.String(); got != "" {
139 t.Errorf("got %q, want empty", got)
140 }
141 }
142
143 type wrappingHandler struct {
144 h Handler
145 }
146
147 func (h wrappingHandler) Enabled(ctx context.Context, level Level) bool {
148 return h.h.Enabled(ctx, level)
149 }
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) }
153
154 func TestAttrs(t *testing.T) {
155 check := func(got []Attr, want ...Attr) {
156 t.Helper()
157 if !attrsEqual(got, want) {
158 t.Errorf("got %v, want %v", got, want)
159 }
160 }
161
162 l1 := New(&captureHandler{}).With("a", 1)
163 l2 := New(l1.Handler()).With("b", 2)
164 l2.Info("m", "c", 3)
165 h := l2.Handler().(*captureHandler)
166 check(h.attrs, Int("a", 1), Int("b", 2))
167 check(attrsSlice(h.r), Int("c", 3))
168 }
169
170 func TestCallDepth(t *testing.T) {
171 h := &captureHandler{}
172 var startLine int
173
174 check := func(count int) {
175 t.Helper()
176 const wantFunc = "golang.org/x/exp/slog.TestCallDepth"
177 const wantFile = "logger_test.go"
178 wantLine := startLine + count*2
179 got := h.r.source()
180 gotFile := filepath.Base(got.File)
181 if got.Function != wantFunc || gotFile != wantFile || got.Line != wantLine {
182 t.Errorf("got (%s, %s, %d), want (%s, %s, %d)",
183 got.Function, gotFile, got.Line, wantFunc, wantFile, wantLine)
184 }
185 }
186
187 logger := New(h)
188 SetDefault(logger)
189
190
191
192 f, _ := runtime.CallersFrames([]uintptr{callerPC(2)}).Next()
193 startLine = f.Line + 4
194
195
196 logger.Log(nil, LevelInfo, "")
197 check(0)
198 logger.LogAttrs(nil, LevelInfo, "")
199 check(1)
200 logger.Debug("")
201 check(2)
202 logger.Info("")
203 check(3)
204 logger.Warn("")
205 check(4)
206 logger.Error("")
207 check(5)
208 Debug("")
209 check(6)
210 Info("")
211 check(7)
212 Warn("")
213 check(8)
214 Error("")
215 check(9)
216 Log(nil, LevelInfo, "")
217 check(10)
218 LogAttrs(nil, LevelInfo, "")
219 check(11)
220 }
221
222 func TestAlloc(t *testing.T) {
223 dl := New(discardHandler{})
224 defer func(d *Logger) { SetDefault(d) }(Default())
225 SetDefault(dl)
226
227 t.Run("Info", func(t *testing.T) {
228 wantAllocs(t, 0, func() { Info("hello") })
229 })
230 t.Run("Error", func(t *testing.T) {
231 wantAllocs(t, 0, func() { Error("hello") })
232 })
233 t.Run("logger.Info", func(t *testing.T) {
234 wantAllocs(t, 0, func() { dl.Info("hello") })
235 })
236 t.Run("logger.Log", func(t *testing.T) {
237 wantAllocs(t, 0, func() { dl.Log(nil, LevelDebug, "hello") })
238 })
239 t.Run("2 pairs", func(t *testing.T) {
240 s := "abc"
241 i := 2000
242 wantAllocs(t, 2, func() {
243 dl.Info("hello",
244 "n", i,
245 "s", s,
246 )
247 })
248 })
249 t.Run("2 pairs disabled inline", func(t *testing.T) {
250 l := New(discardHandler{disabled: true})
251 s := "abc"
252 i := 2000
253 wantAllocs(t, 2, func() {
254 l.Log(nil, LevelInfo, "hello",
255 "n", i,
256 "s", s,
257 )
258 })
259 })
260 t.Run("2 pairs disabled", func(t *testing.T) {
261 l := New(discardHandler{disabled: true})
262 s := "abc"
263 i := 2000
264 wantAllocs(t, 0, func() {
265 if l.Enabled(nil, LevelInfo) {
266 l.Log(nil, LevelInfo, "hello",
267 "n", i,
268 "s", s,
269 )
270 }
271 })
272 })
273 t.Run("9 kvs", func(t *testing.T) {
274 s := "abc"
275 i := 2000
276 d := time.Second
277 wantAllocs(t, 11, func() {
278 dl.Info("hello",
279 "n", i, "s", s, "d", d,
280 "n", i, "s", s, "d", d,
281 "n", i, "s", s, "d", d)
282 })
283 })
284 t.Run("pairs", func(t *testing.T) {
285 wantAllocs(t, 0, func() { dl.Info("", "error", io.EOF) })
286 })
287 t.Run("attrs1", func(t *testing.T) {
288 wantAllocs(t, 0, func() { dl.LogAttrs(nil, LevelInfo, "", Int("a", 1)) })
289 wantAllocs(t, 0, func() { dl.LogAttrs(nil, LevelInfo, "", Any("error", io.EOF)) })
290 })
291 t.Run("attrs3", func(t *testing.T) {
292 wantAllocs(t, 0, func() {
293 dl.LogAttrs(nil, LevelInfo, "hello", Int("a", 1), String("b", "two"), Duration("c", time.Second))
294 })
295 })
296 t.Run("attrs3 disabled", func(t *testing.T) {
297 logger := New(discardHandler{disabled: true})
298 wantAllocs(t, 0, func() {
299 logger.LogAttrs(nil, LevelInfo, "hello", Int("a", 1), String("b", "two"), Duration("c", time.Second))
300 })
301 })
302 t.Run("attrs6", func(t *testing.T) {
303 wantAllocs(t, 1, func() {
304 dl.LogAttrs(nil, LevelInfo, "hello",
305 Int("a", 1), String("b", "two"), Duration("c", time.Second),
306 Int("d", 1), String("e", "two"), Duration("f", time.Second))
307 })
308 })
309 t.Run("attrs9", func(t *testing.T) {
310 wantAllocs(t, 1, func() {
311 dl.LogAttrs(nil, LevelInfo, "hello",
312 Int("a", 1), String("b", "two"), Duration("c", time.Second),
313 Int("d", 1), String("e", "two"), Duration("f", time.Second),
314 Int("d", 1), String("e", "two"), Duration("f", time.Second))
315 })
316 })
317 }
318
319 func TestSetAttrs(t *testing.T) {
320 for _, test := range []struct {
321 args []any
322 want []Attr
323 }{
324 {nil, nil},
325 {[]any{"a", 1}, []Attr{Int("a", 1)}},
326 {[]any{"a", 1, "b", "two"}, []Attr{Int("a", 1), String("b", "two")}},
327 {[]any{"a"}, []Attr{String(badKey, "a")}},
328 {[]any{"a", 1, "b"}, []Attr{Int("a", 1), String(badKey, "b")}},
329 {[]any{"a", 1, 2, 3}, []Attr{Int("a", 1), Int(badKey, 2), Int(badKey, 3)}},
330 } {
331 r := NewRecord(time.Time{}, 0, "", 0)
332 r.Add(test.args...)
333 got := attrsSlice(r)
334 if !attrsEqual(got, test.want) {
335 t.Errorf("%v:\ngot %v\nwant %v", test.args, got, test.want)
336 }
337 }
338 }
339
340 func TestSetDefault(t *testing.T) {
341
342 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
343 defer cancel()
344 defer func(w io.Writer) { log.SetOutput(w) }(log.Writer())
345 log.SetOutput(io.Discard)
346 go func() {
347 Info("A")
348 SetDefault(Default())
349 Info("B")
350 cancel()
351 }()
352 <-ctx.Done()
353 if err := ctx.Err(); err != context.Canceled {
354 t.Errorf("wanted canceled, got %v", err)
355 }
356 }
357
358 func TestLoggerError(t *testing.T) {
359 var buf bytes.Buffer
360
361 removeTime := func(_ []string, a Attr) Attr {
362 if a.Key == TimeKey {
363 return Attr{}
364 }
365 return a
366 }
367 l := New(NewTextHandler(&buf, &HandlerOptions{ReplaceAttr: removeTime}))
368 l.Error("msg", "err", io.EOF, "a", 1)
369 checkLogOutput(t, buf.String(), `level=ERROR msg=msg err=EOF a=1`)
370 buf.Reset()
371 l.Error("msg", "err", io.EOF, "a")
372 checkLogOutput(t, buf.String(), `level=ERROR msg=msg err=EOF !BADKEY=a`)
373 }
374
375 func TestNewLogLogger(t *testing.T) {
376 var buf bytes.Buffer
377 h := NewTextHandler(&buf, nil)
378 ll := NewLogLogger(h, LevelWarn)
379 ll.Print("hello")
380 checkLogOutput(t, buf.String(), "time="+textTimeRE+` level=WARN msg=hello`)
381 }
382
383 func checkLogOutput(t *testing.T, got, wantRegexp string) {
384 t.Helper()
385 got = clean(got)
386 wantRegexp = "^" + wantRegexp + "$"
387 matched, err := regexp.MatchString(wantRegexp, got)
388 if err != nil {
389 t.Fatal(err)
390 }
391 if !matched {
392 t.Errorf("\ngot %s\nwant %s", got, wantRegexp)
393 }
394 }
395
396
397 func clean(s string) string {
398 if len(s) > 0 && s[len(s)-1] == '\n' {
399 s = s[:len(s)-1]
400 }
401 return strings.ReplaceAll(s, "\n", "~")
402 }
403
404 type captureHandler struct {
405 mu sync.Mutex
406 r Record
407 attrs []Attr
408 groups []string
409 }
410
411 func (h *captureHandler) Handle(ctx context.Context, r Record) error {
412 h.mu.Lock()
413 defer h.mu.Unlock()
414 h.r = r
415 return nil
416 }
417
418 func (*captureHandler) Enabled(context.Context, Level) bool { return true }
419
420 func (c *captureHandler) WithAttrs(as []Attr) Handler {
421 c.mu.Lock()
422 defer c.mu.Unlock()
423 var c2 captureHandler
424 c2.r = c.r
425 c2.groups = c.groups
426 c2.attrs = concat(c.attrs, as)
427 return &c2
428 }
429
430 func (c *captureHandler) WithGroup(name string) Handler {
431 c.mu.Lock()
432 defer c.mu.Unlock()
433 var c2 captureHandler
434 c2.r = c.r
435 c2.attrs = c.attrs
436 c2.groups = append(slices.Clip(c.groups), name)
437 return &c2
438 }
439
440 type discardHandler struct {
441 disabled bool
442 attrs []Attr
443 }
444
445 func (d discardHandler) Enabled(context.Context, Level) bool { return !d.disabled }
446 func (discardHandler) Handle(context.Context, Record) error { return nil }
447 func (d discardHandler) WithAttrs(as []Attr) Handler {
448 d.attrs = concat(d.attrs, as)
449 return d
450 }
451 func (h discardHandler) WithGroup(name string) Handler {
452 return h
453 }
454
455
456
457 func concat[T any](s1, s2 []T) []T {
458 s := make([]T, len(s1)+len(s2))
459 copy(s, s1)
460 copy(s[len(s1):], s2)
461 return s
462 }
463
464
465 func BenchmarkNopLog(b *testing.B) {
466 ctx := context.Background()
467 l := New(&captureHandler{})
468 b.Run("no attrs", func(b *testing.B) {
469 b.ReportAllocs()
470 for i := 0; i < b.N; i++ {
471 l.LogAttrs(nil, LevelInfo, "msg")
472 }
473 })
474 b.Run("attrs", func(b *testing.B) {
475 b.ReportAllocs()
476 for i := 0; i < b.N; i++ {
477 l.LogAttrs(nil, LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true))
478 }
479 })
480 b.Run("attrs-parallel", func(b *testing.B) {
481 b.ReportAllocs()
482 b.RunParallel(func(pb *testing.PB) {
483 for pb.Next() {
484 l.LogAttrs(nil, LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true))
485 }
486 })
487 })
488 b.Run("keys-values", func(b *testing.B) {
489 b.ReportAllocs()
490 for i := 0; i < b.N; i++ {
491 l.Log(nil, LevelInfo, "msg", "a", 1, "b", "two", "c", true)
492 }
493 })
494 b.Run("WithContext", func(b *testing.B) {
495 b.ReportAllocs()
496 for i := 0; i < b.N; i++ {
497 l.LogAttrs(ctx, LevelInfo, "msg2", Int("a", 1), String("b", "two"), Bool("c", true))
498 }
499 })
500 b.Run("WithContext-parallel", func(b *testing.B) {
501 b.ReportAllocs()
502 b.RunParallel(func(pb *testing.PB) {
503 for pb.Next() {
504 l.LogAttrs(ctx, LevelInfo, "msg", Int("a", 1), String("b", "two"), Bool("c", true))
505 }
506 })
507 })
508 }
509
510
511 func callerPC(depth int) uintptr {
512 var pcs [1]uintptr
513 runtime.Callers(depth, pcs[:])
514 return pcs[0]
515 }
516
517
518 type panicTextAndJsonMarshaler struct {
519 msg any
520 }
521
522 func (p panicTextAndJsonMarshaler) MarshalText() ([]byte, error) {
523 panic(p.msg)
524 }
525
526 func (p panicTextAndJsonMarshaler) MarshalJSON() ([]byte, error) {
527 panic(p.msg)
528 }
529
530 func TestPanics(t *testing.T) {
531
532
533
534 currentLogger := Default()
535 currentLogWriter := log.Writer()
536 currentLogFlags := log.Flags()
537 t.Cleanup(func() {
538 SetDefault(currentLogger)
539 log.SetOutput(currentLogWriter)
540 log.SetFlags(currentLogFlags)
541 })
542
543 var logBuf bytes.Buffer
544 log.SetOutput(&logBuf)
545 log.SetFlags(log.Lshortfile &^ log.LstdFlags)
546
547 SetDefault(New(newDefaultHandler(log.Output)))
548 for _, pt := range []struct {
549 in any
550 out string
551 }{
552 {(*panicTextAndJsonMarshaler)(nil), `logger_test.go:\d+: INFO msg p=<nil>`},
553 {panicTextAndJsonMarshaler{io.ErrUnexpectedEOF}, `logger_test.go:\d+: INFO msg p="!PANIC: unexpected EOF"`},
554 {panicTextAndJsonMarshaler{"panicking"}, `logger_test.go:\d+: INFO msg p="!PANIC: panicking"`},
555 {panicTextAndJsonMarshaler{42}, `logger_test.go:\d+: INFO msg p="!PANIC: 42"`},
556 } {
557 Info("msg", "p", pt.in)
558 checkLogOutput(t, logBuf.String(), pt.out)
559 logBuf.Reset()
560 }
561
562 SetDefault(New(NewJSONHandler(&logBuf, nil)))
563 for _, pt := range []struct {
564 in any
565 out string
566 }{
567 {(*panicTextAndJsonMarshaler)(nil), `{"time":"` + jsonTimeRE + `","level":"INFO","msg":"msg","p":null}`},
568 {panicTextAndJsonMarshaler{io.ErrUnexpectedEOF}, `{"time":"` + jsonTimeRE + `","level":"INFO","msg":"msg","p":"!PANIC: unexpected EOF"}`},
569 {panicTextAndJsonMarshaler{"panicking"}, `{"time":"` + jsonTimeRE + `","level":"INFO","msg":"msg","p":"!PANIC: panicking"}`},
570 {panicTextAndJsonMarshaler{42}, `{"time":"` + jsonTimeRE + `","level":"INFO","msg":"msg","p":"!PANIC: 42"}`},
571 } {
572 Info("msg", "p", pt.in)
573 checkLogOutput(t, logBuf.String(), pt.out)
574 logBuf.Reset()
575 }
576 }
577
View as plain text