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