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  }