e9026580bf
Currently the textformatter on windows outputs ``←[31mERRO←[0m[0000] test windows`` when coloring is not disabled explicitly. However, windows up to windows 8.1 does not support colored output on cmd entirely. Windows 10 added support for it, which is off by default and has to be enabled via registry or environment variable. Therefore i suggest removing colored output on windows entirely to make the output usable again.
271 lines
6.8 KiB
Go
271 lines
6.8 KiB
Go
package logrus
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
nocolor = 0
|
|
red = 31
|
|
green = 32
|
|
yellow = 33
|
|
blue = 36
|
|
gray = 37
|
|
)
|
|
|
|
var (
|
|
baseTimestamp time.Time
|
|
emptyFieldMap FieldMap
|
|
)
|
|
|
|
func init() {
|
|
baseTimestamp = time.Now()
|
|
}
|
|
|
|
// TextFormatter formats logs into text
|
|
type TextFormatter struct {
|
|
// Set to true to bypass checking for a TTY before outputting colors.
|
|
ForceColors bool
|
|
|
|
// Force disabling colors.
|
|
DisableColors bool
|
|
|
|
// Override coloring based on CLICOLOR and CLICOLOR_FORCE. - https://bixense.com/clicolors/
|
|
EnvironmentOverrideColors bool
|
|
|
|
// Disable timestamp logging. useful when output is redirected to logging
|
|
// system that already adds timestamps.
|
|
DisableTimestamp bool
|
|
|
|
// Enable logging the full timestamp when a TTY is attached instead of just
|
|
// the time passed since beginning of execution.
|
|
FullTimestamp bool
|
|
|
|
// TimestampFormat to use for display when a full timestamp is printed
|
|
TimestampFormat string
|
|
|
|
// The fields are sorted by default for a consistent output. For applications
|
|
// that log extremely frequently and don't use the JSON formatter this may not
|
|
// be desired.
|
|
DisableSorting bool
|
|
|
|
// The keys sorting function, when uninitialized it uses sort.Strings.
|
|
SortingFunc func([]string)
|
|
|
|
// Disables the truncation of the level text to 4 characters.
|
|
DisableLevelTruncation bool
|
|
|
|
// QuoteEmptyFields will wrap empty fields in quotes if true
|
|
QuoteEmptyFields bool
|
|
|
|
// Whether the logger's out is to a terminal
|
|
isTerminal bool
|
|
|
|
// FieldMap allows users to customize the names of keys for default fields.
|
|
// As an example:
|
|
// formatter := &TextFormatter{
|
|
// FieldMap: FieldMap{
|
|
// FieldKeyTime: "@timestamp",
|
|
// FieldKeyLevel: "@level",
|
|
// FieldKeyMsg: "@message"}}
|
|
FieldMap FieldMap
|
|
|
|
terminalInitOnce sync.Once
|
|
}
|
|
|
|
func (f *TextFormatter) init(entry *Entry) {
|
|
if entry.Logger != nil {
|
|
f.isTerminal = checkIfTerminal(entry.Logger.Out)
|
|
|
|
if f.isTerminal {
|
|
initTerminal(entry.Logger.Out)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (f *TextFormatter) isColored() bool {
|
|
isColored := f.ForceColors || f.isTerminal
|
|
|
|
if f.EnvironmentOverrideColors {
|
|
if force, ok := os.LookupEnv("CLICOLOR_FORCE"); ok && force != "0" {
|
|
isColored = true
|
|
} else if ok && force == "0" {
|
|
isColored = false
|
|
} else if os.Getenv("CLICOLOR") == "0" {
|
|
isColored = false
|
|
}
|
|
}
|
|
|
|
return isColored && !f.DisableColors && (runtime.GOOS != "windows")
|
|
}
|
|
|
|
// Format renders a single log entry
|
|
func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
|
|
prefixFieldClashes(entry.Data, f.FieldMap, entry.HasCaller())
|
|
|
|
keys := make([]string, 0, len(entry.Data))
|
|
for k := range entry.Data {
|
|
keys = append(keys, k)
|
|
}
|
|
|
|
fixedKeys := make([]string, 0, 4+len(entry.Data))
|
|
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))
|
|
}
|
|
if entry.HasCaller() {
|
|
fixedKeys = append(fixedKeys,
|
|
f.FieldMap.resolve(FieldKeyFunc), f.FieldMap.resolve(FieldKeyFile))
|
|
}
|
|
|
|
if !f.DisableSorting {
|
|
if f.SortingFunc == nil {
|
|
sort.Strings(keys)
|
|
fixedKeys = append(fixedKeys, keys...)
|
|
} else {
|
|
if !f.isColored() {
|
|
fixedKeys = append(fixedKeys, keys...)
|
|
f.SortingFunc(fixedKeys)
|
|
} else {
|
|
f.SortingFunc(keys)
|
|
}
|
|
}
|
|
} else {
|
|
fixedKeys = append(fixedKeys, keys...)
|
|
}
|
|
|
|
var b *bytes.Buffer
|
|
if entry.Buffer != nil {
|
|
b = entry.Buffer
|
|
} else {
|
|
b = &bytes.Buffer{}
|
|
}
|
|
|
|
f.terminalInitOnce.Do(func() { f.init(entry) })
|
|
|
|
timestampFormat := f.TimestampFormat
|
|
if timestampFormat == "" {
|
|
timestampFormat = defaultTimestampFormat
|
|
}
|
|
if f.isColored() {
|
|
f.printColored(b, entry, keys, timestampFormat)
|
|
} else {
|
|
for _, key := range fixedKeys {
|
|
var value interface{}
|
|
switch {
|
|
case key == f.FieldMap.resolve(FieldKeyTime):
|
|
value = entry.Time.Format(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) && entry.HasCaller():
|
|
value = entry.Caller.Function
|
|
case key == f.FieldMap.resolve(FieldKeyFile) && entry.HasCaller():
|
|
value = fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
|
|
default:
|
|
value = entry.Data[key]
|
|
}
|
|
f.appendKeyValue(b, key, value)
|
|
}
|
|
}
|
|
|
|
b.WriteByte('\n')
|
|
return b.Bytes(), nil
|
|
}
|
|
|
|
func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string, timestampFormat string) {
|
|
var levelColor int
|
|
switch entry.Level {
|
|
case DebugLevel, TraceLevel:
|
|
levelColor = gray
|
|
case WarnLevel:
|
|
levelColor = yellow
|
|
case ErrorLevel, FatalLevel, PanicLevel:
|
|
levelColor = red
|
|
default:
|
|
levelColor = blue
|
|
}
|
|
|
|
levelText := strings.ToUpper(entry.Level.String())
|
|
if !f.DisableLevelTruncation {
|
|
levelText = levelText[0:4]
|
|
}
|
|
|
|
// Remove a single newline if it already exists in the message to keep
|
|
// the behavior of logrus text_formatter the same as the stdlib log package
|
|
entry.Message = strings.TrimSuffix(entry.Message, "\n")
|
|
|
|
caller := ""
|
|
|
|
if entry.HasCaller() {
|
|
caller = fmt.Sprintf("%s:%d %s()",
|
|
entry.Caller.File, entry.Caller.Line, entry.Caller.Function)
|
|
}
|
|
|
|
if f.DisableTimestamp {
|
|
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m%s %-44s ", levelColor, levelText, caller, entry.Message)
|
|
} else if !f.FullTimestamp {
|
|
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d]%s %-44s ", levelColor, levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), caller, entry.Message)
|
|
} else {
|
|
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s]%s %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), caller, entry.Message)
|
|
}
|
|
for _, k := range keys {
|
|
v := entry.Data[k]
|
|
fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=", levelColor, k)
|
|
f.appendValue(b, v)
|
|
}
|
|
}
|
|
|
|
func (f *TextFormatter) needsQuoting(text string) bool {
|
|
if f.QuoteEmptyFields && len(text) == 0 {
|
|
return true
|
|
}
|
|
for _, ch := range text {
|
|
if !((ch >= 'a' && ch <= 'z') ||
|
|
(ch >= 'A' && ch <= 'Z') ||
|
|
(ch >= '0' && ch <= '9') ||
|
|
ch == '-' || ch == '.' || ch == '_' || ch == '/' || ch == '@' || ch == '^' || ch == '+') {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) {
|
|
if b.Len() > 0 {
|
|
b.WriteByte(' ')
|
|
}
|
|
b.WriteString(key)
|
|
b.WriteByte('=')
|
|
f.appendValue(b, value)
|
|
}
|
|
|
|
func (f *TextFormatter) appendValue(b *bytes.Buffer, value interface{}) {
|
|
stringVal, ok := value.(string)
|
|
if !ok {
|
|
stringVal = fmt.Sprint(value)
|
|
}
|
|
|
|
if !f.needsQuoting(stringVal) {
|
|
b.WriteString(stringVal)
|
|
} else {
|
|
b.WriteString(fmt.Sprintf("%q", stringVal))
|
|
}
|
|
}
|