package logrus import ( "bytes" "fmt" "os" "runtime" "sort" "strconv" "strings" "sync" "time" "unicode/utf8" ) const ( red = 31 yellow = 33 blue = 36 gray = 37 ) var baseTimestamp time.Time 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 // 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 { 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 } // Format renders a single log entry func (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 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 } } 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 := 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)) } }