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