blob: fb21649c9ae3d1a34bc58d6a0af0709277d0f50f [file] [log] [blame]
khenaidooac637102019-01-14 15:44:34 -05001package logrus
2
3import (
4 "bytes"
5 "fmt"
6 "os"
7 "runtime"
8 "sort"
9 "strings"
10 "sync"
11 "time"
12)
13
14const (
15 nocolor = 0
16 red = 31
17 green = 32
18 yellow = 33
19 blue = 36
20 gray = 37
21)
22
23var (
24 baseTimestamp time.Time
25 emptyFieldMap FieldMap
26)
27
28func init() {
29 baseTimestamp = time.Now()
30}
31
32// TextFormatter formats logs into text
33type TextFormatter struct {
34 // Set to true to bypass checking for a TTY before outputting colors.
35 ForceColors bool
36
37 // Force disabling colors.
38 DisableColors 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 // QuoteEmptyFields will wrap empty fields in quotes if true
66 QuoteEmptyFields bool
67
68 // Whether the logger's out is to a terminal
69 isTerminal bool
70
71 // FieldMap allows users to customize the names of keys for default fields.
72 // As an example:
73 // formatter := &TextFormatter{
74 // FieldMap: FieldMap{
75 // FieldKeyTime: "@timestamp",
76 // FieldKeyLevel: "@level",
77 // FieldKeyMsg: "@message"}}
78 FieldMap FieldMap
79
80 terminalInitOnce sync.Once
81}
82
83func (f *TextFormatter) init(entry *Entry) {
84 if entry.Logger != nil {
85 f.isTerminal = checkIfTerminal(entry.Logger.Out)
86
87 if f.isTerminal {
88 initTerminal(entry.Logger.Out)
89 }
90 }
91}
92
93func (f *TextFormatter) isColored() bool {
94 isColored := f.ForceColors || (f.isTerminal && (runtime.GOOS != "windows"))
95
96 if f.EnvironmentOverrideColors {
97 if force, ok := os.LookupEnv("CLICOLOR_FORCE"); ok && force != "0" {
98 isColored = true
99 } else if ok && force == "0" {
100 isColored = false
101 } else if os.Getenv("CLICOLOR") == "0" {
102 isColored = false
103 }
104 }
105
106 return isColored && !f.DisableColors
107}
108
109// Format renders a single log entry
110func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
111 data := make(Fields)
112 for k, v := range entry.Data {
113 data[k] = v
114 }
115 prefixFieldClashes(data, f.FieldMap, entry.HasCaller())
116 keys := make([]string, 0, len(data))
117 for k := range data {
118 keys = append(keys, k)
119 }
120
121 fixedKeys := make([]string, 0, 4+len(data))
122 if !f.DisableTimestamp {
123 fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyTime))
124 }
125 fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLevel))
126 if entry.Message != "" {
127 fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyMsg))
128 }
129 if entry.err != "" {
130 fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLogrusError))
131 }
132 if entry.HasCaller() {
133 fixedKeys = append(fixedKeys,
134 f.FieldMap.resolve(FieldKeyFunc), f.FieldMap.resolve(FieldKeyFile))
135 }
136
137 if !f.DisableSorting {
138 if f.SortingFunc == nil {
139 sort.Strings(keys)
140 fixedKeys = append(fixedKeys, keys...)
141 } else {
142 if !f.isColored() {
143 fixedKeys = append(fixedKeys, keys...)
144 f.SortingFunc(fixedKeys)
145 } else {
146 f.SortingFunc(keys)
147 }
148 }
149 } else {
150 fixedKeys = append(fixedKeys, keys...)
151 }
152
153 var b *bytes.Buffer
154 if entry.Buffer != nil {
155 b = entry.Buffer
156 } else {
157 b = &bytes.Buffer{}
158 }
159
160 f.terminalInitOnce.Do(func() { f.init(entry) })
161
162 timestampFormat := f.TimestampFormat
163 if timestampFormat == "" {
164 timestampFormat = defaultTimestampFormat
165 }
166 if f.isColored() {
167 f.printColored(b, entry, keys, data, timestampFormat)
168 } else {
169 for _, key := range fixedKeys {
170 var value interface{}
171 switch {
172 case key == f.FieldMap.resolve(FieldKeyTime):
173 value = entry.Time.Format(timestampFormat)
174 case key == f.FieldMap.resolve(FieldKeyLevel):
175 value = entry.Level.String()
176 case key == f.FieldMap.resolve(FieldKeyMsg):
177 value = entry.Message
178 case key == f.FieldMap.resolve(FieldKeyLogrusError):
179 value = entry.err
180 case key == f.FieldMap.resolve(FieldKeyFunc) && entry.HasCaller():
181 value = entry.Caller.Function
182 case key == f.FieldMap.resolve(FieldKeyFile) && entry.HasCaller():
183 value = fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
184 default:
185 value = data[key]
186 }
187 f.appendKeyValue(b, key, value)
188 }
189 }
190
191 b.WriteByte('\n')
192 return b.Bytes(), nil
193}
194
195func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string, data Fields, timestampFormat string) {
196 var levelColor int
197 switch entry.Level {
198 case DebugLevel, TraceLevel:
199 levelColor = gray
200 case WarnLevel:
201 levelColor = yellow
202 case ErrorLevel, FatalLevel, PanicLevel:
203 levelColor = red
204 default:
205 levelColor = blue
206 }
207
208 levelText := strings.ToUpper(entry.Level.String())
209 if !f.DisableLevelTruncation {
210 levelText = levelText[0:4]
211 }
212
213 // Remove a single newline if it already exists in the message to keep
214 // the behavior of logrus text_formatter the same as the stdlib log package
215 entry.Message = strings.TrimSuffix(entry.Message, "\n")
216
217 caller := ""
218
219 if entry.HasCaller() {
220 caller = fmt.Sprintf("%s:%d %s()",
221 entry.Caller.File, entry.Caller.Line, entry.Caller.Function)
222 }
223
224 if f.DisableTimestamp {
225 fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m%s %-44s ", levelColor, levelText, caller, entry.Message)
226 } else if !f.FullTimestamp {
227 fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d]%s %-44s ", levelColor, levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), caller, entry.Message)
228 } else {
229 fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s]%s %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), caller, entry.Message)
230 }
231 for _, k := range keys {
232 v := data[k]
233 fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=", levelColor, k)
234 f.appendValue(b, v)
235 }
236}
237
238func (f *TextFormatter) needsQuoting(text string) bool {
239 if f.QuoteEmptyFields && len(text) == 0 {
240 return true
241 }
242 for _, ch := range text {
243 if !((ch >= 'a' && ch <= 'z') ||
244 (ch >= 'A' && ch <= 'Z') ||
245 (ch >= '0' && ch <= '9') ||
246 ch == '-' || ch == '.' || ch == '_' || ch == '/' || ch == '@' || ch == '^' || ch == '+') {
247 return true
248 }
249 }
250 return false
251}
252
253func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) {
254 if b.Len() > 0 {
255 b.WriteByte(' ')
256 }
257 b.WriteString(key)
258 b.WriteByte('=')
259 f.appendValue(b, value)
260}
261
262func (f *TextFormatter) appendValue(b *bytes.Buffer, value interface{}) {
263 stringVal, ok := value.(string)
264 if !ok {
265 stringVal = fmt.Sprint(value)
266 }
267
268 if !f.needsQuoting(stringVal) {
269 b.WriteString(stringVal)
270 } else {
271 b.WriteString(fmt.Sprintf("%q", stringVal))
272 }
273}