Skip to content

Commit 7137c6b

Browse files
committed
slog-handler-guide: handler example: types
Begin discussing a running example of a slog.Handler implementation. Initially we'll ignore the WithAttrs and WithGroup methods. The implementation that does that is in indenthandler1. This CL contains the complete implementation, but discusses only the types and constructor function. Change-Id: I3b635aee66a6d5df64bc13ce6bfb7ae4881606fe Reviewed-on: https://go-review.googlesource.com/c/example/+/509955 Reviewed-by: Ian Cottrell <iancottrell@google.com> TryBot-Result: Gopher Robot <gobot@golang.org> Run-TryBot: Jonathan Amsterdam <jba@google.com>
1 parent 72e55f1 commit 7137c6b

File tree

4 files changed

+258
-44
lines changed

4 files changed

+258
-44
lines changed

slog-handler-guide/README.md

Lines changed: 63 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -87,17 +87,74 @@ A logger's `WithGroup` method calls its handler's `WithGroup` method.
8787
# Implementing `Handler` methods
8888

8989
We can now talk about the four `Handler` methods in detail.
90-
Along the way, we will write a handler that formats logs in YAML.
91-
It will display this log output call:
90+
Along the way, we will write a handler that formats logs using a format
91+
reminsicent of YAML. It will display this log output call:
9292

9393
logger.Info("hello", "key", 23)
9494

95-
as this YAML document:
95+
something like this:
9696

9797
time: 2023-05-15T16:29:00
9898
level: INFO
99-
message: hello
99+
message: "hello"
100100
key: 23
101+
---
102+
103+
Although this particular output is valid YAML,
104+
our implementation doesn't consider the subtleties of YAML syntax,
105+
so it will sometimes produce invalid YAML.
106+
For example, it doesn't quote keys that have colons in them.
107+
We'll call it `IndentHandler` to forestall disappointment.
108+
109+
We begin with the `IndentHandler` type
110+
and the `New` function that constructs it from an `io.Writer` and options:
111+
112+
```
113+
type IndentHandler struct {
114+
opts Options
115+
// TODO: state for WithGroup and WithAttrs
116+
mu *sync.Mutex
117+
out io.Writer
118+
}
119+
120+
type Options struct {
121+
// Level reports the minimum level to log.
122+
// Levels with lower levels are discarded.
123+
// If nil, the Handler uses [slog.LevelInfo].
124+
Level slog.Leveler
125+
}
126+
127+
func New(out io.Writer, opts *Options) *IndentHandler {
128+
h := &IndentHandler{out: out, mu: &sync.Mutex{}}
129+
if opts != nil {
130+
h.opts = *opts
131+
}
132+
if h.opts.Level == nil {
133+
h.opts.Level = slog.LevelInfo
134+
}
135+
return h
136+
}
137+
```
138+
139+
We'll support only one option, the ability to set a minimum level in order to
140+
supress detailed log output.
141+
Handlers should always use the `slog.Leveler` type for this option.
142+
`Leveler` is implemented by both `Level` and `LevelVar`.
143+
A `Level` value is easy for the user to provide,
144+
but changing the level of multiple handlers requires tracking them all.
145+
If the user instead passes a `LevelVar`, then a single change to that `LevelVar`
146+
will change the behavior of all handlers that contain it.
147+
Changes to `LevelVar`s are goroutine-safe.
148+
149+
The mutex will be used to ensure that writes to the `io.Writer` happen atomically.
150+
Unusually, `IndentHandler` holds a pointer to a `sync.Mutex` rather than holding a
151+
`sync.Mutex` directly.
152+
But there is a good reason for that, which we'll explain later.
153+
154+
TODO(jba): add link to that later explanation.
155+
156+
Our handler will need additional state to track calls to `WithGroup` and `WithAttrs`.
157+
We will describe that state when we get to those methods.
101158

102159
## The `Enabled` method
103160

@@ -116,23 +173,7 @@ A handler's `Enabled` method could report whether the argument level
116173
is greater than or equal to the context value, allowing the verbosity
117174
of the work done by each request to be controlled independently.
118175

119-
Most implementations of `Enabled` will consult a configured minimum level
120-
instead. For maximum generality, use the `Leveler` type in the configuration of
121-
your handler, as the built-in `HandlerOptions` does.
122-
123-
Our YAML handler's constructor will take a `Leveler`, along with an `io.Writer`
124-
for its output:
125-
126-
TODO(jba): include func yamlhandler.New(w io.Writer, level slog.Leveler)
127-
128-
`Leveler` is implemented by both `Level` and `LevelVar`.
129-
A `Level` value is easy for the user to provide,
130-
but changing the level of multiple handlers requires tracking them all.
131-
If the user instead passes a `LevelVar`, then a single change to that `LevelVar`
132-
will change the behavior of all handlers that contain it.
133-
Changes to `LevelVar`s are goroutine-safe.
134-
135-
TODO(jba): example handler that implements a minimum level and delegates the other methods.
176+
TODO(jba): include Enabled example
136177

