khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 1 | package logrus |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "fmt" |
| 6 | "os" |
khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 7 | "runtime" |
khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 8 | "sort" |
khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 9 | "strconv" |
khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 10 | "strings" |
| 11 | "sync" |
| 12 | "time" |
khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 13 | "unicode/utf8" |
khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 14 | ) |
| 15 | |
| 16 | const ( |
khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 17 | red = 31 |
| 18 | yellow = 33 |
| 19 | blue = 36 |
| 20 | gray = 37 |
khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 21 | ) |
| 22 | |
khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 23 | var baseTimestamp time.Time |
khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 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 | |
khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [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 | |
khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [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 | |
| 56 | // TimestampFormat to use for display when a full timestamp is printed |
| 57 | TimestampFormat string |
| 58 | |
| 59 | // The fields are sorted by default for a consistent output. For applications |
| 60 | // that log extremely frequently and don't use the JSON formatter this may not |
| 61 | // be desired. |
| 62 | DisableSorting bool |
| 63 | |
| 64 | // The keys sorting function, when uninitialized it uses sort.Strings. |
| 65 | SortingFunc func([]string) |
| 66 | |
| 67 | // Disables the truncation of the level text to 4 characters. |
| 68 | DisableLevelTruncation bool |
| 69 | |
khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 70 | // PadLevelText Adds padding the level text so that all the levels output at the same length |
| 71 | // PadLevelText is a superset of the DisableLevelTruncation option |
| 72 | PadLevelText bool |
| 73 | |
khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 74 | // QuoteEmptyFields will wrap empty fields in quotes if true |
| 75 | QuoteEmptyFields bool |
| 76 | |
| 77 | // Whether the logger's out is to a terminal |
| 78 | isTerminal bool |
| 79 | |
| 80 | // FieldMap allows users to customize the names of keys for default fields. |
| 81 | // As an example: |
| 82 | // formatter := &TextFormatter{ |
| 83 | // FieldMap: FieldMap{ |
| 84 | // FieldKeyTime: "@timestamp", |
| 85 | // FieldKeyLevel: "@level", |
| 86 | // FieldKeyMsg: "@message"}} |
| 87 | FieldMap FieldMap |
| 88 | |
khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 89 | // CallerPrettyfier can be set by the user to modify the content |
| 90 | // of the function and file keys in the data when ReportCaller is |
| 91 | // activated. If any of the returned value is the empty string the |
| 92 | // corresponding key will be removed from fields. |
| 93 | CallerPrettyfier func(*runtime.Frame) (function string, file string) |
| 94 | |
khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 95 | terminalInitOnce sync.Once |
khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 96 | |
| 97 | // The max length of the level text, generated dynamically on init |
| 98 | levelTextMaxLength int |
khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 99 | } |
| 100 | |
| 101 | func (f *TextFormatter) init(entry *Entry) { |
| 102 | if entry.Logger != nil { |
| 103 | f.isTerminal = checkIfTerminal(entry.Logger.Out) |
khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 104 | } |
| 105 | // Get the max length of the level text |
| 106 | for _, level := range AllLevels { |
| 107 | levelTextLength := utf8.RuneCount([]byte(level.String())) |
| 108 | if levelTextLength > f.levelTextMaxLength { |
| 109 | f.levelTextMaxLength = levelTextLength |
khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 110 | } |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | func (f *TextFormatter) isColored() bool { |
khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 115 | isColored := f.ForceColors || (f.isTerminal && (runtime.GOOS != "windows")) |
khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 116 | |
| 117 | if f.EnvironmentOverrideColors { |
khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 118 | switch force, ok := os.LookupEnv("CLICOLOR_FORCE"); { |
| 119 | case ok && force != "0": |
khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 120 | isColored = true |
khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 121 | case ok && force == "0", os.Getenv("CLICOLOR") == "0": |
khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 122 | isColored = false |
| 123 | } |
| 124 | } |
| 125 | |
| 126 | return isColored && !f.DisableColors |
| 127 | } |
| 128 | |
| 129 | // Format renders a single log entry |
| 130 | func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { |
khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 131 | data := make(Fields) |
| 132 | for k, v := range entry.Data { |
| 133 | data[k] = v |
| 134 | } |
| 135 | prefixFieldClashes(data, f.FieldMap, entry.HasCaller()) |
| 136 | keys := make([]string, 0, len(data)) |
| 137 | for k := range data { |
khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 138 | keys = append(keys, k) |
| 139 | } |
| 140 | |
khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 141 | var funcVal, fileVal string |
| 142 | |
| 143 | fixedKeys := make([]string, 0, 4+len(data)) |
khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 144 | if !f.DisableTimestamp { |
| 145 | fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyTime)) |
| 146 | } |
| 147 | fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLevel)) |
| 148 | if entry.Message != "" { |
| 149 | fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyMsg)) |
| 150 | } |
| 151 | if entry.err != "" { |
| 152 | fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLogrusError)) |
| 153 | } |
| 154 | if entry.HasCaller() { |
khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 155 | if f.CallerPrettyfier != nil { |
| 156 | funcVal, fileVal = f.CallerPrettyfier(entry.Caller) |
| 157 | } else { |
| 158 | funcVal = entry.Caller.Function |
| 159 | fileVal = fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line) |
| 160 | } |
| 161 | |
| 162 | if funcVal != "" { |
| 163 | fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyFunc)) |
| 164 | } |
| 165 | if fileVal != "" { |
| 166 | fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyFile)) |
| 167 | } |
khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 168 | } |
| 169 | |
| 170 | if !f.DisableSorting { |
| 171 | if f.SortingFunc == nil { |
| 172 | sort.Strings(keys) |
| 173 | fixedKeys = append(fixedKeys, keys...) |
| 174 | } else { |
| 175 | if !f.isColored() { |
| 176 | fixedKeys = append(fixedKeys, keys...) |
| 177 | f.SortingFunc(fixedKeys) |
| 178 | } else { |
| 179 | f.SortingFunc(keys) |
| 180 | } |
| 181 | } |
| 182 | } else { |
| 183 | fixedKeys = append(fixedKeys, keys...) |
| 184 | } |
| 185 | |
| 186 | var b *bytes.Buffer |
| 187 | if entry.Buffer != nil { |
| 188 | b = entry.Buffer |
| 189 | } else { |
| 190 | b = &bytes.Buffer{} |
| 191 | } |
| 192 | |
| 193 | f.terminalInitOnce.Do(func() { f.init(entry) }) |
| 194 | |
| 195 | timestampFormat := f.TimestampFormat |
| 196 | if timestampFormat == "" { |
| 197 | timestampFormat = defaultTimestampFormat |
| 198 | } |
| 199 | if f.isColored() { |
khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 200 | f.printColored(b, entry, keys, data, timestampFormat) |
khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 201 | } else { |
khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 202 | |
khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 203 | for _, key := range fixedKeys { |
| 204 | var value interface{} |
| 205 | switch { |
| 206 | case key == f.FieldMap.resolve(FieldKeyTime): |
| 207 | value = entry.Time.Format(timestampFormat) |
| 208 | case key == f.FieldMap.resolve(FieldKeyLevel): |
| 209 | value = entry.Level.String() |
| 210 | case key == f.FieldMap.resolve(FieldKeyMsg): |
| 211 | value = entry.Message |
| 212 | case key == f.FieldMap.resolve(FieldKeyLogrusError): |
| 213 | value = entry.err |
| 214 | case key == f.FieldMap.resolve(FieldKeyFunc) && entry.HasCaller(): |
khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 215 | value = funcVal |
khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 216 | case key == f.FieldMap.resolve(FieldKeyFile) && entry.HasCaller(): |
khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 217 | value = fileVal |
khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 218 | default: |
khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 219 | value = data[key] |
khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 220 | } |
| 221 | f.appendKeyValue(b, key, value) |
| 222 | } |
| 223 | } |
| 224 | |
| 225 | b.WriteByte('\n') |
| 226 | return b.Bytes(), nil |
| 227 | } |
| 228 | |
khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 229 | func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string, data Fields, timestampFormat string) { |
khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 230 | var levelColor int |
| 231 | switch entry.Level { |
| 232 | case DebugLevel, TraceLevel: |
| 233 | levelColor = gray |
| 234 | case WarnLevel: |
| 235 | levelColor = yellow |
| 236 | case ErrorLevel, FatalLevel, PanicLevel: |
| 237 | levelColor = red |
| 238 | default: |
| 239 | levelColor = blue |
| 240 | } |
| 241 | |
| 242 | levelText := strings.ToUpper(entry.Level.String()) |
khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 243 | if !f.DisableLevelTruncation && !f.PadLevelText { |
khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 244 | levelText = levelText[0:4] |
| 245 | } |
khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 246 | if f.PadLevelText { |
| 247 | // Generates the format string used in the next line, for example "%-6s" or "%-7s". |
| 248 | // Based on the max level text length. |
| 249 | formatString := "%-" + strconv.Itoa(f.levelTextMaxLength) + "s" |
| 250 | // Formats the level text by appending spaces up to the max length, for example: |
| 251 | // - "INFO " |
| 252 | // - "WARNING" |
| 253 | levelText = fmt.Sprintf(formatString, levelText) |
| 254 | } |
khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 255 | |
| 256 | // Remove a single newline if it already exists in the message to keep |
| 257 | // the behavior of logrus text_formatter the same as the stdlib log package |
| 258 | entry.Message = strings.TrimSuffix(entry.Message, "\n") |
| 259 | |
| 260 | caller := "" |
khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 261 | if entry.HasCaller() { |
khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 262 | funcVal := fmt.Sprintf("%s()", entry.Caller.Function) |
| 263 | fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line) |
| 264 | |
| 265 | if f.CallerPrettyfier != nil { |
| 266 | funcVal, fileVal = f.CallerPrettyfier(entry.Caller) |
| 267 | } |
| 268 | |
| 269 | if fileVal == "" { |
| 270 | caller = funcVal |
| 271 | } else if funcVal == "" { |
| 272 | caller = fileVal |
| 273 | } else { |
| 274 | caller = fileVal + " " + funcVal |
| 275 | } |
khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 276 | } |
| 277 | |
khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 278 | switch { |
| 279 | case f.DisableTimestamp: |
khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 280 | fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m%s %-44s ", levelColor, levelText, caller, entry.Message) |
khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 281 | case !f.FullTimestamp: |
khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 282 | fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d]%s %-44s ", levelColor, levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), caller, entry.Message) |
khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 283 | default: |
khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 284 | fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s]%s %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), caller, entry.Message) |
| 285 | } |
| 286 | for _, k := range keys { |
khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 287 | v := data[k] |
khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 288 | fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=", levelColor, k) |
| 289 | f.appendValue(b, v) |
| 290 | } |
| 291 | } |
| 292 | |
| 293 | func (f *TextFormatter) needsQuoting(text string) bool { |
khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 294 | if f.ForceQuote { |
| 295 | return true |
| 296 | } |
khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 297 | if f.QuoteEmptyFields && len(text) == 0 { |
| 298 | return true |
| 299 | } |
khenaidoo | d948f77 | 2021-08-11 17:49:24 -0400 | [diff] [blame] | 300 | if f.DisableQuote { |
| 301 | return false |
| 302 | } |
khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 303 | for _, ch := range text { |
| 304 | if !((ch >= 'a' && ch <= 'z') || |
| 305 | (ch >= 'A' && ch <= 'Z') || |
| 306 | (ch >= '0' && ch <= '9') || |
| 307 | ch == '-' || ch == '.' || ch == '_' || ch == '/' || ch == '@' || ch == '^' || ch == '+') { |
| 308 | return true |
| 309 | } |
| 310 | } |
| 311 | return false |
| 312 | } |
| 313 | |
| 314 | func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) { |
| 315 | if b.Len() > 0 { |
| 316 | b.WriteByte(' ') |
| 317 | } |
| 318 | b.WriteString(key) |
| 319 | b.WriteByte('=') |
| 320 | f.appendValue(b, value) |
| 321 | } |
| 322 | |
| 323 | func (f *TextFormatter) appendValue(b *bytes.Buffer, value interface{}) { |
| 324 | stringVal, ok := value.(string) |
| 325 | if !ok { |
| 326 | stringVal = fmt.Sprint(value) |
| 327 | } |
| 328 | |
| 329 | if !f.needsQuoting(stringVal) { |
| 330 | b.WriteString(stringVal) |
| 331 | } else { |
| 332 | b.WriteString(fmt.Sprintf("%q", stringVal)) |
| 333 | } |
| 334 | } |