123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339 |
- 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
- // 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 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
- 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))
- }
- }
|