package logrus
import (
"bytes"
"fmt"
"maps"
"os"
"runtime"
"slices"
"strconv"
"strings"
"sync"
"time"
)
var baseTimestamp = time.Now()
type TextFormatter struct {
ForceColors bool
DisableColors bool
ForceQuote bool
DisableQuote bool
EnvironmentOverrideColors bool
DisableTimestamp bool
FullTimestamp bool
TimestampFormat string
DisableSorting bool
SortingFunc func([]string)
DisableLevelTruncation bool
PadLevelText bool
QuoteEmptyFields bool
terminal bool
FieldMap FieldMap
CallerPrettyfier func(*runtime.Frame) (function string, file string)
terminalInitOnce sync.Once
}
func (f *TextFormatter) isTerminal(entry *Entry) bool {
if entry == nil || entry.Logger == nil {
return false
}
f.terminalInitOnce.Do(func() {
entry.Logger.mu.Lock()
out := entry.Logger.Out
entry.Logger.mu.Unlock()
f.terminal = checkIfTerminal(out)
})
return f.terminal
}
func (f *TextFormatter) isColored(isTerminal bool) bool {
if f.DisableColors {
return false
}
colored := f.ForceColors || isTerminal
if !f.EnvironmentOverrideColors {
return colored
}
if force, ok := os.LookupEnv("CLICOLOR_FORCE"); ok {
return force != "0"
}
if os.Getenv("CLICOLOR") == "0" {
return false
}
return colored
}
func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
data := make(Fields, len(entry.Data))
maps.Copy(data, entry.Data)
isColored := f.isColored(f.isTerminal(entry))
caller := entry.Caller
hasCaller := caller != nil
prefixFieldClashes(data, f.FieldMap, hasCaller)
keys := make([]string, 0, len(data))
for k := range data {
keys = append(keys, k)
}
b := entry.Buffer
if b == nil {
b = new(bytes.Buffer)
}
if isColored {
f.printColored(b, entry, keys, data)
} else {
f.printPlain(b, entry, keys, data)
}
return b.Bytes(), nil
}
func (f *TextFormatter) printPlain(b *bytes.Buffer, entry *Entry, keys []string, data Fields) {
caller := entry.Caller
hasCaller := caller != nil
fixedKeys := make([]string, 0, len(keys)+defaultFields)
if !f.DisableTimestamp {
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyTime))
}
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLevel))
if entry.Message != "" {
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyMsg))
}
if entry.err != "" {
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLogrusError))
}
var funcVal, fileVal string
if caller != nil {
if f.CallerPrettyfier != nil {
funcVal, fileVal = f.CallerPrettyfier(caller)
} else {
funcVal = caller.Function
fileVal = caller.File + ":" + strconv.FormatInt(int64(caller.Line), 10)
}
if funcVal != "" {
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyFunc))
}
if fileVal != "" {
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyFile))
}
}
if !f.DisableSorting {
if f.SortingFunc == nil {
slices.Sort(keys)
fixedKeys = append(fixedKeys, keys...)
} else {
fixedKeys = append(fixedKeys, keys...)
f.SortingFunc(fixedKeys)
}
} else {
fixedKeys = append(fixedKeys, keys...)
}
for _, key := range fixedKeys {
var value any
switch {
case key == f.FieldMap.resolve(FieldKeyTime):
if f.TimestampFormat == "" {
value = entry.Time.Format(defaultTimestampFormat)
} else {
value = entry.Time.Format(f.TimestampFormat)
}
case key == f.FieldMap.resolve(FieldKeyLevel):
value = entry.Level.String()
case key == f.FieldMap.resolve(FieldKeyMsg):
value = entry.Message
case key == f.FieldMap.resolve(FieldKeyLogrusError):
value = entry.err
case key == f.FieldMap.resolve(FieldKeyFunc) && hasCaller:
value = funcVal
case key == f.FieldMap.resolve(FieldKeyFile) && hasCaller:
value = fileVal
default:
value = data[key]
}
f.appendKeyValue(b, key, value)
}
b.WriteByte('\n')
}
func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string, data Fields) {
entry.Message = strings.TrimSuffix(entry.Message, "\n")
var callerText string
if caller := entry.Caller; caller != nil {
var funcVal, fileVal string
if f.CallerPrettyfier != nil {
funcVal, fileVal = f.CallerPrettyfier(caller)
} else {
if caller.Function != "" {
funcVal = caller.Function + "()"
}
fileVal = caller.File + ":" + strconv.FormatInt(int64(caller.Line), 10)
}
if fileVal == "" {
callerText = funcVal
} else if funcVal == "" {
callerText = fileVal
} else {
callerText = fileVal + " " + funcVal
}
}
levelText := levelPrefix(entry.Level, f.DisableLevelTruncation, f.PadLevelText)
switch {
case f.DisableTimestamp:
_, _ = fmt.Fprintf(b, "%s%s %-44s ", levelText, callerText, entry.Message)
case !f.FullTimestamp:
_, _ = fmt.Fprintf(b, "%s[%04d]%s %-44s ", levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), callerText, entry.Message)
default:
timestampFormat := f.TimestampFormat
if timestampFormat == "" {
timestampFormat = defaultTimestampFormat
}
_, _ = fmt.Fprintf(b, "%s[%s]%s %-44s ", levelText, entry.Time.Format(timestampFormat), callerText, entry.Message)
}
if !f.DisableSorting {
if f.SortingFunc == nil {
slices.Sort(keys)
} else {
f.SortingFunc(keys)
}
}
for _, k := range keys {
b.WriteByte(' ')
b.WriteString(colorize(entry.Level, k))
b.WriteByte('=')
f.appendValue(b, data[k])
}
b.WriteByte('\n')
}
func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value any) {
if b.Len() > 0 {
b.WriteByte(' ')
}
b.WriteString(key)
b.WriteByte('=')
f.appendValue(b, value)
}
func (f *TextFormatter) appendValue(b *bytes.Buffer, value any) {
switch v := value.(type) {
case string:
f.appendString(b, v)
return
case []byte:
f.appendBytes(b, v)
return
case bool:
var raw [8]byte
f.appendBytes(b, strconv.AppendBool(raw[:0], v))
return
case error:
f.appendString(b, v.Error())
return
case fmt.Stringer:
f.appendString(b, v.String())
return
}
var raw [64]byte
var num []byte
switch v := value.(type) {
case int:
num = strconv.AppendInt(raw[:0], int64(v), 10)
case int8:
num = strconv.AppendInt(raw[:0], int64(v), 10)
case int16:
num = strconv.AppendInt(raw[:0], int64(v), 10)
case int32:
num = strconv.AppendInt(raw[:0], int64(v), 10)
case int64:
num = strconv.AppendInt(raw[:0], v, 10)
case uint:
num = strconv.AppendUint(raw[:0], uint64(v), 10)
case uint8:
num = strconv.AppendUint(raw[:0], uint64(v), 10)
case uint16:
num = strconv.AppendUint(raw[:0], uint64(v), 10)
case uint32:
num = strconv.AppendUint(raw[:0], uint64(v), 10)
case uint64:
num = strconv.AppendUint(raw[:0], v, 10)
case uintptr:
num = strconv.AppendUint(raw[:0], uint64(v), 10)
case float32:
num = strconv.AppendFloat(raw[:0], float64(v), 'g', -1, 32)
case float64:
num = strconv.AppendFloat(raw[:0], v, 'g', -1, 64)
default:
f.appendString(b, fmt.Sprint(value))
return
}
f.appendNumeric(b, num)
}
func (f *TextFormatter) appendString(b *bytes.Buffer, s string) {
quote := f.ForceQuote || (f.QuoteEmptyFields && len(s) == 0) || (!f.DisableQuote && needsQuoting(s))
if !quote {
b.WriteString(s)
return
}
if len(s) == 0 {
b.WriteString(`""`)
return
}
var tmp [128]byte
b.Write(strconv.AppendQuote(tmp[:0], s))
}
func (f *TextFormatter) appendBytes(b *bytes.Buffer, bs []byte) {
quote := f.ForceQuote || (f.QuoteEmptyFields && len(bs) == 0) || (!f.DisableQuote && needsQuotingBytes(bs))
if !quote {
b.Write(bs)
return
}
if len(bs) == 0 {
b.WriteString(`""`)
return
}
var tmp [128]byte
b.Write(strconv.AppendQuote(tmp[:0], string(bs)))
}
func (f *TextFormatter) appendNumeric(b *bytes.Buffer, out []byte) {
if f.ForceQuote {
var tmp [128]byte
b.Write(strconv.AppendQuote(tmp[:0], string(out)))
return
}
b.Write(out)
}
func needsQuoting(s string) bool {
for i := range len(s) {
c := s[i]
if !isSafeByte(c) {
return true
}
}
return false
}
func needsQuotingBytes(bs []byte) bool {
for _, c := range bs {
if !isSafeByte(c) {
return true
}
}
return false
}
func isSafeByte(ch byte) bool {
ok := ch < 0x80 && ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9'))
if ok {
return true
}
switch ch {
case '-', '.', '_', '/', '@', '^', '+':
return true
default:
return false
}
}