Skip to content

Commit 94c9d89

Browse files
committed
slog-handler-guide: preformatting
Change-Id: Ib1e64bafc76c296aa0ffb40d754d6e41fe69a33e Reviewed-on: https://go-review.googlesource.com/c/example/+/511638 Run-TryBot: Jonathan Amsterdam <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Ian Cottrell <[email protected]>
1 parent 183ca08 commit 94c9d89

File tree

4 files changed

+578
-2
lines changed

4 files changed

+578
-2
lines changed

slog-handler-guide/README.md

+168-1
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,174 @@ See [github.com/jba/slog/withsupport](https://github.com/jba/slog/withsupport) f
510510

511511
### With pre-formatting
512512

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.
514681

515682
## Testing
516683

slog-handler-guide/guide.md

+91-1
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,97 @@ See [github.com/jba/slog/withsupport](https://github.com/jba/slog/withsupport) f
330330

331331
### With pre-formatting
332332

333-
TODO(jba): write
333+
Our second implementation implements pre-formatting.
334+
This implementation is more complicated than the previous one.
335+
Is the extra complexity worth it?
336+
That depends on your circumstances, but here is one circumstance where
337+
it might be.
338+
Say that you wanted your server to log a lot of information about an incoming
339+
request with every log message that happens during that request. A typical
340+
handler might look something like this:
341+
342+
func (s *Server) handleWidgets(w http.ResponseWriter, r *http.Request) {
343+
logger := s.logger.With(
344+
"url", r.URL,
345+
"traceID": r.Header.Get("X-Cloud-Trace-Context"),
346+
// many other attributes
347+
)
348+
// ...
349+
}
350+
351+
A single handleWidgets request might generate hundreds of log lines.
352+
For instance, it might contain code like this:
353+
354+
for _, w := range widgets {
355+
logger.Info("processing widget", "name", w.Name)
356+
// ...
357+
}
358+
359+
For every such line, the `Handle` method we wrote above will format all
360+
the attributes that were added using `With` above, in addition to the
361+
ones on the log line itself.
362+
363+
Maybe all that extra work doesn't slow down your server significantly, because
364+
it does so much other work that time spent logging is just noise.
365+
But perhaps your server is fast enough that all that extra formatting appears high up
366+
in your CPU profiles. That is when pre-formatting can make a big difference,
367+
by formatting the attributes in a call to `With` just once.
368+
369+
To pre-format the arguments to `WithAttrs`, we need to keep track of some
370+
additional state in the `IndentHandler` struct.
371+
372+
%include indenthandler3/indent_handler.go IndentHandler -
373+
374+
Mainly, we need a buffer to hold the pre-formatted data.
375+
But we also need to keep track of which groups
376+
we've seen but haven't output yet. We'll call those groups "unopened."
377+
We also need to track how many groups we've opened, which we can do
378+
with a simple counter, since an opened group's only effect is to change the
379+
indentation level.
380+
381+
The `WithGroup` implementation is a lot like the previous one: just remember the
382+
new group, which is unopened initially.
383+
384+
%include indenthandler3/indent_handler.go WithGroup -
385+
386+
`WithAttrs` does all the pre-formatting:
387+
388+
%include indenthandler3/indent_handler.go WithAttrs -
389+
390+
It first opens any unopened groups. This handles calls like:
391+
392+
logger.WithGroup("g").WithGroup("h").With("a", 1)
393+
394+
Here, `WithAttrs` must output "g" and "h" before "a". Since a group established
395+
by `WithGroup` is in effect for the rest of the log line, `WithAttrs` increments
396+
the indentation level for each group it opens.
397+
398+
Lastly, `WithAttrs` formats its argument attributes, using the same `appendAttr`
399+
method we saw above.
400+
401+
It's the `Handle` method's job to insert the pre-formatted material in the right
402+
place, which is after the built-in attributes and before the ones in the record:
403+
404+
%include indenthandler3/indent_handler.go Handle -
405+
406+
It must also open any groups that haven't yet been opened. The logic covers
407+
log lines like this one:
408+
409+
logger.WithGroup("g").Info("msg", "a", 1)
410+
411+
where "g" is unopened before `Handle` is called and must be written to produce
412+
the correct output:
413+
414+
level: INFO
415+
msg: "msg"
416+
g:
417+
a: 1
418+
419+
The check for `r.NumAttrs() > 0` handles this case:
420+
421+
logger.WithGroup("g").Info("msg")
422+
423+
Here there are no record attributes, so no group to open.
334424

335425
## Testing
336426

0 commit comments

Comments
 (0)