khenaidoo | ab1f7bd | 2019-11-14 14:00:27 -0500 | [diff] [blame] | 1 | package logrus |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "fmt" |
| 6 | "os" |
| 7 | "sort" |
| 8 | "strings" |
| 9 | "sync" |
| 10 | "time" |
| 11 | ) |
| 12 | |
| 13 | const ( |
| 14 | nocolor = 0 |
| 15 | red = 31 |
| 16 | green = 32 |
| 17 | yellow = 33 |
| 18 | blue = 36 |
| 19 | gray = 37 |
| 20 | ) |
| 21 | |
| 22 | var ( |
| 23 | baseTimestamp time.Time |
| 24 | emptyFieldMap FieldMap |
| 25 | ) |
| 26 | |
| 27 | func init() { |
| 28 | baseTimestamp = time.Now() |
| 29 | } |
| 30 | |
| 31 | // TextFormatter formats logs into text |
| 32 | type 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 | |
| 82 | func (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 | |
| 92 | func (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 |
| 109 | func (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 | |
| 191 | func (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 | |
| 234 | func (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 | |
| 249 | func (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 | |
| 258 | func (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 | } |