mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 15:46:26 -04:00
Bumps [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) from 1.9.4 to 1.10.2. - [Release notes](https://github.com/sirupsen/logrus/releases) - [Changelog](https://github.com/sirupsen/logrus/blob/master/CHANGELOG.md) - [Commits](https://github.com/sirupsen/logrus/compare/v1.9.4...v1.10.2) --- updated-dependencies: - dependency-name: github.com/sirupsen/logrus dependency-version: 1.10.2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com>
484 lines
13 KiB
Go
484 lines
13 KiB
Go
package logrus
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"maps"
|
|
"os"
|
|
"reflect"
|
|
"runtime"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
var baseTimestamp = time.Now()
|
|
|
|
// TextFormatter formats logs into text.
|
|
//
|
|
// Output is logfmt-like: key=value pairs separated by spaces. Fields from
|
|
// [Entry.Data] are included together with the standard fields derived from the
|
|
// entry. If a field conflicts with a standard field, it is prefixed with
|
|
// "fields.". Standard field names can be customized through FieldMap.
|
|
//
|
|
// Field keys are written as-is (unquoted and unescaped) in the plain
|
|
// (non-colored) format; only field values may be quoted depending on
|
|
// DisableQuote, ForceQuote, QuoteEmptyFields, and the value content.
|
|
//
|
|
// When colors are enabled, ANSI escape sequences may be added for presentation.
|
|
// For fully escaped structured output (including safe keys), use JSONFormatter.
|
|
type TextFormatter struct {
|
|
// Set to true to bypass checking for a TTY before outputting colors.
|
|
ForceColors bool
|
|
|
|
// Force disabling colors.
|
|
DisableColors bool
|
|
|
|
// Force quoting of all values
|
|
ForceQuote bool
|
|
|
|
// DisableQuote disables quoting for all values.
|
|
// DisableQuote will have a lower priority than ForceQuote.
|
|
// If both of them are set to true, quote will be forced on all values.
|
|
DisableQuote 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.
|
|
// The format to use is the same than for time.Format or time.Parse from the standard
|
|
// library.
|
|
// The standard Library already provides a set of predefined format.
|
|
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 slices.Sort.
|
|
SortingFunc func([]string)
|
|
|
|
// Disables the truncation of the level text to 4 characters.
|
|
DisableLevelTruncation bool
|
|
|
|
// PadLevelText Adds padding the level text so that all the levels output at the same length
|
|
// PadLevelText is a superset of the DisableLevelTruncation option
|
|
PadLevelText bool
|
|
|
|
// QuoteEmptyFields will wrap empty fields in quotes if true
|
|
QuoteEmptyFields bool
|
|
|
|
// Whether the logger's out is to a terminal. Don't use this field
|
|
// directly; use TextFormatter.isTerminal instead.
|
|
terminal bool
|
|
|
|
// FieldMap allows users to customize the names of keys for default fields.
|
|
// Mapped keys are written as-is, so they should be safe for plain-text output.
|
|
//
|
|
// As an example:
|
|
//
|
|
// formatter := &TextFormatter{
|
|
// FieldMap: FieldMap{
|
|
// FieldKeyTime: "@timestamp",
|
|
// FieldKeyLevel: "@level",
|
|
// FieldKeyMsg: "@message",
|
|
// },
|
|
// }
|
|
FieldMap FieldMap
|
|
|
|
// CallerPrettyfier can be set by the user to modify the content
|
|
// of the function and file keys in the data when ReportCaller is
|
|
// activated. If any of the returned value is the empty string the
|
|
// corresponding key will be removed from fields.
|
|
CallerPrettyfier func(*runtime.Frame) (function string, file string)
|
|
|
|
terminalInitOnce sync.Once
|
|
}
|
|
|
|
func (f *TextFormatter) isTerminal(entry *Entry) bool {
|
|
if entry == nil || entry.Logger == nil {
|
|
// Don't run the terminalInitOnce without a logger, otherwise we'd
|
|
// cache the default (false) forever even if a logger is attached
|
|
// later.
|
|
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
|
|
}
|
|
|
|
// Format renders a single log entry
|
|
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 {
|
|
// Default sorting does not sort the "fixed keys";
|
|
// see https://github.com/sirupsen/logrus/commit/73bc94e60c753099e8bae902f81fbd6e7dd95f26
|
|
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) {
|
|
// 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")
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
// Keys use the same color as the level-prefix.
|
|
for _, k := range keys {
|
|
b.WriteByte(' ')
|
|
b.WriteString(colorize(entry.Level, k))
|
|
b.WriteByte('=')
|
|
f.appendValue(b, data[k])
|
|
}
|
|
|
|
b.WriteByte('\n')
|
|
}
|
|
|
|
// appendKeyValue writes key=value. Keys are written verbatim (unquoted/unescaped);
|
|
// values are subject to quoting/escaping.
|
|
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) {
|
|
// Fast paths.
|
|
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.appendError(b, v)
|
|
return
|
|
case fmt.Stringer:
|
|
f.appendStringer(b, v)
|
|
return
|
|
}
|
|
|
|
// Handle common primitives.
|
|
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 (f *TextFormatter) appendError(b *bytes.Buffer, v error) {
|
|
defer f.recoverValue(b, v, "Error")
|
|
|
|
f.appendString(b, v.Error())
|
|
}
|
|
|
|
func (f *TextFormatter) appendStringer(b *bytes.Buffer, v fmt.Stringer) {
|
|
defer f.recoverValue(b, v, "String")
|
|
|
|
f.appendString(b, v.String())
|
|
}
|
|
|
|
func (f *TextFormatter) recoverValue(b *bytes.Buffer, v any, method string) {
|
|
if r := recover(); r != nil {
|
|
rv := reflect.ValueOf(v)
|
|
if rv.Kind() == reflect.Pointer && rv.IsNil() {
|
|
f.appendString(b, "<nil>")
|
|
} else {
|
|
f.appendString(b, fmt.Sprintf("%%!v(PANIC=%s method: %v)", method, r))
|
|
}
|
|
}
|
|
}
|
|
|
|
// needsQuoting returns true if the string contains any byte that
|
|
// requires quoting. It returns false when every byte is "safe" according
|
|
// to isSafeByte.
|
|
func needsQuoting(s string) bool {
|
|
// use an index loop (avoid rune decoding).
|
|
for i := range len(s) {
|
|
c := s[i]
|
|
if !isSafeByte(c) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// needsQuotingBytes returns true if the byte slice contains any byte that
|
|
// requires quoting. It returns false when every byte is "safe" according
|
|
// to isSafeByte.
|
|
func needsQuotingBytes(bs []byte) bool {
|
|
for _, c := range bs {
|
|
if !isSafeByte(c) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// isSafeByte returns true if the byte is allowed unquoted (ASCII and in the allowlist).
|
|
// It purposely uses byte arithmetic (no runes) for performance.
|
|
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
|
|
}
|
|
}
|