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