Skip to content

Commit 29ffb74

Browse files
committed
slog-handler-guide: handler example: Handle method
Pull the discussion of the Handle method up. It makes more sense to talk about it before WithGroup and WithAttrs. Change-Id: I01a1b4b98131079cf74d2d965742a48acf5c3407 Reviewed-on: https://go-review.googlesource.com/c/example/+/509957 TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Ian Cottrell <[email protected]> Run-TryBot: Jonathan Amsterdam <[email protected]>
1 parent 7137c6b commit 29ffb74

File tree

2 files changed

+179
-133
lines changed

2 files changed

+179
-133
lines changed

slog-handler-guide/README.md

+103-67
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ This document is maintained by Jonathan Amsterdam `[email protected]`.
1111
1. [Loggers and their handlers](#loggers-and-their-handlers)
1212
1. [Implementing `Handler` methods](#implementing-`handler`-methods)
1313
1. [The `Enabled` method](#the-`enabled`-method)
14+
1. [The `Handle` method](#the-`handle`-method)
1415
1. [The `WithAttrs` method](#the-`withattrs`-method)
1516
1. [The `WithGroup` method](#the-`withgroup`-method)
16-
1. [The `Handle` method](#the-`handle`-method)
17+
1. [Testing](#testing)
1718
1. [General considerations](#general-considerations)
1819
1. [Concurrency safety](#concurrency-safety)
1920
1. [Robustness](#robustness)
@@ -149,7 +150,7 @@ Changes to `LevelVar`s are goroutine-safe.
149150
The mutex will be used to ensure that writes to the `io.Writer` happen atomically.
150151
Unusually, `IndentHandler` holds a pointer to a `sync.Mutex` rather than holding a
151152
`sync.Mutex` directly.
152-
But there is a good reason for that, which we'll explain later.
153+
There is a good reason for that, which we'll explain later.
153154

154155
TODO(jba): add link to that later explanation.
155156

@@ -175,6 +176,101 @@ of the work done by each request to be controlled independently.
175176

176177
TODO(jba): include Enabled example
177178

179+
## The `Handle` method
180+
181+
The `Handle` method is passed a `Record` containing all the information to be
182+
logged for a single call to a `Logger` output method.
183+
The `Handle` method should deal with it in some way.
184+
One way is to output the `Record` in some format, as `TextHandler` and `JSONHandler` do.
185+
But other options are to modify the `Record` and pass it on to another handler,
186+
enqueue the `Record` for later processing, or ignore it.
187+
188+
The signature of `Handle` is
189+
190+
Handle(context.Context, Record) error
191+
192+
The context is provided to support applications that provide logging information
193+
along the call chain. In a break with usual Go practice, the `Handle` method
194+
should not treat a canceled context as a signal to stop work.
195+
196+
If `Handle` processes its `Record`, it should follow the rules in the
197+
[documentation](https://pkg.go.dev/log/slog#Handler.Handle).
198+
For example, a zero `Time` field should be ignored, as should zero `Attr`s.
199+
200+
A `Handle` method that is going to produce output should carry out the following steps:
201+
202+
1. Allocate a buffer, typically a `[]byte`, to hold the output.
203+
It's best to construct the output in memory first,
204+
then write it with a single call to `io.Writer.Write`,
205+
to minimize interleaving with other goroutines using the same writer.
206+
207+
2. Format the special fields: time, level, message, and source location (PC).
208+
As a general rule, these fields should appear first and are not nested in
209+
groups established by `WithGroup`.
210+
211+
3. Format the result of `WithGroup` and `WithAttrs` calls.
212+
213+
4. Format the attributes in the `Record`.
214+
215+
5. Output the buffer.
216+
217+
That is how our `IndentHandler`'s `Handle` method is structured:
218+
219+
```
220+
func (h *IndentHandler) Handle(ctx context.Context, r slog.Record) error {
221+
buf := make([]byte, 0, 1024)
222+
if !r.Time.IsZero() {
223+
buf = h.appendAttr(buf, slog.Time(slog.TimeKey, r.Time), 0)
224+
}
225+
buf = h.appendAttr(buf, slog.Any(slog.LevelKey, r.Level), 0)
226+
if r.PC != 0 {
227+
fs := runtime.CallersFrames([]uintptr{r.PC})
228+
f, _ := fs.Next()
229+
buf = h.appendAttr(buf, slog.String(slog.SourceKey, fmt.Sprintf("%s:%d", f.File, f.Line)), 0)
230+
}
231+
buf = h.appendAttr(buf, slog.String(slog.MessageKey, r.Message), 0)
232+
indentLevel := 0
233+
// TODO: output the Attrs and groups from WithAttrs and WithGroup.
234+
r.Attrs(func(a slog.Attr) bool {
235+
buf = h.appendAttr(buf, a, indentLevel)
236+
return true
237+
})
238+
buf = append(buf, "---\n"...)
239+
h.mu.Lock()
240+
defer h.mu.Unlock()
241+
_, err := h.out.Write(buf)
242+
return err
243+
}
244+
```
245+
246+
The first line allocates a `[]byte` that should be large enough for most log
247+
output.
248+
Allocating a buffer with some initial, fairly large capacity is a simple but
249+
significant optimization: it avoids the repeated copying and allocation that
250+
happen when the initial slice is empty or small.
251+
We'll return to this line in the section on [speed](#speed)
252+
and show how we can do even better.
253+
254+
The next part of our `Handle` method formats the special attributes,
255+
observing the rules to ignore a zero time and a zero PC.
256+
257+
Next, the method processes the result of `WithAttrs` and `WithGroup` calls.
258+
We'll skip that for now.
259+
260+
Then it's time to process the attributes in the argument record.
261+
We use the `Record.Attrs` method to iterate over the attributes
262+
in the order the user passed them to the `Logger` output method.
263+
Handlers are free to reorder or de-duplicate the attributes,
264+
but ours does not.
265+
266+
Lastly, after adding the line "---" to the output to separate log records,
267+
our handler makes a single call to `h.out.Write` with the buffer we've accumulated.
268+
We hold the lock for this write to make it atomic with respect to other
269+
goroutines that may be calling `Handle` at the same time.
270+
271+
TODO(jba): talk about appendAttr
272+
273+
178274
## The `WithAttrs` method
179275

180276
One of `slog`'s performance optimizations is support for pre-formatting
@@ -232,76 +328,14 @@ one that doesn't.
232328

233329
TODO(jba): add IndentHandler examples
234330

235-
## The `Handle` method
331+
## Testing
236332

237-
The `Handle` method is passed a `Record` containing all the information to be
238-
logged for a single call to a `Logger` output method.
239-
The `Handle` method should deal with it in some way.
240-
One way is to output the `Record` in some format, as `TextHandler` and `JSONHandler` do.
241-
But other options are to modify the `Record` and pass it on to another handler,
242-
enqueue the `Record` for later processing, or ignore it.
243-
244-
The signature of `Handle` is
245-
246-
Handle(context.Context, Record) error
247-
248-
The context is provided to support applications that provide logging information
249-
along the call chain. In a break with usual Go practice, the `Handle` method
250-
should not treat a canceled context as a signal to stop work.
251-
252-
If `Handle` processes its `Record`, it should follow the rules in the
253-
[documentation](https://pkg.go.dev/log/slog#Handler.Handle).
254-
For example, a zero `Time` field should be ignored, as should zero `Attr`s.
255333
To verify that your handler follows these rules and generally produces proper
256334
output, use the [testing/slogtest package](https://pkg.go.dev/log/slog).
257335

258-
Before processing the attributes in the `Record`, remember to process the
259-
attributes from `WithAttrs` calls, qualified by the groups from `WithGroup`
260-
calls. Then use `Record.Attrs` to process the attributes in the `Record`, also
261-
qualified by the groups from `WithGroup` calls.
262-
263-
The attributes provided to `Handler.WithAttrs` appear in the order the user passed them
264-
to `Logger.With`, and `Record.Attrs` traverses attributes in the order the user
265-
passed them to the `Logger` output method. Handlers are free to reorder or
266-
de-duplicate the attributes, provided group qualification is preserved.
267-
268-
Your handler can make a single copy of a `Record` with an ordinary Go
269-
assignment, channel send or function call if it doesn't retain the
270-
original.
271-
But if its actions result in more than one copy, it should call `Record.Clone`
272-
to make the copies so that they don't share state.
273-
This `Handle` method passes the record to a single handler, so it doesn't require `Clone`:
274-
275-
type Handler1 struct {
276-
h slog.Handler
277-
// ...
278-
}
279-
280-
func (h *Handler1) Handle(ctx context.Context, r slog.Record) error {
281-
return h.h.Handle(ctx, r)
282-
}
283-
284-
This `Handle` method might pass the record to more than one handler, so it
285-
should use `Clone`:
286-
287-
type Handler2 struct {
288-
hs []slog.Handler
289-
// ...
290-
}
291-
292-
func (h *Handler2) Handle(ctx context.Context, r slog.Record) error {
293-
for _, hh := range h.hs {
294-
if err := hh.Handle(ctx, r.Clone()); err != nil {
295-
return err
296-
}
297-
}
298-
return nil
299-
}
300-
301-
302-
303-
TODO(jba): example use of slogtest
336+
TODO(jba): show the test function.
304337

338+
TODO(jba): reintroduce the material on Record.Clone that used to be here.
305339

306340
# General considerations
307341

@@ -392,3 +426,5 @@ error.
392426
## Speed
393427

394428
TODO(jba): discuss
429+
430+
TODO(jba): show how to pool a []byte.

slog-handler-guide/guide.md

+76-66
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ Changes to `LevelVar`s are goroutine-safe.
113113
The mutex will be used to ensure that writes to the `io.Writer` happen atomically.
114114
Unusually, `IndentHandler` holds a pointer to a `sync.Mutex` rather than holding a
115115
`sync.Mutex` directly.
116-
But there is a good reason for that, which we'll explain later.
116+
There is a good reason for that, which we'll explain later.
117117

118118
TODO(jba): add link to that later explanation.
119119

@@ -139,6 +139,76 @@ of the work done by each request to be controlled independently.
139139

140140
TODO(jba): include Enabled example
141141

142+
## The `Handle` method
143+
144+
The `Handle` method is passed a `Record` containing all the information to be
145+
logged for a single call to a `Logger` output method.
146+
The `Handle` method should deal with it in some way.
147+
One way is to output the `Record` in some format, as `TextHandler` and `JSONHandler` do.
148+
But other options are to modify the `Record` and pass it on to another handler,
149+
enqueue the `Record` for later processing, or ignore it.
150+
151+
The signature of `Handle` is
152+
153+
Handle(context.Context, Record) error
154+
155+
The context is provided to support applications that provide logging information
156+
along the call chain. In a break with usual Go practice, the `Handle` method
157+
should not treat a canceled context as a signal to stop work.
158+
159+
If `Handle` processes its `Record`, it should follow the rules in the
160+
[documentation](https://pkg.go.dev/log/slog#Handler.Handle).
161+
For example, a zero `Time` field should be ignored, as should zero `Attr`s.
162+
163+
A `Handle` method that is going to produce output should carry out the following steps:
164+
165+
1. Allocate a buffer, typically a `[]byte`, to hold the output.
166+
It's best to construct the output in memory first,
167+
then write it with a single call to `io.Writer.Write`,
168+
to minimize interleaving with other goroutines using the same writer.
169+
170+
2. Format the special fields: time, level, message, and source location (PC).
171+
As a general rule, these fields should appear first and are not nested in
172+
groups established by `WithGroup`.
173+
174+
3. Format the result of `WithGroup` and `WithAttrs` calls.
175+
176+
4. Format the attributes in the `Record`.
177+
178+
5. Output the buffer.
179+
180+
That is how our `IndentHandler`'s `Handle` method is structured:
181+
182+
%include indenthandler1/indent_handler.go handle -
183+
184+
The first line allocates a `[]byte` that should be large enough for most log
185+
output.
186+
Allocating a buffer with some initial, fairly large capacity is a simple but
187+
significant optimization: it avoids the repeated copying and allocation that
188+
happen when the initial slice is empty or small.
189+
We'll return to this line in the section on [speed](#speed)
190+
and show how we can do even better.
191+
192+
The next part of our `Handle` method formats the special attributes,
193+
observing the rules to ignore a zero time and a zero PC.
194+
195+
Next, the method processes the result of `WithAttrs` and `WithGroup` calls.
196+
We'll skip that for now.
197+
198+
Then it's time to process the attributes in the argument record.
199+
We use the `Record.Attrs` method to iterate over the attributes
200+
in the order the user passed them to the `Logger` output method.
201+
Handlers are free to reorder or de-duplicate the attributes,
202+
but ours does not.
203+
204+
Lastly, after adding the line "---" to the output to separate log records,
205+
our handler makes a single call to `h.out.Write` with the buffer we've accumulated.
206+
We hold the lock for this write to make it atomic with respect to other
207+
goroutines that may be calling `Handle` at the same time.
208+
209+
TODO(jba): talk about appendAttr
210+
211+
142212
## The `WithAttrs` method
143213

144214
One of `slog`'s performance optimizations is support for pre-formatting
@@ -196,76 +266,14 @@ one that doesn't.
196266

197267
TODO(jba): add IndentHandler examples
198268

199-
## The `Handle` method
200-
201-
The `Handle` method is passed a `Record` containing all the information to be
202-
logged for a single call to a `Logger` output method.
203-
The `Handle` method should deal with it in some way.
204-
One way is to output the `Record` in some format, as `TextHandler` and `JSONHandler` do.
205-
But other options are to modify the `Record` and pass it on to another handler,
206-
enqueue the `Record` for later processing, or ignore it.
207-
208-
The signature of `Handle` is
209-
210-
Handle(context.Context, Record) error
211-
212-
The context is provided to support applications that provide logging information
213-
along the call chain. In a break with usual Go practice, the `Handle` method
214-
should not treat a canceled context as a signal to stop work.
269+
## Testing
215270

216-
If `Handle` processes its `Record`, it should follow the rules in the
217-
[documentation](https://pkg.go.dev/log/slog#Handler.Handle).
218-
For example, a zero `Time` field should be ignored, as should zero `Attr`s.
219271
To verify that your handler follows these rules and generally produces proper
220272
output, use the [testing/slogtest package](https://pkg.go.dev/log/slog).
221273

222-
Before processing the attributes in the `Record`, remember to process the
223-
attributes from `WithAttrs` calls, qualified by the groups from `WithGroup`
224-
calls. Then use `Record.Attrs` to process the attributes in the `Record`, also
225-
qualified by the groups from `WithGroup` calls.
226-
227-
The attributes provided to `Handler.WithAttrs` appear in the order the user passed them
228-
to `Logger.With`, and `Record.Attrs` traverses attributes in the order the user
229-
passed them to the `Logger` output method. Handlers are free to reorder or
230-
de-duplicate the attributes, provided group qualification is preserved.
231-
232-
Your handler can make a single copy of a `Record` with an ordinary Go
233-
assignment, channel send or function call if it doesn't retain the
234-
original.
235-
But if its actions result in more than one copy, it should call `Record.Clone`
236-
to make the copies so that they don't share state.
237-
This `Handle` method passes the record to a single handler, so it doesn't require `Clone`:
238-
239-
type Handler1 struct {
240-
h slog.Handler
241-
// ...
242-
}
243-
244-
func (h *Handler1) Handle(ctx context.Context, r slog.Record) error {
245-
return h.h.Handle(ctx, r)
246-
}
247-
248-
This `Handle` method might pass the record to more than one handler, so it
249-
should use `Clone`:
250-
251-
type Handler2 struct {
252-
hs []slog.Handler
253-
// ...
254-
}
255-
256-
func (h *Handler2) Handle(ctx context.Context, r slog.Record) error {
257-
for _, hh := range h.hs {
258-
if err := hh.Handle(ctx, r.Clone()); err != nil {
259-
return err
260-
}
261-
}
262-
return nil
263-
}
264-
265-
266-
267-
TODO(jba): example use of slogtest
274+
TODO(jba): show the test function.
268275

276+
TODO(jba): reintroduce the material on Record.Clone that used to be here.
269277

270278
# General considerations
271279

@@ -356,3 +364,5 @@ error.
356364
## Speed
357365

358366
TODO(jba): discuss
367+
368+
TODO(jba): show how to pool a []byte.

0 commit comments

Comments
 (0)