...

Source file src/github.com/sirupsen/logrus/text_formatter.go

Documentation: github.com/sirupsen/logrus

     1  package logrus
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"os"
     7  	"runtime"
     8  	"sort"
     9  	"strconv"
    10  	"strings"
    11  	"sync"
    12  	"time"
    13  	"unicode/utf8"
    14  )
    15  
    16  const (
    17  	red    = 31
    18  	yellow = 33
    19  	blue   = 36
    20  	gray   = 37
    21  )
    22  
    23  var baseTimestamp time.Time
    24  
    25  func init() {
    26  	baseTimestamp = time.Now()
    27  }
    28  
    29  // TextFormatter formats logs into text
    30  type TextFormatter struct {
    31  	// Set to true to bypass checking for a TTY before outputting colors.
    32  	ForceColors bool
    33  
    34  	// Force disabling colors.
    35  	DisableColors bool
    36  
    37  	// Force quoting of all values
    38  	ForceQuote bool
    39  
    40  	// DisableQuote disables quoting for all values.
    41  	// DisableQuote will have a lower priority than ForceQuote.
    42  	// If both of them are set to true, quote will be forced on all values.
    43  	DisableQuote bool
    44  
    45  	// Override coloring based on CLICOLOR and CLICOLOR_FORCE. - https://bixense.com/clicolors/
    46  	EnvironmentOverrideColors bool
    47  
    48  	// Disable timestamp logging. useful when output is redirected to logging
    49  	// system that already adds timestamps.
    50  	DisableTimestamp bool
    51  
    52  	// Enable logging the full timestamp when a TTY is attached instead of just
    53  	// the time passed since beginning of execution.
    54  	FullTimestamp bool
    55  
    56  	// TimestampFormat to use for display when a full timestamp is printed.
    57  	// The format to use is the same than for time.Format or time.Parse from the standard
    58  	// library.
    59  	// The standard Library already provides a set of predefined format.
    60  	TimestampFormat string
    61  
    62  	// The fields are sorted by default for a consistent output. For applications
    63  	// that log extremely frequently and don't use the JSON formatter this may not
    64  	// be desired.
    65  	DisableSorting bool
    66  
    67  	// The keys sorting function, when uninitialized it uses sort.Strings.
    68  	SortingFunc func([]string)
    69  
    70  	// Disables the truncation of the level text to 4 characters.
    71  	DisableLevelTruncation bool
    72  
    73  	// PadLevelText Adds padding the level text so that all the levels output at the same length
    74  	// PadLevelText is a superset of the DisableLevelTruncation option
    75  	PadLevelText bool
    76  
    77  	// QuoteEmptyFields will wrap empty fields in quotes if true
    78  	QuoteEmptyFields bool
    79  
    80  	// Whether the logger's out is to a terminal
    81  	isTerminal bool
    82  
    83  	// FieldMap allows users to customize the names of keys for default fields.
    84  	// As an example:
    85  	// formatter := &TextFormatter{
    86  	//     FieldMap: FieldMap{
    87  	//         FieldKeyTime:  "@timestamp",
    88  	//         FieldKeyLevel: "@level",
    89  	//         FieldKeyMsg:   "@message"}}
    90  	FieldMap FieldMap
    91  
    92  	// CallerPrettyfier can be set by the user to modify the content
    93  	// of the function and file keys in the data when ReportCaller is
    94  	// activated. If any of the returned value is the empty string the
    95  	// corresponding key will be removed from fields.
    96  	CallerPrettyfier func(*runtime.Frame) (function string, file string)
    97  
    98  	terminalInitOnce sync.Once
    99  
   100  	// The max length of the level text, generated dynamically on init
   101  	levelTextMaxLength int
   102  }
   103  
   104  func (f *TextFormatter) init(entry *Entry) {
   105  	if entry.Logger != nil {
   106  		f.isTerminal = checkIfTerminal(entry.Logger.Out)
   107  	}
   108  	// Get the max length of the level text
   109  	for _, level := range AllLevels {
   110  		levelTextLength := utf8.RuneCount([]byte(level.String()))
   111  		if levelTextLength > f.levelTextMaxLength {
   112  			f.levelTextMaxLength = levelTextLength
   113  		}
   114  	}
   115  }
   116  
   117  func (f *TextFormatter) isColored() bool {
   118  	isColored := f.ForceColors || (f.isTerminal && (runtime.GOOS != "windows"))
   119  
   120  	if f.EnvironmentOverrideColors {
   121  		switch force, ok := os.LookupEnv("CLICOLOR_FORCE"); {
   122  		case ok && force != "0":
   123  			isColored = true
   124  		case ok && force == "0", os.Getenv("CLICOLOR") == "0":
   125  			isColored = false
   126  		}
   127  	}
   128  
   129  	return isColored && !f.DisableColors
   130  }
   131  
   132  // Format renders a single log entry
   133  func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
   134  	data := make(Fields)
   135  	for k, v := range entry.Data {
   136  		data[k] = v
   137  	}
   138  	prefixFieldClashes(data, f.FieldMap, entry.HasCaller())
   139  	keys := make([]string, 0, len(data))
   140  	for k := range data {
   141  		keys = append(keys, k)
   142  	}
   143  
   144  	var funcVal, fileVal string
   145  
   146  	fixedKeys := make([]string, 0, 4+len(data))
   147  	if !f.DisableTimestamp {
   148  		fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyTime))
   149  	}
   150  	fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLevel))
   151  	if entry.Message != "" {
   152  		fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyMsg))
   153  	}
   154  	if entry.err != "" {
   155  		fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLogrusError))
   156  	}
   157  	if entry.HasCaller() {
   158  		if f.CallerPrettyfier != nil {
   159  			funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
   160  		} else {
   161  			funcVal = entry.Caller.Function
   162  			fileVal = fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
   163  		}
   164  
   165  		if funcVal != "" {
   166  			fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyFunc))
   167  		}
   168  		if fileVal != "" {
   169  			fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyFile))
   170  		}
   171  	}
   172  
   173  	if !f.DisableSorting {
   174  		if f.SortingFunc == nil {
   175  			sort.Strings(keys)
   176  			fixedKeys = append(fixedKeys, keys...)
   177  		} else {
   178  			if !f.isColored() {
   179  				fixedKeys = append(fixedKeys, keys...)
   180  				f.SortingFunc(fixedKeys)
   181  			} else {
   182  				f.SortingFunc(keys)
   183  			}
   184  		}
   185  	} else {
   186  		fixedKeys = append(fixedKeys, keys...)
   187  	}
   188  
   189  	var b *bytes.Buffer
   190  	if entry.Buffer != nil {
   191  		b = entry.Buffer
   192  	} else {
   193  		b = &bytes.Buffer{}
   194  	}
   195  
   196  	f.terminalInitOnce.Do(func() { f.init(entry) })
   197  
   198  	timestampFormat := f.TimestampFormat
   199  	if timestampFormat == "" {
   200  		timestampFormat = defaultTimestampFormat
   201  	}
   202  	if f.isColored() {
   203  		f.printColored(b, entry, keys, data, timestampFormat)
   204  	} else {
   205  
   206  		for _, key := range fixedKeys {
   207  			var value interface{}
   208  			switch {
   209  			case key == f.FieldMap.resolve(FieldKeyTime):
   210  				value = entry.Time.Format(timestampFormat)
   211  			case key == f.FieldMap.resolve(FieldKeyLevel):
   212  				value = entry.Level.String()
   213  			case key == f.FieldMap.resolve(FieldKeyMsg):
   214  				value = entry.Message
   215  			case key == f.FieldMap.resolve(FieldKeyLogrusError):
   216  				value = entry.err
   217  			case key == f.FieldMap.resolve(FieldKeyFunc) && entry.HasCaller():
   218  				value = funcVal
   219  			case key == f.FieldMap.resolve(FieldKeyFile) && entry.HasCaller():
   220  				value = fileVal
   221  			default:
   222  				value = data[key]
   223  			}
   224  			f.appendKeyValue(b, key, value)
   225  		}
   226  	}
   227  
   228  	b.WriteByte('\n')
   229  	return b.Bytes(), nil
   230  }
   231  
   232  func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string, data Fields, timestampFormat string) {
   233  	var levelColor int
   234  	switch entry.Level {
   235  	case DebugLevel, TraceLevel:
   236  		levelColor = gray
   237  	case WarnLevel:
   238  		levelColor = yellow
   239  	case ErrorLevel, FatalLevel, PanicLevel:
   240  		levelColor = red
   241  	case InfoLevel:
   242  		levelColor = blue
   243  	default:
   244  		levelColor = blue
   245  	}
   246  
   247  	levelText := strings.ToUpper(entry.Level.String())
   248  	if !f.DisableLevelTruncation && !f.PadLevelText {
   249  		levelText = levelText[0:4]
   250  	}
   251  	if f.PadLevelText {
   252  		// Generates the format string used in the next line, for example "%-6s" or "%-7s".
   253  		// Based on the max level text length.
   254  		formatString := "%-" + strconv.Itoa(f.levelTextMaxLength) + "s"
   255  		// Formats the level text by appending spaces up to the max length, for example:
   256  		// 	- "INFO   "
   257  		//	- "WARNING"
   258  		levelText = fmt.Sprintf(formatString, levelText)
   259  	}
   260  
   261  	// Remove a single newline if it already exists in the message to keep
   262  	// the behavior of logrus text_formatter the same as the stdlib log package
   263  	entry.Message = strings.TrimSuffix(entry.Message, "\n")
   264  
   265  	caller := ""
   266  	if entry.HasCaller() {
   267  		funcVal := fmt.Sprintf("%s()", entry.Caller.Function)
   268  		fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
   269  
   270  		if f.CallerPrettyfier != nil {
   271  			funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
   272  		}
   273  
   274  		if fileVal == "" {
   275  			caller = funcVal
   276  		} else if funcVal == "" {
   277  			caller = fileVal
   278  		} else {
   279  			caller = fileVal + " " + funcVal
   280  		}
   281  	}
   282  
   283  	switch {
   284  	case f.DisableTimestamp:
   285  		fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m%s %-44s ", levelColor, levelText, caller, entry.Message)
   286  	case !f.FullTimestamp:
   287  		fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d]%s %-44s ", levelColor, levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), caller, entry.Message)
   288  	default:
   289  		fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s]%s %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), caller, entry.Message)
   290  	}
   291  	for _, k := range keys {
   292  		v := data[k]
   293  		fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=", levelColor, k)
   294  		f.appendValue(b, v)
   295  	}
   296  }
   297  
   298  func (f *TextFormatter) needsQuoting(text string) bool {
   299  	if f.ForceQuote {
   300  		return true
   301  	}
   302  	if f.QuoteEmptyFields && len(text) == 0 {
   303  		return true
   304  	}
   305  	if f.DisableQuote {
   306  		return false
   307  	}
   308  	for _, ch := range text {
   309  		if !((ch >= 'a' && ch <= 'z') ||
   310  			(ch >= 'A' && ch <= 'Z') ||
   311  			(ch >= '0' && ch <= '9') ||
   312  			ch == '-' || ch == '.' || ch == '_' || ch == '/' || ch == '@' || ch == '^' || ch == '+') {
   313  			return true
   314  		}
   315  	}
   316  	return false
   317  }
   318  
   319  func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) {
   320  	if b.Len() > 0 {
   321  		b.WriteByte(' ')
   322  	}
   323  	b.WriteString(key)
   324  	b.WriteByte('=')
   325  	f.appendValue(b, value)
   326  }
   327  
   328  func (f *TextFormatter) appendValue(b *bytes.Buffer, value interface{}) {
   329  	stringVal, ok := value.(string)
   330  	if !ok {
   331  		stringVal = fmt.Sprint(value)
   332  	}
   333  
   334  	if !f.needsQuoting(stringVal) {
   335  		b.WriteString(stringVal)
   336  	} else {
   337  		b.WriteString(fmt.Sprintf("%q", stringVal))
   338  	}
   339  }
   340  

View as plain text