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 "bytes"
9 "context"
10 "encoding/json"
11 "errors"
12 "fmt"
13 "io"
14 "strconv"
15 "time"
16 "unicode/utf8"
17 18 "golang.org/x/exp/slog/internal/buffer"
19 )
20 21 // JSONHandler is a Handler that writes Records to an io.Writer as
22 // line-delimited JSON objects.
23 type JSONHandler struct {
24 *commonHandler
25 }
26 27 // NewJSONHandler creates a JSONHandler that writes to w,
28 // using the given options.
29 // If opts is nil, the default options are used.
30 func NewJSONHandler(w io.Writer, opts *HandlerOptions) *JSONHandler {
31 if opts == nil {
32 opts = &HandlerOptions{}
33 }
34 return &JSONHandler{
35 &commonHandler{
36 json: true,
37 w: w,
38 opts: *opts,
39 },
40 }
41 }
42 43 // Enabled reports whether the handler handles records at the given level.
44 // The handler ignores records whose level is lower.
45 func (h *JSONHandler) Enabled(_ context.Context, level Level) bool {
46 return h.commonHandler.enabled(level)
47 }
48 49 // WithAttrs returns a new JSONHandler whose attributes consists
50 // of h's attributes followed by attrs.
51 func (h *JSONHandler) WithAttrs(attrs []Attr) Handler {
52 return &JSONHandler{commonHandler: h.commonHandler.withAttrs(attrs)}
53 }
54 55 func (h *JSONHandler) WithGroup(name string) Handler {
56 return &JSONHandler{commonHandler: h.commonHandler.withGroup(name)}
57 }
58 59 // Handle formats its argument Record as a JSON object on a single line.
60 //
61 // If the Record's time is zero, the time is omitted.
62 // Otherwise, the key is "time"
63 // and the value is output as with json.Marshal.
64 //
65 // If the Record's level is zero, the level is omitted.
66 // Otherwise, the key is "level"
67 // and the value of [Level.String] is output.
68 //
69 // If the AddSource option is set and source information is available,
70 // the key is "source"
71 // and the value is output as "FILE:LINE".
72 //
73 // The message's key is "msg".
74 //
75 // To modify these or other attributes, or remove them from the output, use
76 // [HandlerOptions.ReplaceAttr].
77 //
78 // Values are formatted as with an [encoding/json.Encoder] with SetEscapeHTML(false),
79 // with two exceptions.
80 //
81 // First, an Attr whose Value is of type error is formatted as a string, by
82 // calling its Error method. Only errors in Attrs receive this special treatment,
83 // not errors embedded in structs, slices, maps or other data structures that
84 // are processed by the encoding/json package.
85 //
86 // Second, an encoding failure does not cause Handle to return an error.
87 // Instead, the error message is formatted as a string.
88 //
89 // Each call to Handle results in a single serialized call to io.Writer.Write.
90 func (h *JSONHandler) Handle(_ context.Context, r Record) error {
91 return h.commonHandler.handle(r)
92 }
93 94 // Adapted from time.Time.MarshalJSON to avoid allocation.
95 func appendJSONTime(s *handleState, t time.Time) {
96 if y := t.Year(); y < 0 || y >= 10000 {
97 // RFC 3339 is clear that years are 4 digits exactly.
98 // See golang.org/issue/4556#c15 for more discussion.
99 s.appendError(errors.New("time.Time year outside of range [0,9999]"))
100 }
101 s.buf.WriteByte('"')
102 *s.buf = t.AppendFormat(*s.buf, time.RFC3339Nano)
103 s.buf.WriteByte('"')
104 }
105 106 func appendJSONValue(s *handleState, v Value) error {
107 switch v.Kind() {
108 case KindString:
109 s.appendString(v.str())
110 case KindInt64:
111 *s.buf = strconv.AppendInt(*s.buf, v.Int64(), 10)
112 case KindUint64:
113 *s.buf = strconv.AppendUint(*s.buf, v.Uint64(), 10)
114 case KindFloat64:
115 // json.Marshal is funny about floats; it doesn't
116 // always match strconv.AppendFloat. So just call it.
117 // That's expensive, but floats are rare.
118 if err := appendJSONMarshal(s.buf, v.Float64()); err != nil {
119 return err
120 }
121 case KindBool:
122 *s.buf = strconv.AppendBool(*s.buf, v.Bool())
123 case KindDuration:
124 // Do what json.Marshal does.
125 *s.buf = strconv.AppendInt(*s.buf, int64(v.Duration()), 10)
126 case KindTime:
127 s.appendTime(v.Time())
128 case KindAny:
129 a := v.Any()
130 _, jm := a.(json.Marshaler)
131 if err, ok := a.(error); ok && !jm {
132 s.appendString(err.Error())
133 } else {
134 return appendJSONMarshal(s.buf, a)
135 }
136 default:
137 panic(fmt.Sprintf("bad kind: %s", v.Kind()))
138 }
139 return nil
140 }
141 142 func appendJSONMarshal(buf *buffer.Buffer, v any) error {
143 // Use a json.Encoder to avoid escaping HTML.
144 var bb bytes.Buffer
145 enc := json.NewEncoder(&bb)
146 enc.SetEscapeHTML(false)
147 if err := enc.Encode(v); err != nil {
148 return err
149 }
150 bs := bb.Bytes()
151 buf.Write(bs[:len(bs)-1]) // remove final newline
152 return nil
153 }
154 155 // appendEscapedJSONString escapes s for JSON and appends it to buf.
156 // It does not surround the string in quotation marks.
157 //
158 // Modified from encoding/json/encode.go:encodeState.string,
159 // with escapeHTML set to false.
160 func appendEscapedJSONString(buf []byte, s string) []byte {
161 char := func(b byte) { buf = append(buf, b) }
162 str := func(s string) { buf = append(buf, s...) }
163 164 start := 0
165 for i := 0; i < len(s); {
166 if b := s[i]; b < utf8.RuneSelf {
167 if safeSet[b] {
168 i++
169 continue
170 }
171 if start < i {
172 str(s[start:i])
173 }
174 char('\\')
175 switch b {
176 case '\\', '"':
177 char(b)
178 case '\n':
179 char('n')
180 case '\r':
181 char('r')
182 case '\t':
183 char('t')
184 default:
185 // This encodes bytes < 0x20 except for \t, \n and \r.
186 str(`u00`)
187 char(hex[b>>4])
188 char(hex[b&0xF])
189 }
190 i++
191 start = i
192 continue
193 }
194 c, size := utf8.DecodeRuneInString(s[i:])
195 if c == utf8.RuneError && size == 1 {
196 if start < i {
197 str(s[start:i])
198 }
199 str(`\ufffd`)
200 i += size
201 start = i
202 continue
203 }
204 // U+2028 is LINE SEPARATOR.
205 // U+2029 is PARAGRAPH SEPARATOR.
206 // They are both technically valid characters in JSON strings,
207 // but don't work in JSONP, which has to be evaluated as JavaScript,
208 // and can lead to security holes there. It is valid JSON to
209 // escape them, so we do so unconditionally.
210 // See http://timelessrepo.com/json-isnt-a-javascript-subset for discussion.
211 if c == '\u2028' || c == '\u2029' {
212 if start < i {
213 str(s[start:i])
214 }
215 str(`\u202`)
216 char(hex[c&0xF])
217 i += size
218 start = i
219 continue
220 }
221 i += size
222 }
223 if start < len(s) {
224 str(s[start:])
225 }
226 return buf
227 }
228 229 var hex = "0123456789abcdef"
230 231 // Copied from encoding/json/tables.go.
232 //
233 // safeSet holds the value true if the ASCII character with the given array
234 // position can be represented inside a JSON string without any further
235 // escaping.
236 //
237 // All values are true except for the ASCII control characters (0-31), the
238 // double quote ("), and the backslash character ("\").
239 var safeSet = [utf8.RuneSelf]bool{
240 ' ': true,
241 '!': true,
242 '"': false,
243 '#': true,
244 '$': true,
245 '%': true,
246 '&': true,
247 '\'': true,
248 '(': true,
249 ')': true,
250 '*': true,
251 '+': true,
252 ',': true,
253 '-': true,
254 '.': true,
255 '/': true,
256 '0': true,
257 '1': true,
258 '2': true,
259 '3': true,
260 '4': true,
261 '5': true,
262 '6': true,
263 '7': true,
264 '8': true,
265 '9': true,
266 ':': true,
267 ';': true,
268 '<': true,
269 '=': true,
270 '>': true,
271 '?': true,
272 '@': true,
273 'A': true,
274 'B': true,
275 'C': true,
276 'D': true,
277 'E': true,
278 'F': true,
279 'G': true,
280 'H': true,
281 'I': true,
282 'J': true,
283 'K': true,
284 'L': true,
285 'M': true,
286 'N': true,
287 'O': true,
288 'P': true,
289 'Q': true,
290 'R': true,
291 'S': true,
292 'T': true,
293 'U': true,
294 'V': true,
295 'W': true,
296 'X': true,
297 'Y': true,
298 'Z': true,
299 '[': true,
300 '\\': false,
301 ']': true,
302 '^': true,
303 '_': true,
304 '`': true,
305 'a': true,
306 'b': true,
307 'c': true,
308 'd': true,
309 'e': true,
310 'f': true,
311 'g': true,
312 'h': true,
313 'i': true,
314 'j': true,
315 'k': true,
316 'l': true,
317 'm': true,
318 'n': true,
319 'o': true,
320 'p': true,
321 'q': true,
322 'r': true,
323 's': true,
324 't': true,
325 'u': true,
326 'v': true,
327 'w': true,
328 'x': true,
329 'y': true,
330 'z': true,
331 '{': true,
332 '|': true,
333 '}': true,
334 '~': true,
335 '\u007f': true,
336 }
337