gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

text_formatter.go (9169B)


      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 }