| // Copyright 2022 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package slog | |
| import ( | |
| "bytes" | |
| "context" | |
| "encoding/json" | |
| "errors" | |
| "fmt" | |
| "io" | |
| "strconv" | |
| "time" | |
| "unicode/utf8" | |
| "golang.org/x/exp/slog/internal/buffer" | |
| ) | |
| // JSONHandler is a Handler that writes Records to an io.Writer as | |
| // line-delimited JSON objects. | |
| type JSONHandler struct { | |
| *commonHandler | |
| } | |
| // NewJSONHandler creates a JSONHandler that writes to w, | |
| // using the given options. | |
| // If opts is nil, the default options are used. | |
| func NewJSONHandler(w io.Writer, opts *HandlerOptions) *JSONHandler { | |
| if opts == nil { | |
| opts = &HandlerOptions{} | |
| } | |
| return &JSONHandler{ | |
| &commonHandler{ | |
| json: true, | |
| w: w, | |
| opts: *opts, | |
| }, | |
| } | |
| } | |
| // Enabled reports whether the handler handles records at the given level. | |
| // The handler ignores records whose level is lower. | |
| func (h *JSONHandler) Enabled(_ context.Context, level Level) bool { | |
| return h.commonHandler.enabled(level) | |
| } | |
| // WithAttrs returns a new JSONHandler whose attributes consists | |
| // of h's attributes followed by attrs. | |
| func (h *JSONHandler) WithAttrs(attrs []Attr) Handler { | |
| return &JSONHandler{commonHandler: h.commonHandler.withAttrs(attrs)} | |
| } | |
| func (h *JSONHandler) WithGroup(name string) Handler { | |
| return &JSONHandler{commonHandler: h.commonHandler.withGroup(name)} | |
| } | |
| // Handle formats its argument Record as a JSON object on a single line. | |
| // | |
| // If the Record's time is zero, the time is omitted. | |
| // Otherwise, the key is "time" | |
| // and the value is output as with json.Marshal. | |
| // | |
| // If the Record's level is zero, the level is omitted. | |
| // Otherwise, the key is "level" | |
| // and the value of [Level.String] is output. | |
| // | |
| // If the AddSource option is set and source information is available, | |
| // the key is "source" | |
| // and the value is output as "FILE:LINE". | |
| // | |
| // The message's key is "msg". | |
| // | |
| // To modify these or other attributes, or remove them from the output, use | |
| // [HandlerOptions.ReplaceAttr]. | |
| // | |
| // Values are formatted as with an [encoding/json.Encoder] with SetEscapeHTML(false), | |
| // with two exceptions. | |
| // | |
| // First, an Attr whose Value is of type error is formatted as a string, by | |
| // calling its Error method. Only errors in Attrs receive this special treatment, | |
| // not errors embedded in structs, slices, maps or other data structures that | |
| // are processed by the encoding/json package. | |
| // | |
| // Second, an encoding failure does not cause Handle to return an error. | |
| // Instead, the error message is formatted as a string. | |
| // | |
| // Each call to Handle results in a single serialized call to io.Writer.Write. | |
| func (h *JSONHandler) Handle(_ context.Context, r Record) error { | |
| return h.commonHandler.handle(r) | |
| } | |
| // Adapted from time.Time.MarshalJSON to avoid allocation. | |
| func appendJSONTime(s *handleState, t time.Time) { | |
| if y := t.Year(); y < 0 || y >= 10000 { | |
| // RFC 3339 is clear that years are 4 digits exactly. | |
| // See golang.org/issue/4556#c15 for more discussion. | |
| s.appendError(errors.New("time.Time year outside of range [0,9999]")) | |
| } | |
| s.buf.WriteByte('"') | |
| *s.buf = t.AppendFormat(*s.buf, time.RFC3339Nano) | |
| s.buf.WriteByte('"') | |
| } | |
| func appendJSONValue(s *handleState, v Value) error { | |
| switch v.Kind() { | |
| case KindString: | |
| s.appendString(v.str()) | |
| case KindInt64: | |
| *s.buf = strconv.AppendInt(*s.buf, v.Int64(), 10) | |
| case KindUint64: | |
| *s.buf = strconv.AppendUint(*s.buf, v.Uint64(), 10) | |
| case KindFloat64: | |
| // json.Marshal is funny about floats; it doesn't | |
| // always match strconv.AppendFloat. So just call it. | |
| // That's expensive, but floats are rare. | |
| if err := appendJSONMarshal(s.buf, v.Float64()); err != nil { | |
| return err | |
| } | |
| case KindBool: | |
| *s.buf = strconv.AppendBool(*s.buf, v.Bool()) | |
| case KindDuration: | |
| // Do what json.Marshal does. | |
| *s.buf = strconv.AppendInt(*s.buf, int64(v.Duration()), 10) | |
| case KindTime: | |
| s.appendTime(v.Time()) | |
| case KindAny: | |
| a := v.Any() | |
| _, jm := a.(json.Marshaler) | |
| if err, ok := a.(error); ok && !jm { | |
| s.appendString(err.Error()) | |
| } else { | |
| return appendJSONMarshal(s.buf, a) | |
| } | |
| default: | |
| panic(fmt.Sprintf("bad kind: %s", v.Kind())) | |
| } | |
| return nil | |
| } | |
| func appendJSONMarshal(buf *buffer.Buffer, v any) error { | |
| // Use a json.Encoder to avoid escaping HTML. | |
| var bb bytes.Buffer | |
| enc := json.NewEncoder(&bb) | |
| enc.SetEscapeHTML(false) | |
| if err := enc.Encode(v); err != nil { | |
| return err | |
| } | |
| bs := bb.Bytes() | |
| buf.Write(bs[:len(bs)-1]) // remove final newline | |
| return nil | |
| } | |
| // appendEscapedJSONString escapes s for JSON and appends it to buf. | |
| // It does not surround the string in quotation marks. | |
| // | |
| // Modified from encoding/json/encode.go:encodeState.string, | |
| // with escapeHTML set to false. | |
| func appendEscapedJSONString(buf []byte, s string) []byte { | |
| char := func(b byte) { buf = append(buf, b) } | |
| str := func(s string) { buf = append(buf, s...) } | |
| start := 0 | |
| for i := 0; i < len(s); { | |
| if b := s[i]; b < utf8.RuneSelf { | |
| if safeSet[b] { | |
| i++ | |
| continue | |
| } | |
| if start < i { | |
| str(s[start:i]) | |
| } | |
| char('\\') | |
| switch b { | |
| case '\\', '"': | |
| char(b) | |
| case '\n': | |
| char('n') | |
| case '\r': | |
| char('r') | |
| case '\t': | |
| char('t') | |
| default: | |
| // This encodes bytes < 0x20 except for \t, \n and \r. | |
| str(`u00`) | |
| char(hex[b>>4]) | |
| char(hex[b&0xF]) | |
| } | |
| i++ | |
| start = i | |
| continue | |
| } | |
| c, size := utf8.DecodeRuneInString(s[i:]) | |
| if c == utf8.RuneError && size == 1 { | |
| if start < i { | |
| str(s[start:i]) | |
| } | |
| str(`\ufffd`) | |
| i += size | |
| start = i | |
| continue | |
| } | |
| // U+2028 is LINE SEPARATOR. | |
| // U+2029 is PARAGRAPH SEPARATOR. | |
| // They are both technically valid characters in JSON strings, | |
| // but don't work in JSONP, which has to be evaluated as JavaScript, | |
| // and can lead to security holes there. It is valid JSON to | |
| // escape them, so we do so unconditionally. | |
| // See http://timelessrepo.com/json-isnt-a-javascript-subset for discussion. | |
| if c == '\u2028' || c == '\u2029' { | |
| if start < i { | |
| str(s[start:i]) | |
| } | |
| str(`\u202`) | |
| char(hex[c&0xF]) | |
| i += size | |
| start = i | |
| continue | |
| } | |
| i += size | |
| } | |
| if start < len(s) { | |
| str(s[start:]) | |
| } | |
| return buf | |
| } | |
| var hex = "0123456789abcdef" | |
| // Copied from encoding/json/tables.go. | |
| // | |
| // safeSet holds the value true if the ASCII character with the given array | |
| // position can be represented inside a JSON string without any further | |
| // escaping. | |
| // | |
| // All values are true except for the ASCII control characters (0-31), the | |
| // double quote ("), and the backslash character ("\"). | |
| var safeSet = [utf8.RuneSelf]bool{ | |
| ' ': true, | |
| '!': true, | |
| '"': false, | |
| '#': true, | |
| '$': true, | |
| '%': true, | |
| '&': true, | |
| '\'': true, | |
| '(': true, | |
| ')': true, | |
| '*': true, | |
| '+': true, | |
| ',': true, | |
| '-': true, | |
| '.': true, | |
| '/': true, | |
| '0': true, | |
| '1': true, | |
| '2': true, | |
| '3': true, | |
| '4': true, | |
| '5': true, | |
| '6': true, | |
| '7': true, | |
| '8': true, | |
| '9': true, | |
| ':': true, | |
| ';': true, | |
| '<': true, | |
| '=': true, | |
| '>': true, | |
| '?': true, | |
| '@': true, | |
| 'A': true, | |
| 'B': true, | |
| 'C': true, | |
| 'D': true, | |
| 'E': true, | |
| 'F': true, | |
| 'G': true, | |
| 'H': true, | |
| 'I': true, | |
| 'J': true, | |
| 'K': true, | |
| 'L': true, | |
| 'M': true, | |
| 'N': true, | |
| 'O': true, | |
| 'P': true, | |
| 'Q': true, | |
| 'R': true, | |
| 'S': true, | |
| 'T': true, | |
| 'U': true, | |
| 'V': true, | |
| 'W': true, | |
| 'X': true, | |
| 'Y': true, | |
| 'Z': true, | |
| '[': true, | |
| '\\': false, | |
| ']': true, | |
| '^': true, | |
| '_': true, | |
| '`': true, | |
| 'a': true, | |
| 'b': true, | |
| 'c': true, | |
| 'd': true, | |
| 'e': true, | |
| 'f': true, | |
| 'g': true, | |
| 'h': true, | |
| 'i': true, | |
| 'j': true, | |
| 'k': true, | |
| 'l': true, | |
| 'm': true, | |
| 'n': true, | |
| 'o': true, | |
| 'p': true, | |
| 'q': true, | |
| 'r': true, | |
| 's': true, | |
| 't': true, | |
| 'u': true, | |
| 'v': true, | |
| 'w': true, | |
| 'x': true, | |
| 'y': true, | |
| 'z': true, | |
| '{': true, | |
| '|': true, | |
| '}': true, | |
| '~': true, | |
| '\u007f': true, | |
| } |