golang.org/x/exp@v0.0.0-20240506185415-9bf2ced13842/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 "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 }