blob: 2d15a239fefcb2bd164df8417175949a958dcdbb [file] [log] [blame]
Scott Baker105df152020-04-13 15:55:14 -07001package logrus
2
3import (
4 "bytes"
5 "fmt"
6 "os"
7 "runtime"
8 "sort"
9 "strconv"
10 "strings"
11 "sync"
12 "time"
13 "unicode/utf8"
14)
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
37 // Force quoting of all values
38 ForceQuote bool
39
40 // Override coloring based on CLICOLOR and CLICOLOR_FORCE. - https://bixense.com/clicolors/
41 EnvironmentOverrideColors bool
42
43 // Disable timestamp logging. useful when output is redirected to logging
44 // system that already adds timestamps.
45 DisableTimestamp bool
46
47 // Enable logging the full timestamp when a TTY is attached instead of just
48 // the time passed since beginning of execution.
49 FullTimestamp bool
50
51 // TimestampFormat to use for display when a full timestamp is printed
52 TimestampFormat string
53
54 // The fields are sorted by default for a consistent output. For applications
55 // that log extremely frequently and don't use the JSON formatter this may not
56 // be desired.
57 DisableSorting bool
58
59 // The keys sorting function, when uninitialized it uses sort.Strings.
60 SortingFunc func([]string)
61
62 // Disables the truncation of the level text to 4 characters.
63 DisableLevelTruncation bool
64
65 // PadLevelText Adds padding the level text so that all the levels output at the same length
66 // PadLevelText is a superset of the DisableLevelTruncation option
67 PadLevelText bool
68
69 // QuoteEmptyFields will wrap empty fields in quotes if true
70 QuoteEmptyFields bool
71
72 // Whether the logger's out is to a terminal
73 isTerminal bool
74
75 // FieldMap allows users to customize the names of keys for default fields.
76 // As an example:
77 // formatter := &TextFormatter{
78 // FieldMap: FieldMap{
79 // FieldKeyTime: "@timestamp",
80 // FieldKeyLevel: "@level",
81 // FieldKeyMsg: "@message"}}
82 FieldMap FieldMap
83
84 // CallerPrettyfier can be set by the user to modify the content
85 // of the function and file keys in the data when ReportCaller is
86 // activated. If any of the returned value is the empty string the
87 // corresponding key will be removed from fields.
88 CallerPrettyfier func(*runtime.Frame) (function string, file string)
89
90 terminalInitOnce sync.Once
91
92 // The max length of the level text, generated dynamically on init
93 levelTextMaxLength int
94}
95
96func (f *TextFormatter) init(entry *Entry) {
97 if entry.Logger != nil {
98 f.isTerminal = checkIfTerminal(entry.Logger.Out)
99 }
100 // Get the max length of the level text
101 for _, level := range AllLevels {
102 levelTextLength := utf8.RuneCount([]byte(level.String()))
103 if levelTextLength > f.levelTextMaxLength {
104 f.levelTextMaxLength = levelTextLength
105 }
106 }
107}
108
109func (f *TextFormatter) isColored() bool {
110 isColored := f.ForceColors || (f.isTerminal && (runtime.GOOS != "windows"))
111
112 if f.EnvironmentOverrideColors {
113 switch force, ok := os.LookupEnv("CLICOLOR_FORCE"); {
114 case ok && force != "0":
115 isColored = true
116 case ok && force == "0", os.Getenv("CLICOLOR") == "0":
117 isColored = false
118 }
119 }
120
121 return isColored && !f.DisableColors
122}
123
124// Format renders a single log entry
125func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
126 data := make(Fields)
127 for k, v := range entry.Data {
128 data[k] = v
129 }
130 prefixFieldClashes(data, f.FieldMap, entry.HasCaller())
131 keys := make([]string, 0, len(data))
132 for k := range data {
133 keys = append(keys, k)
134 }
135
136 var funcVal, fileVal string
137
138 fixedKeys := make([]string, 0, 4+len(data))
139 if !f.DisableTimestamp {
140 fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyTime))
141 }
142 fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLevel))
143 if entry.Message != "" {
144 fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyMsg))
145 }
146 if entry.err != "" {
147 fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLogrusError))
148 }
149 if entry.HasCaller() {
150 if f.CallerPrettyfier != nil {
151 funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
152 } else {
153 funcVal = entry.Caller.Function
154 fileVal = fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
155 }
156
157 if funcVal != "" {
158 fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyFunc))
159 }
160 if fileVal != "" {
161 fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyFile))
162 }
163 }
164
165 if !f.DisableSorting {
166 if f.SortingFunc == nil {
167 sort.Strings(keys)
168 fixedKeys = append(fixedKeys, keys...)
169 } else {
170 if !f.isColored() {
171 fixedKeys = append(fixedKeys, keys...)
172 f.SortingFunc(fixedKeys)
173 } else {
174 f.SortingFunc(keys)
175 }
176 }
177 } else {
178 fixedKeys = append(fixedKeys, keys...)
179 }
180
181 var b *bytes.Buffer
182 if entry.Buffer != nil {
183 b = entry.Buffer
184 } else {
185 b = &bytes.Buffer{}
186 }
187
188 f.terminalInitOnce.Do(func() { f.init(entry) })
189
190 timestampFormat := f.TimestampFormat
191 if timestampFormat == "" {
192 timestampFormat = defaultTimestampFormat
193 }
194 if f.isColored() {
195 f.printColored(b, entry, keys, data, timestampFormat)
196 } else {
197
198 for _, key := range fixedKeys {
199 var value interface{}
200 switch {
201 case key == f.FieldMap.resolve(FieldKeyTime):
202 value = entry.Time.Format(timestampFormat)
203 case key == f.FieldMap.resolve(FieldKeyLevel):
204 value = entry.Level.String()
205 case key == f.FieldMap.resolve(FieldKeyMsg):
206 value = entry.Message
207 case key == f.FieldMap.resolve(FieldKeyLogrusError):
208 value = entry.err
209 case key == f.FieldMap.resolve(FieldKeyFunc) && entry.HasCaller():
210 value = funcVal
211 case key == f.FieldMap.resolve(FieldKeyFile) && entry.HasCaller():
212 value = fileVal
213 default:
214 value = data[key]
215 }
216 f.appendKeyValue(b, key, value)
217 }
218 }
219
220 b.WriteByte('\n')
221 return b.Bytes(), nil
222}
223
224func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string, data Fields, timestampFormat string) {
225 var levelColor int
226 switch entry.Level {
227 case DebugLevel, TraceLevel:
228 levelColor = gray
229 case WarnLevel:
230 levelColor = yellow
231 case ErrorLevel, FatalLevel, PanicLevel:
232 levelColor = red
233 default:
234 levelColor = blue
235 }
236
237 levelText := strings.ToUpper(entry.Level.String())
238 if !f.DisableLevelTruncation && !f.PadLevelText {
239 levelText = levelText[0:4]
240 }
241 if f.PadLevelText {
242 // Generates the format string used in the next line, for example "%-6s" or "%-7s".
243 // Based on the max level text length.
244 formatString := "%-" + strconv.Itoa(f.levelTextMaxLength) + "s"
245 // Formats the level text by appending spaces up to the max length, for example:
246 // - "INFO "
247 // - "WARNING"
248 levelText = fmt.Sprintf(formatString, levelText)
249 }
250
251 // Remove a single newline if it already exists in the message to keep
252 // the behavior of logrus text_formatter the same as the stdlib log package
253 entry.Message = strings.TrimSuffix(entry.Message, "\n")
254
255 caller := ""
256 if entry.HasCaller() {
257 funcVal := fmt.Sprintf("%s()", entry.Caller.Function)
258 fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
259
260 if f.CallerPrettyfier != nil {
261 funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
262 }
263
264 if fileVal == "" {
265 caller = funcVal
266 } else if funcVal == "" {
267 caller = fileVal
268 } else {
269 caller = fileVal + " " + funcVal
270 }
271 }
272
273 switch {
274 case f.DisableTimestamp:
275 fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m%s %-44s ", levelColor, levelText, caller, entry.Message)
276 case !f.FullTimestamp:
277 fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d]%s %-44s ", levelColor, levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), caller, entry.Message)
278 default:
279 fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s]%s %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), caller, entry.Message)
280 }
281 for _, k := range keys {
282 v := data[k]
283 fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=", levelColor, k)
284 f.appendValue(b, v)
285 }
286}
287
288func (f *TextFormatter) needsQuoting(text string) bool {
289 if f.ForceQuote {
290 return true
291 }
292 if f.QuoteEmptyFields && len(text) == 0 {
293 return true
294 }
295 for _, ch := range text {
296 if !((ch >= 'a' && ch <= 'z') ||
297 (ch >= 'A' && ch <= 'Z') ||
298 (ch >= '0' && ch <= '9') ||
299 ch == '-' || ch == '.' || ch == '_' || ch == '/' || ch == '@' || ch == '^' || ch == '+') {
300 return true
301 }
302 }
303 return false
304}
305
306func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) {
307 if b.Len() > 0 {
308 b.WriteByte(' ')
309 }
310 b.WriteString(key)
311 b.WriteByte('=')
312 f.appendValue(b, value)
313}
314
315func (f *TextFormatter) appendValue(b *bytes.Buffer, value interface{}) {
316 stringVal, ok := value.(string)
317 if !ok {
318 stringVal = fmt.Sprint(value)
319 }
320
321 if !f.needsQuoting(stringVal) {
322 b.WriteString(stringVal)
323 } else {
324 b.WriteString(fmt.Sprintf("%q", stringVal))
325 }
326}