mirror of
https://github.com/imgproxy/imgproxy.git
synced 2025-12-28 22:21:10 +02:00
352 lines
7.4 KiB
Go
352 lines
7.4 KiB
Go
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,
|
|
}
|