package format import ( "bytes" "fmt" "runtime" "sort" "sync" "time" "github.com/francoispqt/gojay" "github.com/mattermost/logr" ) // ContextField is a name/value pair within the context fields. type ContextField struct { Key string Val interface{} } // JSON formats log records as JSON. type JSON struct { // DisableTimestamp disables output of timestamp field. DisableTimestamp bool // DisableLevel disables output of level field. DisableLevel bool // DisableMsg disables output of msg field. DisableMsg bool // DisableContext disables output of all context fields. DisableContext bool // DisableStacktrace disables output of stack trace. DisableStacktrace bool // TimestampFormat is an optional format for timestamps. If empty // then DefTimestampFormat is used. TimestampFormat string // Deprecated: this has no effect. Indent string // EscapeHTML determines if certain characters (e.g. `<`, `>`, `&`) // are escaped. EscapeHTML bool // KeyTimestamp overrides the timestamp field key name. KeyTimestamp string // KeyLevel overrides the level field key name. KeyLevel string // KeyMsg overrides the msg field key name. KeyMsg string // KeyContextFields when not empty will group all context fields // under this key. KeyContextFields string // KeyStacktrace overrides the stacktrace field key name. KeyStacktrace string // ContextSorter allows custom sorting for the context fields. ContextSorter func(fields logr.Fields) []ContextField once sync.Once } // Format converts a log record to bytes in JSON format. func (j *JSON) Format(rec *logr.LogRec, stacktrace bool, buf *bytes.Buffer) (*bytes.Buffer, error) { j.once.Do(j.applyDefaultKeyNames) if buf == nil { buf = &bytes.Buffer{} } enc := gojay.BorrowEncoder(buf) defer func() { enc.Release() }() sorter := j.ContextSorter if sorter == nil { sorter = j.defaultContextSorter } jlr := JSONLogRec{ LogRec: rec, JSON: j, stacktrace: stacktrace, sorter: sorter, } err := enc.EncodeObject(jlr) if err != nil { return nil, err } buf.WriteByte('\n') return buf, nil } func (j *JSON) applyDefaultKeyNames() { if j.KeyTimestamp == "" { j.KeyTimestamp = "timestamp" } if j.KeyLevel == "" { j.KeyLevel = "level" } if j.KeyMsg == "" { j.KeyMsg = "msg" } if j.KeyStacktrace == "" { j.KeyStacktrace = "stacktrace" } } // defaultContextSorter sorts the context fields alphabetically by key. func (j *JSON) defaultContextSorter(fields logr.Fields) []ContextField { keys := make([]string, 0, len(fields)) for k := range fields { keys = append(keys, k) } sort.Strings(keys) cf := make([]ContextField, 0, len(keys)) for _, k := range keys { cf = append(cf, ContextField{Key: k, Val: fields[k]}) } return cf } // JSONLogRec decorates a LogRec adding JSON encoding. type JSONLogRec struct { *logr.LogRec *JSON stacktrace bool sorter func(fields logr.Fields) []ContextField } // MarshalJSONObject encodes the LogRec as JSON. func (rec JSONLogRec) MarshalJSONObject(enc *gojay.Encoder) { if !rec.DisableTimestamp { timestampFmt := rec.TimestampFormat if timestampFmt == "" { timestampFmt = logr.DefTimestampFormat } time := rec.Time() enc.AddTimeKey(rec.KeyTimestamp, &time, timestampFmt) } if !rec.DisableLevel { enc.AddStringKey(rec.KeyLevel, rec.Level().Name) } if !rec.DisableMsg { enc.AddStringKey(rec.KeyMsg, rec.Msg()) } if !rec.DisableContext { ctxFields := rec.sorter(rec.Fields()) if rec.KeyContextFields != "" { enc.AddObjectKey(rec.KeyContextFields, jsonFields(ctxFields)) } else { if len(ctxFields) > 0 { for _, cf := range ctxFields { key := rec.prefixCollision(cf.Key) encodeField(enc, key, cf.Val) } } } } if rec.stacktrace && !rec.DisableStacktrace { frames := rec.StackFrames() if len(frames) > 0 { enc.AddArrayKey(rec.KeyStacktrace, stackFrames(frames)) } } } // IsNil returns true if the LogRec pointer is nil. func (rec JSONLogRec) IsNil() bool { return rec.LogRec == nil } func (rec JSONLogRec) prefixCollision(key string) string { switch key { case rec.KeyTimestamp, rec.KeyLevel, rec.KeyMsg, rec.KeyStacktrace: return rec.prefixCollision("_" + key) } return key } type stackFrames []runtime.Frame // MarshalJSONArray encodes stackFrames slice as JSON. func (s stackFrames) MarshalJSONArray(enc *gojay.Encoder) { for _, frame := range s { enc.AddObject(stackFrame(frame)) } } // IsNil returns true if stackFrames is empty slice. func (s stackFrames) IsNil() bool { return len(s) == 0 } type stackFrame runtime.Frame // MarshalJSONArray encodes stackFrame as JSON. func (f stackFrame) MarshalJSONObject(enc *gojay.Encoder) { enc.AddStringKey("Function", f.Function) enc.AddStringKey("File", f.File) enc.AddIntKey("Line", f.Line) } func (f stackFrame) IsNil() bool { return false } type jsonFields []ContextField // MarshalJSONObject encodes Fields map to JSON. func (f jsonFields) MarshalJSONObject(enc *gojay.Encoder) { for _, ctxField := range f { encodeField(enc, ctxField.Key, ctxField.Val) } } // IsNil returns true if map is nil. func (f jsonFields) IsNil() bool { return f == nil } func encodeField(enc *gojay.Encoder, key string, val interface{}) { switch vt := val.(type) { case gojay.MarshalerJSONObject: enc.AddObjectKey(key, vt) case gojay.MarshalerJSONArray: enc.AddArrayKey(key, vt) case string: enc.AddStringKey(key, vt) case error: enc.AddStringKey(key, vt.Error()) case bool: enc.AddBoolKey(key, vt) case int: enc.AddIntKey(key, vt) case int64: enc.AddInt64Key(key, vt) case int32: enc.AddIntKey(key, int(vt)) case int16: enc.AddIntKey(key, int(vt)) case int8: enc.AddIntKey(key, int(vt)) case uint64: enc.AddIntKey(key, int(vt)) case uint32: enc.AddIntKey(key, int(vt)) case uint16: enc.AddIntKey(key, int(vt)) case uint8: enc.AddIntKey(key, int(vt)) case float64: enc.AddFloatKey(key, vt) case float32: enc.AddFloat32Key(key, vt) case *gojay.EmbeddedJSON: enc.AddEmbeddedJSONKey(key, vt) case time.Time: enc.AddTimeKey(key, &vt, logr.DefTimestampFormat) case *time.Time: enc.AddTimeKey(key, vt, logr.DefTimestampFormat) default: s := fmt.Sprintf("%v", vt) enc.AddStringKey(key, s) } }