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