blob: be2c6efe5ed037364c352382713ffa913e1cf602 [file] [log] [blame]
kesavand2cde6582020-06-22 04:56:23 -04001package logrus
2
3import (
4 "bytes"
5 "fmt"
6 "os"
7 "runtime"
8 "sort"
kesavandc71914f2022-03-25 11:19:03 +05309 "strconv"
kesavand2cde6582020-06-22 04:56:23 -040010 "strings"
11 "sync"
12 "time"
kesavandc71914f2022-03-25 11:19:03 +053013 "unicode/utf8"
kesavand2cde6582020-06-22 04:56:23 -040014)
15
16const (
17 red = 31
18 yellow = 33
19 blue = 36
20 gray = 37
21)
22
23var baseTimestamp time.Time
24
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
kesavandc71914f2022-03-25 11:19:03 +053037 // 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
kesavand2cde6582020-06-22 04:56:23 -040045 // 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
kesavandc71914f2022-03-25 11:19:03 +053056 // 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.
kesavand2cde6582020-06-22 04:56:23 -040060 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
kesavandc71914f2022-03-25 11:19:03 +053073 // 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
kesavand2cde6582020-06-22 04:56:23 -040077 // 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
kesavandc71914f2022-03-25 11:19:03 +053099
100 // The max length of the level text, generated dynamically on init
101 levelTextMaxLength int
kesavand2cde6582020-06-22 04:56:23 -0400102}
103
104func (f *TextFormatter) init(entry *Entry) {
105 if entry.Logger != nil {
106 f.isTerminal = checkIfTerminal(entry.Logger.Out)
107 }
kesavandc71914f2022-03-25 11:19:03 +0530108 // 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 }
kesavand2cde6582020-06-22 04:56:23 -0400115}
116
117func (f *TextFormatter) isColored() bool {
118 isColored := f.ForceColors || (f.isTerminal && (runtime.GOOS != "windows"))
119
120 if f.EnvironmentOverrideColors {
kesavandc71914f2022-03-25 11:19:03 +0530121 switch force, ok := os.LookupEnv("CLICOLOR_FORCE"); {
122 case ok && force != "0":
kesavand2cde6582020-06-22 04:56:23 -0400123 isColored = true
kesavandc71914f2022-03-25 11:19:03 +0530124 case ok && force == "0", os.Getenv("CLICOLOR") == "0":
kesavand2cde6582020-06-22 04:56:23 -0400125 isColored = false
126 }
127 }
128
129 return isColored && !f.DisableColors
130}
131
132// Format renders a single log entry
133func (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
232func (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
kesavandc71914f2022-03-25 11:19:03 +0530241 case InfoLevel:
242 levelColor = blue
kesavand2cde6582020-06-22 04:56:23 -0400243 default:
244 levelColor = blue
245 }
246
247 levelText := strings.ToUpper(entry.Level.String())
kesavandc71914f2022-03-25 11:19:03 +0530248 if !f.DisableLevelTruncation && !f.PadLevelText {
kesavand2cde6582020-06-22 04:56:23 -0400249 levelText = levelText[0:4]
250 }
kesavandc71914f2022-03-25 11:19:03 +0530251 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 }
kesavand2cde6582020-06-22 04:56:23 -0400260
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
kesavandc71914f2022-03-25 11:19:03 +0530283 switch {
284 case f.DisableTimestamp:
kesavand2cde6582020-06-22 04:56:23 -0400285 fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m%s %-44s ", levelColor, levelText, caller, entry.Message)
kesavandc71914f2022-03-25 11:19:03 +0530286 case !f.FullTimestamp:
kesavand2cde6582020-06-22 04:56:23 -0400287 fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d]%s %-44s ", levelColor, levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), caller, entry.Message)
kesavandc71914f2022-03-25 11:19:03 +0530288 default:
kesavand2cde6582020-06-22 04:56:23 -0400289 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
298func (f *TextFormatter) needsQuoting(text string) bool {
kesavandc71914f2022-03-25 11:19:03 +0530299 if f.ForceQuote {
300 return true
301 }
kesavand2cde6582020-06-22 04:56:23 -0400302 if f.QuoteEmptyFields && len(text) == 0 {
303 return true
304 }
kesavandc71914f2022-03-25 11:19:03 +0530305 if f.DisableQuote {
306 return false
307 }
kesavand2cde6582020-06-22 04:56:23 -0400308 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
319func (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
328func (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}