123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351 |
- package logger
- import (
- "encoding/json"
- "log/slog"
- "time"
- "unicode/utf8"
- )
- const (
- jsonGroupOpenToken = '{'
- jsonGroupCloseToken = '}'
- )
- var jsonAttributeSep = []byte(",")
- // formatterJSON is a JSON log formatter.
- type formatterJSON struct {
- formatterCommon
- levelKey string
- messageKey string
- sep []byte
- groupsOpened int
- }
- // newFormatterJSON creates a new formatterJSON instance.
- func newFormatterJSON(groups []attrGroup, buf *buffer, gcpStyle bool) *formatterJSON {
- f := &formatterJSON{
- formatterCommon: newFormatterCommon(groups, buf),
- }
- // Set the level and message keys based on the style.
- if gcpStyle {
- f.levelKey = "severity"
- f.messageKey = "message"
- } else {
- f.levelKey = slog.LevelKey
- f.messageKey = slog.MessageKey
- }
- return f
- }
- // format formats a log record.
- func (s *formatterJSON) format(r slog.Record) {
- // Open the JSON object and defer closing it.
- s.buf.append(jsonGroupOpenToken)
- defer func() {
- s.buf.append(jsonGroupCloseToken)
- }()
- // Append timestamp
- s.appendKey(slog.TimeKey)
- s.appendTime(r.Time)
- // Append log level
- s.appendKey(s.levelKey)
- s.appendString(s.levelName(r.Level))
- // Append message
- s.appendKey(s.messageKey)
- s.appendString(r.Message)
- // Append groups added with [Handler.WithAttrs] and [Handler.WithGroup]
- for _, g := range s.groups {
- if g.name != "" {
- s.openGroup(g.name)
- }
- s.appendAttributes(g.attrs)
- }
- // Append attributes from the record
- r.Attrs(func(attr slog.Attr) bool {
- s.appendAttribute(attr)
- return true
- })
- // Close all opened groups.
- for s.groupsOpened > 0 {
- s.closeGroup()
- }
- // Append error, source, and stack if present
- if s.error.Key != "" {
- s.appendKey(s.error.Key)
- s.appendValue(s.error.Value)
- }
- if s.source.Key != "" {
- s.appendKey(s.source.Key)
- s.appendValue(s.source.Value)
- }
- if s.stack.Key != "" {
- s.appendKey(s.stack.Key)
- s.appendValue(s.stack.Value)
- }
- }
- // appendAttributes appends a list of attributes to the buffer.
- func (s *formatterJSON) appendAttributes(attrs []slog.Attr) {
- for _, attr := range attrs {
- s.appendAttribute(attr)
- }
- }
- // appendAttribute appends a single attribute to the buffer.
- func (s *formatterJSON) appendAttribute(attr slog.Attr) {
- // Resolve [slog.LogValuer] values
- attr.Value = attr.Value.Resolve()
- // If there are no groups opened, save special attributes for later
- if s.groupsOpened == 0 && s.saveSpecialAttr(attr) {
- return
- }
- // Groups need special handling
- if attr.Value.Kind() == slog.KindGroup {
- s.appendGroup(attr.Key, attr.Value.Group())
- return
- }
- s.appendKey(attr.Key)
- s.appendValue(attr.Value)
- }
- // appendKey appends an attribute key to the buffer.
- func (s *formatterJSON) appendKey(key string) {
- s.buf.append(s.sep...)
- s.sep = jsonAttributeSep
- s.appendString(key)
- s.buf.append(':')
- }
- // appendValue appends a value to the buffer, applying quoting rules as necessary.
- func (s *formatterJSON) appendValue(val slog.Value) {
- switch val.Kind() {
- case slog.KindString:
- s.appendString(val.String())
- case slog.KindInt64:
- s.buf.appendInt(val.Int64())
- case slog.KindUint64:
- s.buf.appendUint(val.Uint64())
- case slog.KindFloat64:
- // strconv.FormatFloat result sometimes differs from json.Marshal,
- // so we use json.Marshal for consistency.
- s.appendJSONMarshal(val.Float64())
- case slog.KindBool:
- s.buf.appendBool(val.Bool())
- case slog.KindDuration:
- s.buf.appendInt(int64(val.Duration()))
- case slog.KindTime:
- s.appendTime(val.Time())
- default:
- s.appendJSONMarshal(val.Any())
- }
- }
- // appendString appends a string value to the buffer.
- // If the string does not require escaping, it is appended directly.
- // Otherwise, it is JSON marshaled.
- func (s *formatterJSON) appendString(val string) {
- if !s.isStringSafe(val) {
- s.appendJSONMarshal(val)
- return
- }
- s.buf.append('"')
- s.buf.appendStringRaw(val)
- s.buf.append('"')
- }
- // isStringSafe checks if a string is safe to append without escaping.
- func (s *formatterJSON) isStringSafe(val string) bool {
- for i := 0; i < len(val); i++ {
- if b := val[i]; b >= utf8.RuneSelf || !jsonSafeSet[b] {
- return false
- }
- }
- return true
- }
- // appendTime appends a time value to the buffer.
- func (s *formatterJSON) appendTime(val time.Time) {
- s.buf.append('"')
- s.buf.appendStringRaw(val.Format(time.RFC3339))
- s.buf.append('"')
- }
- // appendJSONMarshal appends a JSON marshaled value to the buffer.
- func (s *formatterJSON) appendJSONMarshal(val any) {
- if err, ok := val.(error); ok && err != nil {
- s.appendString(err.Error())
- return
- }
- buf := newBuffer()
- defer func() {
- buf.free()
- }()
- enc := json.NewEncoder(buf)
- enc.SetEscapeHTML(false)
- if err := enc.Encode(val); err != nil {
- // This should be a very unlikely situation, but just in case...
- s.buf.appendStringRaw(`"<json marshal error>"`)
- return
- }
- buf.removeNewline()
- s.buf.append(*buf...)
- }
- // appendGroup appends a group of attributes to the buffer.
- func (s *formatterJSON) appendGroup(name string, attrs []slog.Attr) {
- if len(attrs) == 0 {
- return
- }
- if len(name) > 0 {
- // If the group has a name, open it and defer closing it.
- // Unnamed groups should be treated as sets of regular attributes.
- s.openGroup(name)
- defer s.closeGroup()
- }
- s.appendAttributes(attrs)
- }
- // openGroup opens a new group in the buffer.
- func (s *formatterJSON) openGroup(name string) {
- s.groupsOpened++
- s.appendKey(name)
- s.buf.append(jsonGroupOpenToken)
- s.sep = nil
- }
- // closeGroup closes the most recently opened group in the buffer.
- func (s *formatterJSON) closeGroup() {
- s.groupsOpened--
- s.buf.append(jsonGroupCloseToken)
- s.sep = jsonAttributeSep
- }
- // jsonSafeSet is a set of runes that are safe to include in JSON strings without escaping.
- // Some runes here are explicitly marked as unsafe for clarity.
- // The unlisted runes are considered unsafe by default.
- // Shamesly stolen from https://github.com/golang/go/blob/master/src/encoding/json/tables.go.
- var jsonSafeSet = [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,
- }
|