text_formatter.go raw

   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