kesavand | 2cde658 | 2020-06-22 04:56:23 -0400 | [diff] [blame] | 1 | package logrus |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "fmt" |
| 6 | "os" |
| 7 | "runtime" |
| 8 | "sort" |
kesavand | c71914f | 2022-03-25 11:19:03 +0530 | [diff] [blame] | 9 | "strconv" |
kesavand | 2cde658 | 2020-06-22 04:56:23 -0400 | [diff] [blame] | 10 | "strings" |
| 11 | "sync" |
| 12 | "time" |
kesavand | c71914f | 2022-03-25 11:19:03 +0530 | [diff] [blame] | 13 | "unicode/utf8" |
kesavand | 2cde658 | 2020-06-22 04:56:23 -0400 | [diff] [blame] | 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 | |
kesavand | c71914f | 2022-03-25 11:19:03 +0530 | [diff] [blame] | 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 | |
kesavand | 2cde658 | 2020-06-22 04:56:23 -0400 | [diff] [blame] | 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 | |
kesavand | c71914f | 2022-03-25 11:19:03 +0530 | [diff] [blame] | 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. |
kesavand | 2cde658 | 2020-06-22 04:56:23 -0400 | [diff] [blame] | 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 | |
kesavand | c71914f | 2022-03-25 11:19:03 +0530 | [diff] [blame] | 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 | |
kesavand | 2cde658 | 2020-06-22 04:56:23 -0400 | [diff] [blame] | 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 |
kesavand | c71914f | 2022-03-25 11:19:03 +0530 | [diff] [blame] | 99 | |
| 100 | // The max length of the level text, generated dynamically on init |
| 101 | levelTextMaxLength int |
kesavand | 2cde658 | 2020-06-22 04:56:23 -0400 | [diff] [blame] | 102 | } |
| 103 | |
| 104 | func (f *TextFormatter) init(entry *Entry) { |
| 105 | if entry.Logger != nil { |
| 106 | f.isTerminal = checkIfTerminal(entry.Logger.Out) |
| 107 | } |
kesavand | c71914f | 2022-03-25 11:19:03 +0530 | [diff] [blame] | 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 | } |
kesavand | 2cde658 | 2020-06-22 04:56:23 -0400 | [diff] [blame] | 115 | } |
| 116 | |
| 117 | func (f *TextFormatter) isColored() bool { |
| 118 | isColored := f.ForceColors || (f.isTerminal && (runtime.GOOS != "windows")) |
| 119 | |
| 120 | if f.EnvironmentOverrideColors { |
kesavand | c71914f | 2022-03-25 11:19:03 +0530 | [diff] [blame] | 121 | switch force, ok := os.LookupEnv("CLICOLOR_FORCE"); { |
| 122 | case ok && force != "0": |
kesavand | 2cde658 | 2020-06-22 04:56:23 -0400 | [diff] [blame] | 123 | isColored = true |
kesavand | c71914f | 2022-03-25 11:19:03 +0530 | [diff] [blame] | 124 | case ok && force == "0", os.Getenv("CLICOLOR") == "0": |
kesavand | 2cde658 | 2020-06-22 04:56:23 -0400 | [diff] [blame] | 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 |
kesavand | c71914f | 2022-03-25 11:19:03 +0530 | [diff] [blame] | 241 | case InfoLevel: |
| 242 | levelColor = blue |
kesavand | 2cde658 | 2020-06-22 04:56:23 -0400 | [diff] [blame] | 243 | default: |
| 244 | levelColor = blue |
| 245 | } |
| 246 | |
| 247 | levelText := strings.ToUpper(entry.Level.String()) |
kesavand | c71914f | 2022-03-25 11:19:03 +0530 | [diff] [blame] | 248 | if !f.DisableLevelTruncation && !f.PadLevelText { |
kesavand | 2cde658 | 2020-06-22 04:56:23 -0400 | [diff] [blame] | 249 | levelText = levelText[0:4] |
| 250 | } |
kesavand | c71914f | 2022-03-25 11:19:03 +0530 | [diff] [blame] | 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 | } |
kesavand | 2cde658 | 2020-06-22 04:56:23 -0400 | [diff] [blame] | 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 | |
kesavand | c71914f | 2022-03-25 11:19:03 +0530 | [diff] [blame] | 283 | switch { |
| 284 | case f.DisableTimestamp: |
kesavand | 2cde658 | 2020-06-22 04:56:23 -0400 | [diff] [blame] | 285 | fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m%s %-44s ", levelColor, levelText, caller, entry.Message) |
kesavand | c71914f | 2022-03-25 11:19:03 +0530 | [diff] [blame] | 286 | case !f.FullTimestamp: |
kesavand | 2cde658 | 2020-06-22 04:56:23 -0400 | [diff] [blame] | 287 | fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d]%s %-44s ", levelColor, levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), caller, entry.Message) |
kesavand | c71914f | 2022-03-25 11:19:03 +0530 | [diff] [blame] | 288 | default: |
kesavand | 2cde658 | 2020-06-22 04:56:23 -0400 | [diff] [blame] | 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 { |
kesavand | c71914f | 2022-03-25 11:19:03 +0530 | [diff] [blame] | 299 | if f.ForceQuote { |
| 300 | return true |
| 301 | } |
kesavand | 2cde658 | 2020-06-22 04:56:23 -0400 | [diff] [blame] | 302 | if f.QuoteEmptyFields && len(text) == 0 { |
| 303 | return true |
| 304 | } |
kesavand | c71914f | 2022-03-25 11:19:03 +0530 | [diff] [blame] | 305 | if f.DisableQuote { |
| 306 | return false |
| 307 | } |
kesavand | 2cde658 | 2020-06-22 04:56:23 -0400 | [diff] [blame] | 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 | } |