137178
## The `WithAttrs` method
138179

@@ -189,7 +230,7 @@ the implementations of `Handler.WithGroup` and `Handler.WithAttrs`.
189230
We will look at two implementations of `WithGroup` and `WithAttrs`, one that pre-formats and
190231
one that doesn't.
191232

192-
TODO(jba): add YAML handler examples
233+
TODO(jba): add IndentHandler examples
193234

194235
## The `Handle` method
195236

slog-handler-guide/guide.md

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -76,17 +76,49 @@ A logger's `WithGroup` method calls its handler's `WithGroup` method.
7676
# Implementing `Handler` methods
7777

7878
We can now talk about the four `Handler` methods in detail.
79-
Along the way, we will write a handler that formats logs in YAML.
80-
It will display this log output call:
79+
Along the way, we will write a handler that formats logs using a format
80+
reminsicent of YAML. It will display this log output call:
8181

8282
logger.Info("hello", "key", 23)
8383

84-
as this YAML document:
84+
something like this:
8585

8686
time: 2023-05-15T16:29:00
8787
level: INFO
88-
message: hello
88+
message: "hello"
8989
key: 23
90+
---
91+
92+
Although this particular output is valid YAML,
93+
our implementation doesn't consider the subtleties of YAML syntax,
94+
so it will sometimes produce invalid YAML.
95+
For example, it doesn't quote keys that have colons in them.
96+
We'll call it `IndentHandler` to forestall disappointment.
97+
98+
We begin with the `IndentHandler` type
99+
and the `New` function that constructs it from an `io.Writer` and options:
100+
101+
%include indenthandler1/indent_handler.go types -
102+
103+
We'll support only one option, the ability to set a minimum level in order to
104+
supress detailed log output.
105+
Handlers should always use the `slog.Leveler` type for this option.
106+
`Leveler` is implemented by both `Level` and `LevelVar`.
107+
A `Level` value is easy for the user to provide,
108+
but changing the level of multiple handlers requires tracking them all.
109+
If the user instead passes a `LevelVar`, then a single change to that `LevelVar`
110+
will change the behavior of all handlers that contain it.
111+
Changes to `LevelVar`s are goroutine-safe.
112+
113+
The mutex will be used to ensure that writes to the `io.Writer` happen atomically.
114+
Unusually, `IndentHandler` holds a pointer to a `sync.Mutex` rather than holding a
115+
`sync.Mutex` directly.
116+
But there is a good reason for that, which we'll explain later.
117+
118+
TODO(jba): add link to that later explanation.
119+
120+
Our handler will need additional state to track calls to `WithGroup` and `WithAttrs`.
121+
We will describe that state when we get to those methods.
90122

91123
## The `Enabled` method
92124

@@ -105,23 +137,7 @@ A handler's `Enabled` method could report whether the argument level
105137
is greater than or equal to the context value, allowing the verbosity
106138
of the work done by each request to be controlled independently.
107139

108-
Most implementations of `Enabled` will consult a configured minimum level
109-
instead. For maximum generality, use the `Leveler` type in the configuration of
110-
your handler, as the built-in `HandlerOptions` does.
111-
112-
Our YAML handler's constructor will take a `Leveler`, along with an `io.Writer`
113-
for its output:
114-
115-
TODO(jba): include func yamlhandler.New(w io.Writer, level slog.Leveler)
116-
117-
`Leveler` is implemented by both `Level` and `LevelVar`.
118-
A `Level` value is easy for the user to provide,
119-
but changing the level of multiple handlers requires tracking them all.
120-
If the user instead passes a `LevelVar`, then a single change to that `LevelVar`
121-
will change the behavior of all handlers that contain it.
122-
Changes to `LevelVar`s are goroutine-safe.
123-
124-
TODO(jba): example handler that implements a minimum level and delegates the other methods.
140+
TODO(jba): include Enabled example
125141

126142
## The `WithAttrs` method
127143

@@ -178,7 +194,7 @@ the implementations of `Handler.WithGroup` and `Handler.WithAttrs`.
178194
We will look at two implementations of `WithGroup` and `WithAttrs`, one that pre-formats and
179195
one that doesn't.
180196

181-
TODO(jba): add YAML handler examples
197+
TODO(jba): add IndentHandler examples
182198

