| package logrus |
| |
| import ( |
| "bytes" |
| "fmt" |
| "os" |
| "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 |
| } |
| |
| // 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)) |
| } |
| } |