// 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, }