66
|
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 }
|