183199
## The `Handle` method
184200

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
//go:build go1.21
2+
3+
package indenthandler
4+
5+
import (
6+
"context"
7+
"fmt"
8+
"io"
9+
"log/slog"
10+
"runtime"
11+
"sync"
12+
"time"
13+
)
14+
15+
// !+types
16+
type IndentHandler struct {
17+
opts Options
18+
// TODO: state for WithGroup and WithAttrs
19+
mu *sync.Mutex
20+
out io.Writer
21+
}
22+
23+
type Options struct {
24+
// Level reports the minimum level to log.
25+
// Levels with lower levels are discarded.
26+
// If nil, the Handler uses [slog.LevelInfo].
27+
Level slog.Leveler
28+
}
29+
30+
func New(out io.Writer, opts *Options) *IndentHandler {
31+
h := &IndentHandler{out: out, mu: &sync.Mutex{}}
32+
if opts != nil {
33+
h.opts = *opts
34+
}
35+
if h.opts.Level == nil {
36+
h.opts.Level = slog.LevelInfo
37+
}
38+
return h
39+
}
40+
41+
//!-types
42+
43+
func (h *IndentHandler) Enabled(ctx context.Context, level slog.Level) bool {
44+
return level >= h.opts.Level.Level()
45+
}
46+
47+
func (h *IndentHandler) WithGroup(name string) slog.Handler {
48+
// TODO: implement.
49+
return h
50+
}
51+
52+
func (h *IndentHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
53+
// TODO: implement.
54+
return h
55+
}
56+
57+
func (h *IndentHandler) Handle(ctx context.Context, r slog.Record) error {
58+
buf := make([]byte, 0, 1024)
59+
if !r.Time.IsZero() {
60+
buf = h.appendAttr(buf, slog.Time(slog.TimeKey, r.Time), 0)
61+
}
62+
buf = h.appendAttr(buf, slog.Any(slog.LevelKey, r.Level), 0)
63+
if r.PC != 0 {
64+
fs := runtime.CallersFrames([]uintptr{r.PC})
65+
f, _ := fs.Next()
66+
buf = h.appendAttr(buf, slog.String(slog.SourceKey, fmt.Sprintf("%s:%d", f.File, f.Line)), 0)
67+
}
68+
buf = h.appendAttr(buf, slog.String(slog.MessageKey, r.Message), 0)
69+
indentLevel := 0
70+
// TODO: output the Attrs and groups from WithAttrs and WithGroup.
71+
r.Attrs(func(a slog.Attr) bool {
72+
buf = h.appendAttr(buf, a, indentLevel)
73+
return true
74+
})
75+
buf = append(buf, "---\n"...)
76+
h.mu.Lock()
77+
defer h.mu.Unlock()
78+
_, err := h.out.Write(buf)
79+
return err
80+
}
81+
82+
func (h *IndentHandler) appendAttr(buf []byte, a slog.Attr, indentLevel int) []byte {
83+
// Resolve the Attr's value before doing anything else.
84+
a.Value = a.Value.Resolve()
85+
// Ignore empty Attrs.
86+
if a.Equal(slog.Attr{}) {
87+
return buf
88+
}
89+
// Indent 4 spaces per level.
90+
buf = fmt.Appendf(buf, "%*s", indentLevel*4, "")
91+
switch a.Value.Kind() {
92+
case slog.KindString:
93+
// Quote string values, to make them easy to parse.
94+
buf = fmt.Appendf(buf, "%s: %q\n", a.Key, a.Value.String())
95+
case slog.KindTime:
96+
// Write times in a standard way, without the monotonic time.
97+
buf = fmt.Appendf(buf, "%s: %s\n", a.Key, a.Value.Time().Format(time.RFC3339Nano))
98+
case slog.KindGroup:
99+
attrs := a.Value.Group()
100+
// Ignore empty groups.
101+
if len(attrs) == 0 {
102+
return buf
103+
}
104+
// If the key is non-empty, write it out and indent the rest of the attrs.
105+
// Otherwise, inline the attrs.
106+
if a.Key != "" {
107+
buf = fmt.Appendf(buf, "%s:\n", a.Key)
108+
indentLevel++
109+
}
110+
for _, ga := range attrs {
111+
buf = h.appendAttr(buf, ga, indentLevel)
112+
}
113+
default:
114+
buf = fmt.Appendf(buf, "%s: %s\n", a.Key, a.Value)
115+
}
116+
return buf
117+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//go:build go1.21
2+
3+
package indenthandler
4+
5+
import (
6+
"bytes"
7+
"regexp"
8+
"testing"
9+
10+
"log/slog"
11+
)
12+
13+
func Test(t *testing.T) {
14+
var buf bytes.Buffer
15+
l := slog.New(New(&buf, nil))
16+
l.Info("hello", "a", 1, "b", true, "c", 3.14, slog.Group("g", "h", 1, "i", 2), "d", "NO")
17+
got := buf.String()
18+
wantre := `time: [-0-9T:.]+Z?
19+
level: INFO
20+
source: ".*/indent_handler_test.go:\d+"
21+
msg: "hello"
22+
a: 1
23+
b: true
24+
c: 3.14
25+
g:
26+
h: 1
27+
i: 2
28+
d: "NO"
29+
`
30+
re := regexp.MustCompile(wantre)
31+
if !re.MatchString(got) {
32+
t.Errorf("\ngot:\n%q\nwant:\n%q", got, wantre)
33+
}
34+
35+
buf.Reset()
36+
l.Debug("test")
37+
if got := buf.Len(); got != 0 {
38+
t.Errorf("got buf.Len() = %d, want 0", got)
39+
}
40+
}

0 commit comments

Comments
 (0)