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