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
30 type TextFormatter struct {
31
32 ForceColors bool
33
34
35 DisableColors bool
36
37
38 ForceQuote bool
39
40
41
42
43 DisableQuote bool
44
45
46 EnvironmentOverrideColors bool
47
48
49
50 DisableTimestamp bool
51
52
53
54 FullTimestamp bool
55
56
57
58
59
60 TimestampFormat string
61
62
63
64
65 DisableSorting bool
66
67
68 SortingFunc func([]string)
69
70
71 DisableLevelTruncation bool
72
73
74
75 PadLevelText bool
76
77
78 QuoteEmptyFields bool
79
80
81 isTerminal bool
82
83
84
85
86
87
88
89
90 FieldMap FieldMap
91
92
93
94
95
96 CallerPrettyfier func(*runtime.Frame) (function string, file string)
97
98 terminalInitOnce sync.Once
99
100
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
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
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
253
254 formatString := "%-" + strconv.Itoa(f.levelTextMaxLength) + "s"
255
256
257
258 levelText = fmt.Sprintf(formatString, levelText)
259 }
260
261
262
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 }
340
View as plain text