@@ -510,7 +510,174 @@ See [github.com/jba/slog/withsupport](https://github.com/jba/slog/withsupport) f
510
510
511
511
### With pre-formatting
512
512
513
- TODO(jba): write
513
+ Our second implementation implements pre-formatting.
514
+ This implementation is more complicated than the previous one.
515
+ Is the extra complexity worth it?
516
+ That depends on your circumstances, but here is one circumstance where
517
+ it might be.
518
+ Say that you wanted your server to log a lot of information about an incoming
519
+ request with every log message that happens during that request. A typical
520
+ handler might look something like this:
521
+
522
+ func (s *Server) handleWidgets(w http.ResponseWriter, r *http.Request) {
523
+ logger := s.logger.With(
524
+ "url", r.URL,
525
+ "traceID": r.Header.Get("X-Cloud-Trace-Context"),
526
+ // many other attributes
527
+ )
528
+ // ...
529
+ }
530
+
531
+ A single handleWidgets request might generate hundreds of log lines.
532
+ For instance, it might contain code like this:
533
+
534
+ for _, w := range widgets {
535
+ logger.Info("processing widget", "name", w.Name)
536
+ // ...
537
+ }
538
+
539
+ For every such line, the ` Handle ` method we wrote above will format all
540
+ the attributes that were added using ` With ` above, in addition to the
541
+ ones on the log line itself.
542
+
543
+ Maybe all that extra work doesn't slow down your server significantly, because
544
+ it does so much other work that time spent logging is just noise.
545
+ But perhaps your server is fast enough that all that extra formatting appears high up
546
+ in your CPU profiles. That is when pre-formatting can make a big difference,
547
+ by formatting the attributes in a call to ` With ` just once.
548
+
549
+ To pre-format the arguments to ` WithAttrs ` , we need to keep track of some
550
+ additional state in the ` IndentHandler ` struct.
551
+
552
+ ```
553
+ type IndentHandler struct {
554
+ opts Options
555
+ preformatted []byte // data from WithGroup and WithAttrs
556
+ unopenedGroups []string // groups from WithGroup that haven't been opened
557
+ indentLevel int // same as number of opened groups so far
558
+ mu *sync.Mutex
559
+ out io.Writer
560
+ }
561
+ ```
562
+
563
+ Mainly, we need a buffer to hold the pre-formatted data.
564
+ But we also need to keep track of which groups
565
+ we've seen but haven't output yet. We'll call those groups "unopened."
566
+ We also need to track how many groups we've opened, which we can do
567
+ with a simple counter, since an opened group's only effect is to change the
568
+ indentation level.
569
+
570
+ The ` WithGroup ` implementation is a lot like the previous one: just remember the
571
+ new group, which is unopened initially.
572
+
573
+ ```
574
+ func (h *IndentHandler) WithGroup(name string) slog.Handler {
575
+ if name == "" {
576
+ return h
577
+ }
578
+ h2 := *h
579
+ // Add an unopened group to h2 without modifying h.
580
+ h2.unopenedGroups = make([]string, len(h.unopenedGroups)+1)
581
+ copy(h2.unopenedGroups, h.unopenedGroups)
582
+ h2.unopenedGroups[len(h2.unopenedGroups)-1] = name
583
+ return &h2
584
+ }
585
+ ```
586
+
587
+ ` WithAttrs ` does all the pre-formatting:
588
+
589
+ ```
590
+ func (h *IndentHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
591
+ if len(attrs) == 0 {
592
+ return h
593
+ }
594
+ h2 := *h
595
+ // Force an append to copy the underlying array.
596
+ pre := slices.Clip(h.preformatted)
597
+ // Add all groups from WithGroup that haven't already been added.
598
+ h2.preformatted = h2.appendUnopenedGroups(pre, h2.indentLevel)
599
+ // Each of those groups increased the indent level by 1.
600
+ h2.indentLevel += len(h2.unopenedGroups)
601
+ // Now all groups have been opened.
602
+ h2.unopenedGroups = nil
603
+ // Pre-format the attributes.
604
+ for _, a := range attrs {
605
+ h2.preformatted = h2.appendAttr(h2.preformatted, a, h2.indentLevel)
606
+ }
607
+ return &h2
608
+ }
609
+
610
+ func (h *IndentHandler) appendUnopenedGroups(buf []byte, indentLevel int) []byte {
611
+ for _, g := range h.unopenedGroups {
612
+ buf = fmt.Appendf(buf, "%*s%s:\n", indentLevel*4, "", g)
613
+ indentLevel++
614
+ }
615
+ return buf
616
+ }
617
+ ```
618
+
619
+ It first opens any unopened groups. This handles calls like:
620
+
621
+ logger.WithGroup("g").WithGroup("h").With("a", 1)
622
+
623
+ Here, ` WithAttrs ` must output "g" and "h" before "a". Since a group established
624
+ by ` WithGroup ` is in effect for the rest of the log line, ` WithAttrs ` increments
625
+ the indentation level for each group it opens.
626
+
627
+ Lastly, ` WithAttrs ` formats its argument attributes, using the same ` appendAttr `
628
+ method we saw above.
629
+
630
+ It's the ` Handle ` method's job to insert the pre-formatted material in the right
631
+ place, which is after the built-in attributes and before the ones in the record:
632
+
633
+ ```
634
+ func (h *IndentHandler) Handle(ctx context.Context, r slog.Record) error {
635
+ buf := make([]byte, 0, 1024)
636
+ if !r.Time.IsZero() {
637
+ buf = h.appendAttr(buf, slog.Time(slog.TimeKey, r.Time), 0)
638
+ }
639
+ buf = h.appendAttr(buf, slog.Any(slog.LevelKey, r.Level), 0)
640
+ if r.PC != 0 {
641
+ fs := runtime.CallersFrames([]uintptr{r.PC})
642
+ f, _ := fs.Next()
643
+ buf = h.appendAttr(buf, slog.String(slog.SourceKey, fmt.Sprintf("%s:%d", f.File, f.Line)), 0)
644
+ }
645
+ buf = h.appendAttr(buf, slog.String(slog.MessageKey, r.Message), 0)
646
+ // Insert preformatted attributes just after built-in ones.
647
+ buf = append(buf, h.preformatted...)
648
+ if r.NumAttrs() > 0 {
649
+ buf = h.appendUnopenedGroups(buf, h.indentLevel)
650
+ r.Attrs(func(a slog.Attr) bool {
651
+ buf = h.appendAttr(buf, a, h.indentLevel+len(h.unopenedGroups))
652
+ return true
653
+ })
654
+ }
655
+ buf = append(buf, "---\n"...)
656
+ h.mu.Lock()
657
+ defer h.mu.Unlock()
658
+ _, err := h.out.Write(buf)
659
+ return err
660
+ }
661
+ ```
662
+
663
+ It must also open any groups that haven't yet been opened. The logic covers
664
+ log lines like this one:
665
+
666
+ logger.WithGroup("g").Info("msg", "a", 1)
667
+
668
+ where "g" is unopened before ` Handle ` is called and must be written to produce
669
+ the correct output:
670
+
671
+ level: INFO
672
+ msg: "msg"
673
+ g:
674
+ a: 1
675
+
676
+ The check for ` r.NumAttrs() > 0 ` handles this case:
677
+
678
+ logger.WithGroup("g").Info("msg")
679
+
680
+ Here there are no record attributes, so no group to open.
514
681
515
682
## Testing
516
683
0 commit comments