| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339 | package logrusimport (	"bytes"	"fmt"	"os"	"runtime"	"sort"	"strconv"	"strings"	"sync"	"time"	"unicode/utf8")const (	red    = 31	yellow = 33	blue   = 36	gray   = 37)var baseTimestamp time.Timefunc init() {	baseTimestamp = time.Now()}// TextFormatter formats logs into texttype 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 sort.Strings.	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	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	// 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	// The max length of the level text, generated dynamically on init	levelTextMaxLength int}func (f *TextFormatter) init(entry *Entry) {	if entry.Logger != nil {		f.isTerminal = checkIfTerminal(entry.Logger.Out)	}	// Get the max length of the level text	for _, level := range AllLevels {		levelTextLength := utf8.RuneCount([]byte(level.String()))		if levelTextLength > f.levelTextMaxLength {			f.levelTextMaxLength = levelTextLength		}	}}func (f *TextFormatter) isColored() bool {	isColored := f.ForceColors || (f.isTerminal && (runtime.GOOS != "windows"))	if f.EnvironmentOverrideColors {		switch force, ok := os.LookupEnv("CLICOLOR_FORCE"); {		case ok && force != "0":			isColored = true		case ok && force == "0", os.Getenv("CLICOLOR") == "0":			isColored = false		}	}	return isColored && !f.DisableColors}// Format renders a single log entryfunc (f *TextFormatter) Format(entry *Entry) ([]byte, error) {	data := make(Fields)	for k, v := range entry.Data {		data[k] = v	}	prefixFieldClashes(data, f.FieldMap, entry.HasCaller())	keys := make([]string, 0, len(data))	for k := range data {		keys = append(keys, k)	}	var funcVal, fileVal string	fixedKeys := make([]string, 0, 4+len(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() {		if f.CallerPrettyfier != nil {			funcVal, fileVal = f.CallerPrettyfier(entry.Caller)		} else {			funcVal = entry.Caller.Function			fileVal = fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)		}		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 {			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, data, 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 = funcVal			case key == f.FieldMap.resolve(FieldKeyFile) && entry.HasCaller():				value = fileVal			default:				value = 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, data Fields, timestampFormat string) {	var levelColor int	switch entry.Level {	case DebugLevel, TraceLevel:		levelColor = gray	case WarnLevel:		levelColor = yellow	case ErrorLevel, FatalLevel, PanicLevel:		levelColor = red	case InfoLevel:		levelColor = blue	default:		levelColor = blue	}	levelText := strings.ToUpper(entry.Level.String())	if !f.DisableLevelTruncation && !f.PadLevelText {		levelText = levelText[0:4]	}	if f.PadLevelText {		// Generates the format string used in the next line, for example "%-6s" or "%-7s".		// Based on the max level text length.		formatString := "%-" + strconv.Itoa(f.levelTextMaxLength) + "s"		// Formats the level text by appending spaces up to the max length, for example:		// 	- "INFO   "		//	- "WARNING"		levelText = fmt.Sprintf(formatString, levelText)	}	// 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() {		funcVal := fmt.Sprintf("%s()", entry.Caller.Function)		fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)		if f.CallerPrettyfier != nil {			funcVal, fileVal = f.CallerPrettyfier(entry.Caller)		}		if fileVal == "" {			caller = funcVal		} else if funcVal == "" {			caller = fileVal		} else {			caller = fileVal + " " + funcVal		}	}	switch {	case f.DisableTimestamp:		fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m%s %-44s ", levelColor, levelText, caller, entry.Message)	case !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)	default:		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 := 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.ForceQuote {		return true	}	if f.QuoteEmptyFields && len(text) == 0 {		return true	}	if f.DisableQuote {		return false	}	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))	}}
 |