forked from toolshed/abra
		
	
		
			
				
	
	
		
			337 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			337 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // 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,
 | |
| }
 |