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