f76d643702
There are two different code paths for rendering a key/value pair. The non-color version uses a type switch that handles specific types such as "error", and the color version uses the %+v printf format specifier. This causes an inconsistency between the two formats. In particular, errors created using the github.com/pkg/errors package will include a stack trace of where the error was created when printed to the terminal, but not to a file. Printing the stack trace as part of the log field is probably not the right behavior. The output is also inconsistent between the two forms because strings are not quoted/escaped when colors are used. This can make log output unparseable. Fix this by making both code paths use the type switch and escaping rules. Fix the escaping code to pass the error value to Fprintf, not the error itself, which seems to be necessary to avoid blank output with errors created by github.com/pkg/errors.
169 lines
3.6 KiB
Go
169 lines
3.6 KiB
Go
package logrus
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
nocolor = 0
|
|
red = 31
|
|
green = 32
|
|
yellow = 33
|
|
blue = 34
|
|
gray = 37
|
|
)
|
|
|
|
var (
|
|
baseTimestamp time.Time
|
|
isTerminal bool
|
|
)
|
|
|
|
func init() {
|
|
baseTimestamp = time.Now()
|
|
isTerminal = IsTerminal()
|
|
}
|
|
|
|
func miniTS() int {
|
|
return int(time.Since(baseTimestamp) / time.Second)
|
|
}
|
|
|
|
type TextFormatter struct {
|
|
// Set to true to bypass checking for a TTY before outputting colors.
|
|
ForceColors bool
|
|
|
|
// Force disabling colors.
|
|
DisableColors 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
|
|
}
|
|
|
|
func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
|
|
var b *bytes.Buffer
|
|
var keys []string = make([]string, 0, len(entry.Data))
|
|
for k := range entry.Data {
|
|
keys = append(keys, k)
|
|
}
|
|
|
|
if !f.DisableSorting {
|
|
sort.Strings(keys)
|
|
}
|
|
if entry.Buffer != nil {
|
|
b = entry.Buffer
|
|
} else {
|
|
b = &bytes.Buffer{}
|
|
}
|
|
|
|
prefixFieldClashes(entry.Data)
|
|
|
|
isColorTerminal := isTerminal && (runtime.GOOS != "windows")
|
|
isColored := (f.ForceColors || isColorTerminal) && !f.DisableColors
|
|
|
|
timestampFormat := f.TimestampFormat
|
|
if timestampFormat == "" {
|
|
timestampFormat = DefaultTimestampFormat
|
|
}
|
|
if isColored {
|
|
f.printColored(b, entry, keys, timestampFormat)
|
|
} else {
|
|
if !f.DisableTimestamp {
|
|
f.appendKeyValue(b, "time", entry.Time.Format(timestampFormat))
|
|
}
|
|
f.appendKeyValue(b, "level", entry.Level.String())
|
|
if entry.Message != "" {
|
|
f.appendKeyValue(b, "msg", entry.Message)
|
|
}
|
|
for _, key := range keys {
|
|
f.appendKeyValue(b, key, entry.Data[key])
|
|
}
|
|
}
|
|
|
|
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:
|
|
levelColor = gray
|
|
case WarnLevel:
|
|
levelColor = yellow
|
|
case ErrorLevel, FatalLevel, PanicLevel:
|
|
levelColor = red
|
|
default:
|
|
levelColor = blue
|
|
}
|
|
|
|
levelText := strings.ToUpper(entry.Level.String())[0:4]
|
|
|
|
if !f.FullTimestamp {
|
|
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d] %-44s ", levelColor, levelText, miniTS(), entry.Message)
|
|
} else {
|
|
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s] %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), 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 needsQuoting(text string) bool {
|
|
for _, ch := range text {
|
|
if !((ch >= 'a' && ch <= 'z') ||
|
|
(ch >= 'A' && ch <= 'Z') ||
|
|
(ch >= '0' && ch <= '9') ||
|
|
ch == '-' || ch == '.') {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) {
|
|
|
|
b.WriteString(key)
|
|
b.WriteByte('=')
|
|
f.appendValue(b, value)
|
|
b.WriteByte(' ')
|
|
}
|
|
|
|
func (f *TextFormatter) appendValue(b *bytes.Buffer, value interface{}) {
|
|
switch value := value.(type) {
|
|
case string:
|
|
if !needsQuoting(value) {
|
|
b.WriteString(value)
|
|
} else {
|
|
fmt.Fprintf(b, "%q", value)
|
|
}
|
|
case error:
|
|
errmsg := value.Error()
|
|
if !needsQuoting(errmsg) {
|
|
b.WriteString(errmsg)
|
|
} else {
|
|
fmt.Fprintf(b, "%q", errmsg)
|
|
}
|
|
default:
|
|
fmt.Fprint(b, value)
|
|
}
|
|
}
|