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 }