Skip to content

Commit 60527bc

Browse files
committed
slog: TextHandler
A Handler that writes Records as ordinary text lines. Also, revise the common handler code. The previous approach required a stateful appender, which couldn't easily be both zero-alloc and concurrency-safe. Change-Id: Ibe2749e2744a560086acec9287a18dd434ba9aa8 Reviewed-on: https://go-review.googlesource.com/c/exp/+/429435 Run-TryBot: Jonathan Amsterdam <[email protected]> Reviewed-by: Alan Donovan <[email protected]>
1 parent 1cac5f9 commit 60527bc

File tree

4 files changed

+306
-35
lines changed

4 files changed

+306
-35
lines changed

slog/handler.go

Lines changed: 29 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ type HandlerOptions struct {
8888
}
8989

9090
type commonHandler struct {
91-
appender
91+
newAppender func(*buffer.Buffer) appender
9292
opts HandlerOptions
9393
attrs []Attr
9494
preformattedAttrs []byte
@@ -104,7 +104,7 @@ func (h *commonHandler) Enabled(l Level) bool {
104104

105105
func (h *commonHandler) with(as []Attr) *commonHandler {
106106
h2 := &commonHandler{
107-
appender: h.appender,
107+
newAppender: h.newAppender,
108108
opts: h.opts,
109109
attrs: concat(h.attrs, as),
110110
preformattedAttrs: h.preformattedAttrs,
@@ -117,68 +117,66 @@ func (h *commonHandler) with(as []Attr) *commonHandler {
117117
}
118118

119119
// Pre-format the attributes as an optimization.
120-
h2.setBuffer((*buffer.Buffer)(&h2.preformattedAttrs))
120+
app := h2.newAppender((*buffer.Buffer)(&h2.preformattedAttrs))
121121
for _, p := range h2.attrs[len(h.attrs):] {
122-
h2.appendAttr(p)
122+
appendAttr(app, p)
123123
}
124124
return h2
125125
}
126126

127127
func (h *commonHandler) handle(r Record) error {
128128
buf := buffer.New()
129129
defer buf.Free()
130-
h.setBuffer(buf)
131-
130+
app := h.newAppender(buf)
132131
rep := h.opts.ReplaceAttr
133-
134132
replace := func(a Attr) {
135133
a = rep(a)
136134
if a.Key() != "" {
137-
h.appendKey(a.Key())
138-
h.appendAttrValue(a)
135+
app.appendKey(a.Key())
136+
app.appendAttrValue(a)
139137
}
140138
}
141139

142-
h.appendStart()
140+
app.appendStart()
143141
if !r.Time().IsZero() {
144142
key := "time"
145143
val := r.Time().Round(0) // strip monotonic to match Attr behavior
146144
if rep == nil {
147-
h.appendKey(key)
148-
h.appendTime(val)
145+
app.appendKey(key)
146+
app.appendTime(val)
149147
} else {
150148
replace(Time(key, val))
151149
}
152-
h.appendSep()
150+
app.appendSep()
153151
}
154152
if r.Level() != 0 {
155153
key := "level"
156154
val := r.Level()
157155
if rep == nil {
158-
h.appendKey(key)
159-
h.appendString(val.String())
156+
app.appendKey(key)
157+
app.appendString(val.String())
160158
} else {
161159
// Don't use replace: we want to output Levels as strings
162160
// to match the above.
163161
a := rep(Any(key, val))
164162
if a.Key() != "" {
165-
h.appendKey(a.Key())
163+
app.appendKey(a.Key())
166164
if l, ok := a.any.(Level); ok {
167-
h.appendString(l.String())
165+
app.appendString(l.String())
168166
} else {
169-
h.appendAttrValue(a)
167+
app.appendAttrValue(a)
170168
}
171169
}
172170
}
173-
h.appendSep()
171+
app.appendSep()
174172
}
175173
if h.opts.AddSource {
176174
file, line := r.SourceLine()
177175
if file != "" {
178176
key := "source"
179177
if rep == nil {
180-
h.appendKey(key)
181-
h.appendSource(file, line)
178+
app.appendKey(key)
179+
app.appendSource(file, line)
182180
} else {
183181
buf := buffer.New()
184182
buf.WriteString(file)
@@ -188,14 +186,14 @@ func (h *commonHandler) handle(r Record) error {
188186
buf.Free()
189187
replace(String(key, s))
190188
}
191-
h.appendSep()
189+
app.appendSep()
192190
}
193191
}
194192
key := "msg"
195193
val := r.Message()
196194
if rep == nil {
197-
h.appendKey(key)
198-
h.appendString(val)
195+
app.appendKey(key)
196+
app.appendString(val)
199197
} else {
200198
replace(String(key, val))
201199
}
@@ -205,9 +203,9 @@ func (h *commonHandler) handle(r Record) error {
205203
if rep != nil {
206204
a = rep(a)
207205
}
208-
h.appendAttr(a)
206+
appendAttr(app, a)
209207
}
210-
h.appendEnd()
208+
app.appendEnd()
211209
buf.WriteByte('\n')
212210

213211
h.mu.Lock()
@@ -216,20 +214,19 @@ func (h *commonHandler) handle(r Record) error {
216214
return err
217215
}
218216

219-
func (h *commonHandler) appendAttr(a Attr) {
217+
func appendAttr(app appender, a Attr) {
220218
if a.Key() != "" {
221-
h.appendSep()
222-
h.appendKey(a.Key())
223-
if err := h.appendAttrValue(a); err != nil {
224-
h.appendString(fmt.Sprintf("!ERROR:%v", err))
219+
app.appendSep()
220+
app.appendKey(a.Key())
221+
if err := app.appendAttrValue(a); err != nil {
222+
app.appendString(fmt.Sprintf("!ERROR:%v", err))
225223
}
226224
}
227225
}
228226

229227
// An appender appends keys and values to a buffer.
230228
// TextHandler and JSONHandler both implement it.
231229
type appender interface {
232-
setBuffer(*buffer.Buffer) // set the buffer to use for output
233230
appendStart() // start a sequence of Attrs
234231
appendEnd() // end a sequence of Attrs
235232
appendSep() // separate one Attr from the next

slog/handler_test.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ func TestCommonHandle(t *testing.T) {
8787
t.Run(test.name, func(t *testing.T) {
8888
ma := &memAppender{m: map[string]any{}}
8989
test.h.w = &bytes.Buffer{}
90-
test.h.appender = ma
90+
test.h.newAppender = func(*buffer.Buffer) appender { return ma }
9191
if err := test.h.handle(r); err != nil {
9292
t.Fatal(err)
9393
}
@@ -105,8 +105,6 @@ type memAppender struct {
105105

106106
func (a *memAppender) set(v any) { a.m[a.key] = v }
107107

108-
func (a *memAppender) setBuffer(*buffer.Buffer) {}
109-
110108
func (a *memAppender) appendStart() {}
111109
func (a *memAppender) appendSep() {}
112110
func (a *memAppender) appendEnd() {}

slog/text_handler.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// Copyright 2022 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package slog
6+
7+
import (
8+
"encoding"
9+
"fmt"
10+
"io"
11+
"strconv"
12+
"strings"
13+
"time"
14+
"unicode"
15+
16+
"golang.org/x/exp/slog/internal/buffer"
17+
)
18+
19+
// TextHandler is a Handler that writes Records to an io.Writer as a
20+
// sequence of key=value pairs separated by spaces and followed by a newline.
21+
type TextHandler struct {
22+
*commonHandler
23+
}
24+
25+
// NewTextHandler creates a TextHandler that writes to w,
26+
// using the default options.
27+
func NewTextHandler(w io.Writer) *TextHandler {
28+
return (HandlerOptions{}).NewTextHandler(w)
29+
}
30+
31+
// NewTextHandler creates a TextHandler with the given options that writes to w.
32+
func (opts HandlerOptions) NewTextHandler(w io.Writer) *TextHandler {
33+
return &TextHandler{
34+
&commonHandler{
35+
newAppender: func(buf *buffer.Buffer) appender { return (*textAppender)(buf) },
36+
w: w,
37+
opts: opts,
38+
},
39+
}
40+
}
41+
42+
// With returns a new TextHandler whose attributes consists
43+
// of h's attributes followed by attrs.
44+
func (h *TextHandler) With(attrs []Attr) Handler {
45+
return &TextHandler{commonHandler: h.commonHandler.with(attrs)}
46+
}
47+
48+
// Handle formats its argument Record as a single line of space-separated
49+
// key=value items.
50+
//
51+
// If the Record's time is zero, the time is omitted.
52+
// Otherwise, the key is "time"
53+
// and the value is output in RFC3339 format with millisecond precision.
54+
//
55+
// If the Record's level is zero, the level is omitted.
56+
// Otherwise, the key is "level"
57+
// and the value of [Level.String] is output.
58+
//
59+
// If the AddSource option is set and source information is available,
60+
// the key is "source" and the value is output as FILE:LINE.
61+
//
62+
// The message's key "msg".
63+
//
64+
// To modify these or other attributes, or remove them from the output, use
65+
// [HandlerOptions.ReplaceAttr].
66+
//
67+
// If a value implements [encoding.TextMarshaler], the result of MarshalText is
68+
// written. Otherwise, the result of fmt.Sprint is written.
69+
//
70+
// Keys and values are quoted if they are long or contain Unicode space
71+
// characters, '"' or '='.
72+
//
73+
// Each call to Handle results in a single serialized call to
74+
// io.Writer.Write.
75+
func (h *TextHandler) Handle(r Record) error {
76+
return h.commonHandler.handle(r)
77+
}
78+
79+
type textAppender buffer.Buffer
80+
81+
func (a *textAppender) buf() *buffer.Buffer { return (*buffer.Buffer)(a) }
82+
83+
func (a *textAppender) appendKey(key string) {
84+
a.appendString(key)
85+
a.buf().WriteByte('=')
86+
}
87+
88+
func (a *textAppender) appendString(s string) {
89+
if needsQuoting(s) {
90+
*a.buf() = strconv.AppendQuote(*a.buf(), s)
91+
} else {
92+
a.buf().WriteString(s)
93+
}
94+
}
95+
96+
func needsQuoting(s string) bool {
97+
return len(s) > maxCheckQuoteSize ||
98+
strings.IndexFunc(s, func(r rune) bool {
99+
return unicode.IsSpace(r) || r == '"' || r == '='
100+
}) >= 0
101+
}
102+
103+
func (a *textAppender) appendStart() {}
104+
func (a *textAppender) appendEnd() {}
105+
func (a *textAppender) appendSep() { a.buf().WriteByte(' ') }
106+
107+
const maxCheckQuoteSize = 80
108+
109+
func (a *textAppender) appendTime(t time.Time) {
110+
*a.buf() = appendTimeRFC3339Millis(*a.buf(), t)
111+
}
112+
113+
func (a *textAppender) appendSource(file string, line int) {
114+
a.appendString(file)
115+
a.buf().WriteByte(':')
116+
itoa((*[]byte)(a.buf()), line, -1)
117+
}
118+
119+
func (ap *textAppender) appendAttrValue(a Attr) error {
120+
switch a.Kind() {
121+
case StringKind:
122+
ap.appendString(a.str())
123+
return nil
124+
case TimeKind:
125+
ap.appendTime(a.Time())
126+
case AnyKind:
127+
if tm, ok := a.any.(encoding.TextMarshaler); ok {
128+
data, err := tm.MarshalText()
129+
if err != nil {
130+
ap.appendString("!ERROR:" + err.Error())
131+
} else {
132+
// TODO: avoid the conversion to string.
133+
ap.appendString(string(data))
134+
}
135+
return nil
136+
}
137+
ap.appendString(fmt.Sprint(a.Value()))
138+
default:
139+
*ap.buf() = a.AppendValue(*ap.buf())
140+
}
141+
return nil
142+
}

0 commit comments

Comments
 (0)