blob: 3c28b54cabae823892a72cefe5c72db6ff631fe2 [file] [log] [blame]
khenaidooab1f7bd2019-11-14 14:00:27 -05001package logrus
2
3import (
4 "bytes"
5 "fmt"
6 "os"
khenaidood948f772021-08-11 17:49:24 -04007 "runtime"
khenaidooab1f7bd2019-11-14 14:00:27 -05008 "sort"
khenaidood948f772021-08-11 17:49:24 -04009 "strconv"
khenaidooab1f7bd2019-11-14 14:00:27 -050010 "strings"
11 "sync"
12 "time"
khenaidood948f772021-08-11 17:49:24 -040013 "unicode/utf8"
khenaidooab1f7bd2019-11-14 14:00:27 -050014)
15
16const (
khenaidood948f772021-08-11 17:49:24 -040017 red = 31
18 yellow = 33
19 blue = 36
20 gray = 37
khenaidooab1f7bd2019-11-14 14:00:27 -050021)
22
khenaidood948f772021-08-11 17:49:24 -040023var baseTimestamp time.Time
khenaidooab1f7bd2019-11-14 14:00:27 -050024
25func init() {
26 baseTimestamp = time.Now()
27}
28
29// TextFormatter formats logs into text
30type 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
khenaidood948f772021-08-11 17:49:24 -040037 // 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
khenaidooab1f7bd2019-11-14 14:00:27 -050045 // 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 TimestampFormat string
58
59 // The fields are sorted by default for a consistent output. For applications
60 // that log extremely frequently and don't use the JSON formatter this may not
61 // be desired.
62 DisableSorting bool
63
64 // The keys sorting function, when uninitialized it uses sort.Strings.
65 SortingFunc func([]string)
66
67 // Disables the truncation of the level text to 4 characters.
68 DisableLevelTruncation bool
69
khenaidood948f772021-08-11 17:49:24 -040070 // PadLevelText Adds padding the level text so that all the levels output at the same length
71 // PadLevelText is a superset of the DisableLevelTruncation option
72 PadLevelText bool
73
khenaidooab1f7bd2019-11-14 14:00:27 -050074 // QuoteEmptyFields will wrap empty fields in quotes if true
75 QuoteEmptyFields bool
76
77 // Whether the logger's out is to a terminal
78 isTerminal bool
79
80 // FieldMap allows users to customize the names of keys for default fields.
81 // As an example:
82 // formatter := &TextFormatter{
83 // FieldMap: FieldMap{
84 // FieldKeyTime: "@timestamp",
85 // FieldKeyLevel: "@level",
86 // FieldKeyMsg: "@message"}}
87 FieldMap FieldMap
88
khenaidood948f772021-08-11 17:49:24 -040089 // CallerPrettyfier can be set by the user to modify the content
90 // of the function and file keys in the data when ReportCaller is
91 // activated. If any of the returned value is the empty string the
92 // corresponding key will be removed from fields.
93 CallerPrettyfier func(*runtime.Frame) (function string, file string)
94
khenaidooab1f7bd2019-11-14 14:00:27 -050095 terminalInitOnce sync.Once
khenaidood948f772021-08-11 17:49:24 -040096
97 // The max length of the level text, generated dynamically on init
98 levelTextMaxLength int
khenaidooab1f7bd2019-11-14 14:00:27 -050099}
100
101func (f *TextFormatter) init(entry *Entry) {
102 if entry.Logger != nil {
103 f.isTerminal = checkIfTerminal(entry.Logger.Out)
khenaidood948f772021-08-11 17:49:24 -0400104 }
105 // Get the max length of the level text
106 for _, level := range AllLevels {
107 levelTextLength := utf8.RuneCount([]byte(level.String()))
108 if levelTextLength > f.levelTextMaxLength {
109 f.levelTextMaxLength = levelTextLength
khenaidooab1f7bd2019-11-14 14:00:27 -0500110 }
111 }
112}
113
114func (f *TextFormatter) isColored() bool {
khenaidood948f772021-08-11 17:49:24 -0400115 isColored := f.ForceColors || (f.isTerminal && (runtime.GOOS != "windows"))
khenaidooab1f7bd2019-11-14 14:00:27 -0500116
117 if f.EnvironmentOverrideColors {
khenaidood948f772021-08-11 17:49:24 -0400118 switch force, ok := os.LookupEnv("CLICOLOR_FORCE"); {
119 case ok && force != "0":
khenaidooab1f7bd2019-11-14 14:00:27 -0500120 isColored = true
khenaidood948f772021-08-11 17:49:24 -0400121 case ok && force == "0", os.Getenv("CLICOLOR") == "0":
khenaidooab1f7bd2019-11-14 14:00:27 -0500122 isColored = false
123 }
124 }
125
126 return isColored && !f.DisableColors
127}
128
129// Format renders a single log entry
130func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
khenaidood948f772021-08-11 17:49:24 -0400131 data := make(Fields)
132 for k, v := range entry.Data {
133 data[k] = v
134 }
135 prefixFieldClashes(data, f.FieldMap, entry.HasCaller())
136 keys := make([]string, 0, len(data))
137 for k := range data {
khenaidooab1f7bd2019-11-14 14:00:27 -0500138 keys = append(keys, k)
139 }
140
khenaidood948f772021-08-11 17:49:24 -0400141 var funcVal, fileVal string
142
143 fixedKeys := make([]string, 0, 4+len(data))
khenaidooab1f7bd2019-11-14 14:00:27 -0500144 if !f.DisableTimestamp {
145 fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyTime))
146 }
147 fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLevel))
148 if entry.Message != "" {
149 fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyMsg))
150 }
151 if entry.err != "" {
152 fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLogrusError))
153 }
154 if entry.HasCaller() {
khenaidood948f772021-08-11 17:49:24 -0400155 if f.CallerPrettyfier != nil {
156 funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
157 } else {
158 funcVal = entry.Caller.Function
159 fileVal = fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
160 }
161
162 if funcVal != "" {
163 fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyFunc))
164 }
165 if fileVal != "" {
166 fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyFile))
167 }
khenaidooab1f7bd2019-11-14 14:00:27 -0500168 }
169
170 if !f.DisableSorting {
171 if f.SortingFunc == nil {
172 sort.Strings(keys)
173 fixedKeys = append(fixedKeys, keys...)
174 } else {
175 if !f.isColored() {
176 fixedKeys = append(fixedKeys, keys...)
177 f.SortingFunc(fixedKeys)
178 } else {
179 f.SortingFunc(keys)
180 }
181 }
182 } else {
183 fixedKeys = append(fixedKeys, keys...)
184 }
185
186 var b *bytes.Buffer
187 if entry.Buffer != nil {
188 b = entry.Buffer
189 } else {
190 b = &bytes.Buffer{}
191 }
192
193 f.terminalInitOnce.Do(func() { f.init(entry) })
194
195 timestampFormat := f.TimestampFormat
196 if timestampFormat == "" {
197 timestampFormat = defaultTimestampFormat
198 }
199 if f.isColored() {
khenaidood948f772021-08-11 17:49:24 -0400200 f.printColored(b, entry, keys, data, timestampFormat)
khenaidooab1f7bd2019-11-14 14:00:27 -0500201 } else {
khenaidood948f772021-08-11 17:49:24 -0400202
khenaidooab1f7bd2019-11-14 14:00:27 -0500203 for _, key := range fixedKeys {
204 var value interface{}
205 switch {
206 case key == f.FieldMap.resolve(FieldKeyTime):
207 value = entry.Time.Format(timestampFormat)
208 case key == f.FieldMap.resolve(FieldKeyLevel):
209 value = entry.Level.String()
210 case key == f.FieldMap.resolve(FieldKeyMsg):
211 value = entry.Message
212 case key == f.FieldMap.resolve(FieldKeyLogrusError):
213 value = entry.err
214 case key == f.FieldMap.resolve(FieldKeyFunc) && entry.HasCaller():
khenaidood948f772021-08-11 17:49:24 -0400215 value = funcVal
khenaidooab1f7bd2019-11-14 14:00:27 -0500216 case key == f.FieldMap.resolve(FieldKeyFile) && entry.HasCaller():
khenaidood948f772021-08-11 17:49:24 -0400217 value = fileVal
khenaidooab1f7bd2019-11-14 14:00:27 -0500218 default:
khenaidood948f772021-08-11 17:49:24 -0400219 value = data[key]
khenaidooab1f7bd2019-11-14 14:00:27 -0500220 }
221 f.appendKeyValue(b, key, value)
222 }
223 }
224
225 b.WriteByte('\n')
226 return b.Bytes(), nil
227}
228
khenaidood948f772021-08-11 17:49:24 -0400229func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string, data Fields, timestampFormat string) {
khenaidooab1f7bd2019-11-14 14:00:27 -0500230 var levelColor int
231 switch entry.Level {
232 case DebugLevel, TraceLevel:
233 levelColor = gray
234 case WarnLevel:
235 levelColor = yellow
236 case ErrorLevel, FatalLevel, PanicLevel:
237 levelColor = red
238 default:
239 levelColor = blue
240 }
241
242 levelText := strings.ToUpper(entry.Level.String())
khenaidood948f772021-08-11 17:49:24 -0400243 if !f.DisableLevelTruncation && !f.PadLevelText {
khenaidooab1f7bd2019-11-14 14:00:27 -0500244 levelText = levelText[0:4]
245 }
khenaidood948f772021-08-11 17:49:24 -0400246 if f.PadLevelText {
247 // Generates the format string used in the next line, for example "%-6s" or "%-7s".
248 // Based on the max level text length.
249 formatString := "%-" + strconv.Itoa(f.levelTextMaxLength) + "s"
250 // Formats the level text by appending spaces up to the max length, for example:
251 // - "INFO "
252 // - "WARNING"
253 levelText = fmt.Sprintf(formatString, levelText)
254 }
khenaidooab1f7bd2019-11-14 14:00:27 -0500255
256 // Remove a single newline if it already exists in the message to keep
257 // the behavior of logrus text_formatter the same as the stdlib log package
258 entry.Message = strings.TrimSuffix(entry.Message, "\n")
259
260 caller := ""
khenaidooab1f7bd2019-11-14 14:00:27 -0500261 if entry.HasCaller() {
khenaidood948f772021-08-11 17:49:24 -0400262 funcVal := fmt.Sprintf("%s()", entry.Caller.Function)
263 fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
264
265 if f.CallerPrettyfier != nil {
266 funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
267 }
268
269 if fileVal == "" {
270 caller = funcVal
271 } else if funcVal == "" {
272 caller = fileVal
273 } else {
274 caller = fileVal + " " + funcVal
275 }
khenaidooab1f7bd2019-11-14 14:00:27 -0500276 }
277
khenaidood948f772021-08-11 17:49:24 -0400278 switch {
279 case f.DisableTimestamp:
khenaidooab1f7bd2019-11-14 14:00:27 -0500280 fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m%s %-44s ", levelColor, levelText, caller, entry.Message)
khenaidood948f772021-08-11 17:49:24 -0400281 case !f.FullTimestamp:
khenaidooab1f7bd2019-11-14 14:00:27 -0500282 fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d]%s %-44s ", levelColor, levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), caller, entry.Message)
khenaidood948f772021-08-11 17:49:24 -0400283 default:
khenaidooab1f7bd2019-11-14 14:00:27 -0500284 fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s]%s %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), caller, entry.Message)
285 }
286 for _, k := range keys {
khenaidood948f772021-08-11 17:49:24 -0400287 v := data[k]
khenaidooab1f7bd2019-11-14 14:00:27 -0500288 fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=", levelColor, k)
289 f.appendValue(b, v)
290 }
291}
292
293func (f *TextFormatter) needsQuoting(text string) bool {
khenaidood948f772021-08-11 17:49:24 -0400294 if f.ForceQuote {
295 return true
296 }
khenaidooab1f7bd2019-11-14 14:00:27 -0500297 if f.QuoteEmptyFields && len(text) == 0 {
298 return true
299 }
khenaidood948f772021-08-11 17:49:24 -0400300 if f.DisableQuote {
301 return false
302 }
khenaidooab1f7bd2019-11-14 14:00:27 -0500303 for _, ch := range text {
304 if !((ch >= 'a' && ch <= 'z') ||
305 (ch >= 'A' && ch <= 'Z') ||
306 (ch >= '0' && ch <= '9') ||
307 ch == '-' || ch == '.' || ch == '_' || ch == '/' || ch == '@' || ch == '^' || ch == '+') {
308 return true
309 }
310 }
311 return false
312}
313
314func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) {
315 if b.Len() > 0 {
316 b.WriteByte(' ')
317 }
318 b.WriteString(key)
319 b.WriteByte('=')
320 f.appendValue(b, value)
321}
322
323func (f *TextFormatter) appendValue(b *bytes.Buffer, value interface{}) {
324 stringVal, ok := value.(string)
325 if !ok {
326 stringVal = fmt.Sprint(value)
327 }
328
329 if !f.needsQuoting(stringVal) {
330 b.WriteString(stringVal)
331 } else {
332 b.WriteString(fmt.Sprintf("%q", stringVal))
333 }
334}