Source file
src/log/slog/handler_test.go
1
2
3
4
5 package slog
6
7 import (
8 "bytes"
9 "context"
10 "encoding/json"
11 "fmt"
12 "io"
13 "log/slog/internal/buffer"
14 "os"
15 "path/filepath"
16 "slices"
17 "strconv"
18 "strings"
19 "sync"
20 "testing"
21 "time"
22 )
23
24 func TestDefaultHandle(t *testing.T) {
25 ctx := context.Background()
26 preAttrs := []Attr{Int("pre", 0)}
27 attrs := []Attr{Int("a", 1), String("b", "two")}
28 for _, test := range []struct {
29 name string
30 with func(Handler) Handler
31 attrs []Attr
32 want string
33 }{
34 {
35 name: "no attrs",
36 want: "INFO message",
37 },
38 {
39 name: "attrs",
40 attrs: attrs,
41 want: "INFO message a=1 b=two",
42 },
43 {
44 name: "preformatted",
45 with: func(h Handler) Handler { return h.WithAttrs(preAttrs) },
46 attrs: attrs,
47 want: "INFO message pre=0 a=1 b=two",
48 },
49 {
50 name: "groups",
51 attrs: []Attr{
52 Int("a", 1),
53 Group("g",
54 Int("b", 2),
55 Group("h", Int("c", 3)),
56 Int("d", 4)),
57 Int("e", 5),
58 },
59 want: "INFO message a=1 g.b=2 g.h.c=3 g.d=4 e=5",
60 },
61 {
62 name: "group",
63 with: func(h Handler) Handler { return h.WithAttrs(preAttrs).WithGroup("s") },
64 attrs: attrs,
65 want: "INFO message pre=0 s.a=1 s.b=two",
66 },
67 {
68 name: "preformatted groups",
69 with: func(h Handler) Handler {
70 return h.WithAttrs([]Attr{Int("p1", 1)}).
71 WithGroup("s1").
72 WithAttrs([]Attr{Int("p2", 2)}).
73 WithGroup("s2")
74 },
75 attrs: attrs,
76 want: "INFO message p1=1 s1.p2=2 s1.s2.a=1 s1.s2.b=two",
77 },
78 {
79 name: "two with-groups",
80 with: func(h Handler) Handler {
81 return h.WithAttrs([]Attr{Int("p1", 1)}).
82 WithGroup("s1").
83 WithGroup("s2")
84 },
85 attrs: attrs,
86 want: "INFO message p1=1 s1.s2.a=1 s1.s2.b=two",
87 },
88 } {
89 t.Run(test.name, func(t *testing.T) {
90 var got string
91 var h Handler = newDefaultHandler(func(_ uintptr, b []byte) error {
92 got = string(b)
93 return nil
94 })
95 if test.with != nil {
96 h = test.with(h)
97 }
98 r := NewRecord(time.Time{}, LevelInfo, "message", 0)
99 r.AddAttrs(test.attrs...)
100 if err := h.Handle(ctx, r); err != nil {
101 t.Fatal(err)
102 }
103 if got != test.want {
104 t.Errorf("\ngot %s\nwant %s", got, test.want)
105 }
106 })
107 }
108 }
109
110 func TestConcurrentWrites(t *testing.T) {
111 ctx := context.Background()
112 count := 1000
113 for _, handlerType := range []string{"text", "json"} {
114 t.Run(handlerType, func(t *testing.T) {
115 var buf bytes.Buffer
116 var h Handler
117 switch handlerType {
118 case "text":
119 h = NewTextHandler(&buf, nil)
120 case "json":
121 h = NewJSONHandler(&buf, nil)
122 default:
123 t.Fatalf("unexpected handlerType %q", handlerType)
124 }
125 sub1 := h.WithAttrs([]Attr{Bool("sub1", true)})
126 sub2 := h.WithAttrs([]Attr{Bool("sub2", true)})
127 var wg sync.WaitGroup
128 for i := 0; i < count; i++ {
129 sub1Record := NewRecord(time.Time{}, LevelInfo, "hello from sub1", 0)
130 sub1Record.AddAttrs(Int("i", i))
131 sub2Record := NewRecord(time.Time{}, LevelInfo, "hello from sub2", 0)
132 sub2Record.AddAttrs(Int("i", i))
133 wg.Add(1)
134 go func() {
135 defer wg.Done()
136 if err := sub1.Handle(ctx, sub1Record); err != nil {
137 t.Error(err)
138 }
139 if err := sub2.Handle(ctx, sub2Record); err != nil {
140 t.Error(err)
141 }
142 }()
143 }
144 wg.Wait()
145 for i := 1; i <= 2; i++ {
146 want := "hello from sub" + strconv.Itoa(i)
147 n := strings.Count(buf.String(), want)
148 if n != count {
149 t.Fatalf("want %d occurrences of %q, got %d", count, want, n)
150 }
151 }
152 })
153 }
154 }
155
156
157 func TestJSONAndTextHandlers(t *testing.T) {
158
159 removeAll := func(_ []string, a Attr) Attr { return Attr{} }
160
161 attrs := []Attr{String("a", "one"), Int("b", 2), Any("", nil)}
162 preAttrs := []Attr{Int("pre", 3), String("x", "y")}
163
164 for _, test := range []struct {
165 name string
166 replace func([]string, Attr) Attr
167 addSource bool
168 with func(Handler) Handler
169 preAttrs []Attr
170 attrs []Attr
171 wantText string
172 wantJSON string
173 }{
174 {
175 name: "basic",
176 attrs: attrs,
177 wantText: "time=2000-01-02T03:04:05.000Z level=INFO msg=message a=one b=2",
178 wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","a":"one","b":2}`,
179 },
180 {
181 name: "empty key",
182 attrs: append(slices.Clip(attrs), Any("", "v")),
183 wantText: `time=2000-01-02T03:04:05.000Z level=INFO msg=message a=one b=2 ""=v`,
184 wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","a":"one","b":2,"":"v"}`,
185 },
186 {
187 name: "cap keys",
188 replace: upperCaseKey,
189 attrs: attrs,
190 wantText: "TIME=2000-01-02T03:04:05.000Z LEVEL=INFO MSG=message A=one B=2",
191 wantJSON: `{"TIME":"2000-01-02T03:04:05Z","LEVEL":"INFO","MSG":"message","A":"one","B":2}`,
192 },
193 {
194 name: "remove all",
195 replace: removeAll,
196 attrs: attrs,
197 wantText: "",
198 wantJSON: `{}`,
199 },
200 {
201 name: "preformatted",
202 with: func(h Handler) Handler { return h.WithAttrs(preAttrs) },
203 preAttrs: preAttrs,
204 attrs: attrs,
205 wantText: "time=2000-01-02T03:04:05.000Z level=INFO msg=message pre=3 x=y a=one b=2",
206 wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","pre":3,"x":"y","a":"one","b":2}`,
207 },
208 {
209 name: "preformatted cap keys",
210 replace: upperCaseKey,
211 with: func(h Handler) Handler { return h.WithAttrs(preAttrs) },
212 preAttrs: preAttrs,
213 attrs: attrs,
214 wantText: "TIME=2000-01-02T03:04:05.000Z LEVEL=INFO MSG=message PRE=3 X=y A=one B=2",
215 wantJSON: `{"TIME":"2000-01-02T03:04:05Z","LEVEL":"INFO","MSG":"message","PRE":3,"X":"y","A":"one","B":2}`,
216 },
217 {
218 name: "preformatted remove all",
219 replace: removeAll,
220 with: func(h Handler) Handler { return h.WithAttrs(preAttrs) },
221 preAttrs: preAttrs,
222 attrs: attrs,
223 wantText: "",
224 wantJSON: "{}",
225 },
226 {
227 name: "remove built-in",
228 replace: removeKeys(TimeKey, LevelKey, MessageKey),
229 attrs: attrs,
230 wantText: "a=one b=2",
231 wantJSON: `{"a":"one","b":2}`,
232 },
233 {
234 name: "preformatted remove built-in",
235 replace: removeKeys(TimeKey, LevelKey, MessageKey),
236 with: func(h Handler) Handler { return h.WithAttrs(preAttrs) },
237 attrs: attrs,
238 wantText: "pre=3 x=y a=one b=2",
239 wantJSON: `{"pre":3,"x":"y","a":"one","b":2}`,
240 },
241 {
242 name: "groups",
243 replace: removeKeys(TimeKey, LevelKey),
244 attrs: []Attr{
245 Int("a", 1),
246 Group("g",
247 Int("b", 2),
248 Group("h", Int("c", 3)),
249 Int("d", 4)),
250 Int("e", 5),
251 },
252 wantText: "msg=message a=1 g.b=2 g.h.c=3 g.d=4 e=5",
253 wantJSON: `{"msg":"message","a":1,"g":{"b":2,"h":{"c":3},"d":4},"e":5}`,
254 },
255 {
256 name: "empty group",
257 replace: removeKeys(TimeKey, LevelKey),
258 attrs: []Attr{Group("g"), Group("h", Int("a", 1))},
259 wantText: "msg=message h.a=1",
260 wantJSON: `{"msg":"message","h":{"a":1}}`,
261 },
262 {
263 name: "nested empty group",
264 replace: removeKeys(TimeKey, LevelKey),
265 attrs: []Attr{
266 Group("g",
267 Group("h",
268 Group("i"), Group("j"))),
269 },
270 wantText: `msg=message`,
271 wantJSON: `{"msg":"message"}`,
272 },
273 {
274 name: "nested non-empty group",
275 replace: removeKeys(TimeKey, LevelKey),
276 attrs: []Attr{
277 Group("g",
278 Group("h",
279 Group("i"), Group("j", Int("a", 1)))),
280 },
281 wantText: `msg=message g.h.j.a=1`,
282 wantJSON: `{"msg":"message","g":{"h":{"j":{"a":1}}}}`,
283 },
284 {
285 name: "escapes",
286 replace: removeKeys(TimeKey, LevelKey),
287 attrs: []Attr{
288 String("a b", "x\t\n\000y"),
289 Group(" b.c=\"\\x2E\t",
290 String("d=e", "f.g\""),
291 Int("m.d", 1)),
292 },
293 wantText: `msg=message "a b"="x\t\n\x00y" " b.c=\"\\x2E\t.d=e"="f.g\"" " b.c=\"\\x2E\t.m.d"=1`,
294 wantJSON: `{"msg":"message","a b":"x\t\n\u0000y"," b.c=\"\\x2E\t":{"d=e":"f.g\"","m.d":1}}`,
295 },
296 {
297 name: "LogValuer",
298 replace: removeKeys(TimeKey, LevelKey),
299 attrs: []Attr{
300 Int("a", 1),
301 Any("name", logValueName{"Ren", "Hoek"}),
302 Int("b", 2),
303 },
304 wantText: "msg=message a=1 name.first=Ren name.last=Hoek b=2",
305 wantJSON: `{"msg":"message","a":1,"name":{"first":"Ren","last":"Hoek"},"b":2}`,
306 },
307 {
308
309 name: "resolve",
310 attrs: []Attr{
311 Any("", &replace{Value{}}),
312 Any("name", logValueName{"Ren", "Hoek"}),
313 },
314 wantText: "time=2000-01-02T03:04:05.000Z level=INFO msg=message name.first=Ren name.last=Hoek",
315 wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","name":{"first":"Ren","last":"Hoek"}}`,
316 },
317 {
318 name: "with-group",
319 replace: removeKeys(TimeKey, LevelKey),
320 with: func(h Handler) Handler { return h.WithAttrs(preAttrs).WithGroup("s") },
321 attrs: attrs,
322 wantText: "msg=message pre=3 x=y s.a=one s.b=2",
323 wantJSON: `{"msg":"message","pre":3,"x":"y","s":{"a":"one","b":2}}`,
324 },
325 {
326 name: "preformatted with-groups",
327 replace: removeKeys(TimeKey, LevelKey),
328 with: func(h Handler) Handler {
329 return h.WithAttrs([]Attr{Int("p1", 1)}).
330 WithGroup("s1").
331 WithAttrs([]Attr{Int("p2", 2)}).
332 WithGroup("s2").
333 WithAttrs([]Attr{Int("p3", 3)})
334 },
335 attrs: attrs,
336 wantText: "msg=message p1=1 s1.p2=2 s1.s2.p3=3 s1.s2.a=one s1.s2.b=2",
337 wantJSON: `{"msg":"message","p1":1,"s1":{"p2":2,"s2":{"p3":3,"a":"one","b":2}}}`,
338 },
339 {
340 name: "two with-groups",
341 replace: removeKeys(TimeKey, LevelKey),
342 with: func(h Handler) Handler {
343 return h.WithAttrs([]Attr{Int("p1", 1)}).
344 WithGroup("s1").
345 WithGroup("s2")
346 },
347 attrs: attrs,
348 wantText: "msg=message p1=1 s1.s2.a=one s1.s2.b=2",
349 wantJSON: `{"msg":"message","p1":1,"s1":{"s2":{"a":"one","b":2}}}`,
350 },
351 {
352 name: "empty with-groups",
353 replace: removeKeys(TimeKey, LevelKey),
354 with: func(h Handler) Handler {
355 return h.WithGroup("x").WithGroup("y")
356 },
357 wantText: "msg=message",
358 wantJSON: `{"msg":"message"}`,
359 },
360 {
361 name: "empty with-groups, no non-empty attrs",
362 replace: removeKeys(TimeKey, LevelKey),
363 with: func(h Handler) Handler {
364 return h.WithGroup("x").WithAttrs([]Attr{Group("g")}).WithGroup("y")
365 },
366 wantText: "msg=message",
367 wantJSON: `{"msg":"message"}`,
368 },
369 {
370 name: "one empty with-group",
371 replace: removeKeys(TimeKey, LevelKey),
372 with: func(h Handler) Handler {
373 return h.WithGroup("x").WithAttrs([]Attr{Int("a", 1)}).WithGroup("y")
374 },
375 attrs: []Attr{Group("g", Group("h"))},
376 wantText: "msg=message x.a=1",
377 wantJSON: `{"msg":"message","x":{"a":1}}`,
378 },
379 {
380 name: "GroupValue as Attr value",
381 replace: removeKeys(TimeKey, LevelKey),
382 attrs: []Attr{{"v", AnyValue(IntValue(3))}},
383 wantText: "msg=message v=3",
384 wantJSON: `{"msg":"message","v":3}`,
385 },
386 {
387 name: "byte slice",
388 replace: removeKeys(TimeKey, LevelKey),
389 attrs: []Attr{Any("bs", []byte{1, 2, 3, 4})},
390 wantText: `msg=message bs="\x01\x02\x03\x04"`,
391 wantJSON: `{"msg":"message","bs":"AQIDBA=="}`,
392 },
393 {
394 name: "json.RawMessage",
395 replace: removeKeys(TimeKey, LevelKey),
396 attrs: []Attr{Any("bs", json.RawMessage([]byte("1234")))},
397 wantText: `msg=message bs="1234"`,
398 wantJSON: `{"msg":"message","bs":1234}`,
399 },
400 {
401 name: "inline group",
402 replace: removeKeys(TimeKey, LevelKey),
403 attrs: []Attr{
404 Int("a", 1),
405 Group("", Int("b", 2), Int("c", 3)),
406 Int("d", 4),
407 },
408 wantText: `msg=message a=1 b=2 c=3 d=4`,
409 wantJSON: `{"msg":"message","a":1,"b":2,"c":3,"d":4}`,
410 },
411 {
412 name: "Source",
413 replace: func(gs []string, a Attr) Attr {
414 if a.Key == SourceKey {
415 s := a.Value.Any().(*Source)
416 s.File = filepath.Base(s.File)
417 return Any(a.Key, s)
418 }
419 return removeKeys(TimeKey, LevelKey)(gs, a)
420 },
421 addSource: true,
422 wantText: `source=handler_test.go:$LINE msg=message`,
423 wantJSON: `{"source":{"function":"log/slog.TestJSONAndTextHandlers","file":"handler_test.go","line":$LINE},"msg":"message"}`,
424 },
425 {
426 name: "replace built-in with group",
427 replace: func(_ []string, a Attr) Attr {
428 if a.Key == TimeKey {
429 return Group(TimeKey, "mins", 3, "secs", 2)
430 }
431 if a.Key == LevelKey {
432 return Attr{}
433 }
434 return a
435 },
436 wantText: `time.mins=3 time.secs=2 msg=message`,
437 wantJSON: `{"time":{"mins":3,"secs":2},"msg":"message"}`,
438 },
439 {
440 name: "replace empty",
441 replace: func([]string, Attr) Attr { return Attr{} },
442 attrs: []Attr{Group("g", Int("a", 1))},
443 wantText: "",
444 wantJSON: `{}`,
445 },
446 {
447 name: "replace empty 1",
448 with: func(h Handler) Handler {
449 return h.WithGroup("g").WithAttrs([]Attr{Int("a", 1)})
450 },
451 replace: func([]string, Attr) Attr { return Attr{} },
452 attrs: []Attr{Group("h", Int("b", 2))},
453 wantText: "",
454 wantJSON: `{}`,
455 },
456 {
457 name: "replace empty 2",
458 with: func(h Handler) Handler {
459 return h.WithGroup("g").WithAttrs([]Attr{Int("a", 1)}).WithGroup("h").WithAttrs([]Attr{Int("b", 2)})
460 },
461 replace: func([]string, Attr) Attr { return Attr{} },
462 attrs: []Attr{Group("i", Int("c", 3))},
463 wantText: "",
464 wantJSON: `{}`,
465 },
466 {
467 name: "replace empty 3",
468 with: func(h Handler) Handler { return h.WithGroup("g") },
469 replace: func([]string, Attr) Attr { return Attr{} },
470 attrs: []Attr{Int("a", 1)},
471 wantText: "",
472 wantJSON: `{}`,
473 },
474 {
475 name: "replace empty inline",
476 with: func(h Handler) Handler {
477 return h.WithGroup("g").WithAttrs([]Attr{Int("a", 1)}).WithGroup("h").WithAttrs([]Attr{Int("b", 2)})
478 },
479 replace: func([]string, Attr) Attr { return Attr{} },
480 attrs: []Attr{Group("", Int("c", 3))},
481 wantText: "",
482 wantJSON: `{}`,
483 },
484 {
485 name: "replace partial empty attrs 1",
486 with: func(h Handler) Handler {
487 return h.WithGroup("g").WithAttrs([]Attr{Int("a", 1)}).WithGroup("h").WithAttrs([]Attr{Int("b", 2)})
488 },
489 replace: func(groups []string, attr Attr) Attr {
490 return removeKeys(TimeKey, LevelKey, MessageKey, "a")(groups, attr)
491 },
492 attrs: []Attr{Group("i", Int("c", 3))},
493 wantText: "g.h.b=2 g.h.i.c=3",
494 wantJSON: `{"g":{"h":{"b":2,"i":{"c":3}}}}`,
495 },
496 {
497 name: "replace partial empty attrs 2",
498 with: func(h Handler) Handler {
499 return h.WithGroup("g").WithAttrs([]Attr{Int("a", 1)}).WithAttrs([]Attr{Int("n", 4)}).WithGroup("h").WithAttrs([]Attr{Int("b", 2)})
500 },
501 replace: func(groups []string, attr Attr) Attr {
502 return removeKeys(TimeKey, LevelKey, MessageKey, "a", "b")(groups, attr)
503 },
504 attrs: []Attr{Group("i", Int("c", 3))},
505 wantText: "g.n=4 g.h.i.c=3",
506 wantJSON: `{"g":{"n":4,"h":{"i":{"c":3}}}}`,
507 },
508 {
509 name: "replace partial empty attrs 3",
510 with: func(h Handler) Handler {
511 return h.WithGroup("g").WithAttrs([]Attr{Int("x", 0)}).WithAttrs([]Attr{Int("a", 1)}).WithAttrs([]Attr{Int("n", 4)}).WithGroup("h").WithAttrs([]Attr{Int("b", 2)})
512 },
513 replace: func(groups []string, attr Attr) Attr {
514 return removeKeys(TimeKey, LevelKey, MessageKey, "a", "c")(groups, attr)
515 },
516 attrs: []Attr{Group("i", Int("c", 3))},
517 wantText: "g.x=0 g.n=4 g.h.b=2",
518 wantJSON: `{"g":{"x":0,"n":4,"h":{"b":2}}}`,
519 },
520 {
521 name: "replace resolved group",
522 replace: func(groups []string, a Attr) Attr {
523 if a.Value.Kind() == KindGroup {
524 return Attr{"bad", IntValue(1)}
525 }
526 return removeKeys(TimeKey, LevelKey, MessageKey)(groups, a)
527 },
528 attrs: []Attr{Any("name", logValueName{"Perry", "Platypus"})},
529 wantText: "name.first=Perry name.last=Platypus",
530 wantJSON: `{"name":{"first":"Perry","last":"Platypus"}}`,
531 },
532 {
533 name: "group and key (or both) needs quoting",
534 replace: removeKeys(TimeKey, LevelKey),
535 attrs: []Attr{
536 Group("prefix",
537 String(" needs quoting ", "v"), String("NotNeedsQuoting", "v"),
538 ),
539 Group("prefix needs quoting",
540 String(" needs quoting ", "v"), String("NotNeedsQuoting", "v"),
541 ),
542 },
543 wantText: `msg=message "prefix. needs quoting "=v prefix.NotNeedsQuoting=v "prefix needs quoting. needs quoting "=v "prefix needs quoting.NotNeedsQuoting"=v`,
544 wantJSON: `{"msg":"message","prefix":{" needs quoting ":"v","NotNeedsQuoting":"v"},"prefix needs quoting":{" needs quoting ":"v","NotNeedsQuoting":"v"}}`,
545 },
546 } {
547 r := NewRecord(testTime, LevelInfo, "message", callerPC(2))
548 source := r.Source()
549 if source == nil {
550 t.Fatal("source is nil")
551 }
552 line := strconv.Itoa(source.Line)
553 r.AddAttrs(test.attrs...)
554 var buf bytes.Buffer
555 opts := HandlerOptions{ReplaceAttr: test.replace, AddSource: test.addSource}
556 t.Run(test.name, func(t *testing.T) {
557 for _, handler := range []struct {
558 name string
559 h Handler
560 want string
561 }{
562 {"text", NewTextHandler(&buf, &opts), test.wantText},
563 {"json", NewJSONHandler(&buf, &opts), test.wantJSON},
564 } {
565 t.Run(handler.name, func(t *testing.T) {
566 h := handler.h
567 if test.with != nil {
568 h = test.with(h)
569 }
570 buf.Reset()
571 if err := h.Handle(nil, r); err != nil {
572 t.Fatal(err)
573 }
574 want := strings.ReplaceAll(handler.want, "$LINE", line)
575 got := strings.TrimSuffix(buf.String(), "\n")
576 if got != want {
577 t.Errorf("\ngot %s\nwant %s\n", got, want)
578 }
579 })
580 }
581 })
582 }
583 }
584
585
586
587 func removeKeys(keys ...string) func([]string, Attr) Attr {
588 return func(_ []string, a Attr) Attr {
589 for _, k := range keys {
590 if a.Key == k {
591 return Attr{}
592 }
593 }
594 return a
595 }
596 }
597
598 func upperCaseKey(_ []string, a Attr) Attr {
599 a.Key = strings.ToUpper(a.Key)
600 return a
601 }
602
603 type logValueName struct {
604 first, last string
605 }
606
607 func (n logValueName) LogValue() Value {
608 return GroupValue(
609 String("first", n.first),
610 String("last", n.last))
611 }
612
613 func TestHandlerEnabled(t *testing.T) {
614 levelVar := func(l Level) *LevelVar {
615 var al LevelVar
616 al.Set(l)
617 return &al
618 }
619
620 for _, test := range []struct {
621 leveler Leveler
622 want bool
623 }{
624 {nil, true},
625 {LevelWarn, false},
626 {&LevelVar{}, true},
627 {levelVar(LevelWarn), false},
628 {LevelDebug, true},
629 {levelVar(LevelDebug), true},
630 } {
631 h := &commonHandler{opts: HandlerOptions{Level: test.leveler}}
632 got := h.enabled(LevelInfo)
633 if got != test.want {
634 t.Errorf("%v: got %t, want %t", test.leveler, got, test.want)
635 }
636 }
637 }
638
639 func TestJSONAndTextHandlersWithUnavailableSource(t *testing.T) {
640
641
642 var buf bytes.Buffer
643 opts := &HandlerOptions{
644 ReplaceAttr: removeKeys(LevelKey),
645 AddSource: true,
646 }
647
648 for _, test := range []struct {
649 name string
650 h Handler
651 want string
652 }{
653 {"text", NewTextHandler(&buf, opts), "msg=message"},
654 {"json", NewJSONHandler(&buf, opts), `{"msg":"message"}`},
655 } {
656 t.Run(test.name, func(t *testing.T) {
657 buf.Reset()
658 r := NewRecord(time.Time{}, LevelInfo, "message", 0)
659 err := test.h.Handle(t.Context(), r)
660 if err != nil {
661 t.Fatal(err)
662 }
663
664 want := strings.TrimSpace(test.want)
665 got := strings.TrimSpace(buf.String())
666 if got != want {
667 t.Errorf("\ngot %s\nwant %s", got, want)
668 }
669 })
670 }
671 }
672
673 func TestSecondWith(t *testing.T) {
674
675
676 var buf bytes.Buffer
677 h := NewTextHandler(&buf, &HandlerOptions{ReplaceAttr: removeKeys(TimeKey)})
678 logger := New(h).With(
679 String("app", "playground"),
680 String("role", "tester"),
681 Int("data_version", 2),
682 )
683 appLogger := logger.With("type", "log")
684 _ = logger.With("type", "metric")
685 appLogger.Info("foo")
686 got := strings.TrimSpace(buf.String())
687 want := `level=INFO msg=foo app=playground role=tester data_version=2 type=log`
688 if got != want {
689 t.Errorf("\ngot %s\nwant %s", got, want)
690 }
691 }
692
693 func TestReplaceAttrGroups(t *testing.T) {
694
695 type ga struct {
696 groups string
697 key string
698 val string
699 }
700
701 var got []ga
702
703 h := NewTextHandler(io.Discard, &HandlerOptions{ReplaceAttr: func(gs []string, a Attr) Attr {
704 v := a.Value.String()
705 if a.Key == TimeKey {
706 v = "<now>"
707 }
708 got = append(got, ga{strings.Join(gs, ","), a.Key, v})
709 return a
710 }})
711 New(h).
712 With(Int("a", 1)).
713 WithGroup("g1").
714 With(Int("b", 2)).
715 WithGroup("g2").
716 With(
717 Int("c", 3),
718 Group("g3", Int("d", 4)),
719 Int("e", 5)).
720 Info("m",
721 Int("f", 6),
722 Group("g4", Int("h", 7)),
723 Int("i", 8))
724
725 want := []ga{
726 {"", "a", "1"},
727 {"g1", "b", "2"},
728 {"g1,g2", "c", "3"},
729 {"g1,g2,g3", "d", "4"},
730 {"g1,g2", "e", "5"},
731 {"", "time", "<now>"},
732 {"", "level", "INFO"},
733 {"", "msg", "m"},
734 {"g1,g2", "f", "6"},
735 {"g1,g2,g4", "h", "7"},
736 {"g1,g2", "i", "8"},
737 }
738 if !slices.Equal(got, want) {
739 t.Errorf("\ngot %v\nwant %v", got, want)
740 }
741 }
742
743 const rfc3339Millis = "2006-01-02T15:04:05.000Z07:00"
744
745 func TestWriteTimeRFC3339(t *testing.T) {
746 for _, tm := range []time.Time{
747 time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC),
748 time.Date(2000, 1, 2, 3, 4, 5, 400, time.Local),
749 time.Date(2000, 11, 12, 3, 4, 500, 5e7, time.UTC),
750 } {
751 got := string(appendRFC3339Millis(nil, tm))
752 want := tm.Format(rfc3339Millis)
753 if got != want {
754 t.Errorf("got %s, want %s", got, want)
755 }
756 }
757 }
758
759 func BenchmarkWriteTime(b *testing.B) {
760 tm := time.Date(2022, 3, 4, 5, 6, 7, 823456789, time.Local)
761 var buf []byte
762 for b.Loop() {
763 buf = appendRFC3339Millis(buf[:0], tm)
764 }
765 }
766
767 func TestDiscardHandler(t *testing.T) {
768 ctx := context.Background()
769 stdout, stderr := os.Stdout, os.Stderr
770 os.Stdout, os.Stderr = nil, nil
771 t.Cleanup(func() {
772 os.Stdout, os.Stderr = stdout, stderr
773 })
774
775
776 l := New(DiscardHandler)
777 l.Info("msg", "a", 1, "b", 2)
778 l.Debug("bg", Int("a", 1), "b", 2)
779 l.Warn("w", Duration("dur", 3*time.Second))
780 l.Error("bad", "a", 1)
781 l.Log(ctx, LevelWarn+1, "w", Int("a", 1), String("b", "two"))
782 l.LogAttrs(ctx, LevelInfo+1, "a b c", Int("a", 1), String("b", "two"))
783 l.Info("info", "a", []Attr{Int("i", 1)})
784 l.Info("info", "a", GroupValue(Int("i", 1)))
785 }
786
787 func BenchmarkAppendKey(b *testing.B) {
788 for _, size := range []int{5, 10, 30, 50, 100} {
789 for _, quoting := range []string{"no_quoting", "pre_quoting", "key_quoting", "both_quoting"} {
790 b.Run(fmt.Sprintf("%s_prefix_size_%d", quoting, size), func(b *testing.B) {
791 var (
792 hs = NewJSONHandler(io.Discard, nil).newHandleState(buffer.New(), false, "")
793 prefix = bytes.Repeat([]byte("x"), size)
794 key = "key"
795 )
796
797 if quoting == "pre_quoting" || quoting == "both_quoting" {
798 prefix[0] = '"'
799 }
800 if quoting == "key_quoting" || quoting == "both_quoting" {
801 key = "ke\""
802 }
803
804 hs.prefix = (*buffer.Buffer)(&prefix)
805
806 for b.Loop() {
807 hs.appendKey(key)
808 hs.buf.Reset()
809 }
810 })
811 }
812 }
813 }
814
View as plain text