github.com/amazechain/amc@v0.1.3/log/logrus-prefixed-formatter/formatter.go (about) 1 package prefixed 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "os" 8 "regexp" 9 "sort" 10 "strings" 11 "sync" 12 "time" 13 "unicode" 14 15 "github.com/mgutz/ansi" 16 "github.com/sirupsen/logrus" 17 "golang.org/x/crypto/ssh/terminal" 18 ) 19 20 const defaultTimestampFormat = time.RFC3339 21 22 var ( 23 baseTimestamp time.Time = time.Now() 24 defaultColorScheme *ColorScheme = &ColorScheme{ 25 InfoLevelStyle: "green", 26 WarnLevelStyle: "yellow", 27 ErrorLevelStyle: "red", 28 FatalLevelStyle: "red", 29 PanicLevelStyle: "red", 30 DebugLevelStyle: "blue", 31 PrefixStyle: "cyan", 32 TimestampStyle: "black+h", 33 } 34 noColorsColorScheme *compiledColorScheme = &compiledColorScheme{ 35 InfoLevelColor: ansi.ColorFunc(""), 36 WarnLevelColor: ansi.ColorFunc(""), 37 ErrorLevelColor: ansi.ColorFunc(""), 38 FatalLevelColor: ansi.ColorFunc(""), 39 PanicLevelColor: ansi.ColorFunc(""), 40 DebugLevelColor: ansi.ColorFunc(""), 41 PrefixColor: ansi.ColorFunc(""), 42 TimestampColor: ansi.ColorFunc(""), 43 } 44 defaultCompiledColorScheme *compiledColorScheme = compileColorScheme(defaultColorScheme) 45 ) 46 47 func miniTS() int { 48 return int(time.Since(baseTimestamp) / time.Second) 49 } 50 51 type ColorScheme struct { 52 InfoLevelStyle string 53 WarnLevelStyle string 54 ErrorLevelStyle string 55 FatalLevelStyle string 56 PanicLevelStyle string 57 DebugLevelStyle string 58 PrefixStyle string 59 TimestampStyle string 60 } 61 62 type compiledColorScheme struct { 63 InfoLevelColor func(string) string 64 WarnLevelColor func(string) string 65 ErrorLevelColor func(string) string 66 FatalLevelColor func(string) string 67 PanicLevelColor func(string) string 68 DebugLevelColor func(string) string 69 PrefixColor func(string) string 70 TimestampColor func(string) string 71 } 72 73 type TextFormatter struct { 74 // Set to true to bypass checking for a TTY before outputting colors. 75 ForceColors bool 76 77 // Whether the logger's out is to a terminal. 78 isTerminal bool 79 80 // Force disabling colors. For a TTY colors are enabled by default. 81 DisableColors bool 82 83 // Force formatted layout, even for non-TTY output. 84 ForceFormatting bool 85 86 // Disable timestamp logging. useful when output is redirected to logging 87 // system that already adds timestamps. 88 DisableTimestamp bool 89 90 // Disable the conversion of the log levels to uppercase 91 DisableUppercase bool 92 93 // Enable logging the full timestamp when a TTY is attached instead of just 94 // the time passed since beginning of execution. 95 FullTimestamp bool 96 97 // The fields are sorted by default for a consistent output. For applications 98 // that log extremely frequently and don't use the JSON formatter this may not 99 // be desired. 100 DisableSorting bool 101 102 // Wrap empty fields in quotes if true. 103 QuoteEmptyFields bool 104 105 sync.Once 106 107 // Pad msg field with spaces on the right for display. 108 // The value for this parameter will be the size of padding. 109 // Its default value is zero, which means no padding will be applied for msg. 110 SpacePadding int 111 112 // Color scheme to use. 113 colorScheme *compiledColorScheme 114 115 // Can be set to the override the default quoting character " 116 // with something else. For example: ', or `. 117 QuoteCharacter string 118 119 // Timestamp format to use for display when a full timestamp is printed. 120 TimestampFormat string 121 } 122 123 func getCompiledColor(main string, fallback string) func(string) string { 124 var style string 125 if main != "" { 126 style = main 127 } else { 128 style = fallback 129 } 130 return ansi.ColorFunc(style) 131 } 132 133 func compileColorScheme(s *ColorScheme) *compiledColorScheme { 134 return &compiledColorScheme{ 135 InfoLevelColor: getCompiledColor(s.InfoLevelStyle, defaultColorScheme.InfoLevelStyle), 136 WarnLevelColor: getCompiledColor(s.WarnLevelStyle, defaultColorScheme.WarnLevelStyle), 137 ErrorLevelColor: getCompiledColor(s.ErrorLevelStyle, defaultColorScheme.ErrorLevelStyle), 138 FatalLevelColor: getCompiledColor(s.FatalLevelStyle, defaultColorScheme.FatalLevelStyle), 139 PanicLevelColor: getCompiledColor(s.PanicLevelStyle, defaultColorScheme.PanicLevelStyle), 140 DebugLevelColor: getCompiledColor(s.DebugLevelStyle, defaultColorScheme.DebugLevelStyle), 141 PrefixColor: getCompiledColor(s.PrefixStyle, defaultColorScheme.PrefixStyle), 142 TimestampColor: getCompiledColor(s.TimestampStyle, defaultColorScheme.TimestampStyle), 143 } 144 } 145 146 func (f *TextFormatter) init(entry *logrus.Entry) { 147 if len(f.QuoteCharacter) == 0 { 148 f.QuoteCharacter = "\"" 149 } 150 if entry.Logger != nil { 151 f.isTerminal = f.checkIfTerminal(entry.Logger.Out) 152 } 153 } 154 155 func (f *TextFormatter) checkIfTerminal(w io.Writer) bool { 156 switch v := w.(type) { 157 case *os.File: 158 return terminal.IsTerminal(int(v.Fd())) 159 default: 160 return false 161 } 162 } 163 164 func (f *TextFormatter) SetColorScheme(colorScheme *ColorScheme) { 165 f.colorScheme = compileColorScheme(colorScheme) 166 } 167 168 func (f *TextFormatter) Format(entry *logrus.Entry) ([]byte, error) { 169 var b *bytes.Buffer 170 keys := make([]string, 0, len(entry.Data)) 171 for k := range entry.Data { 172 keys = append(keys, k) 173 } 174 lastKeyIdx := len(keys) - 1 175 176 if !f.DisableSorting { 177 sort.Strings(keys) 178 } 179 if entry.Buffer != nil { 180 b = entry.Buffer 181 } else { 182 b = &bytes.Buffer{} 183 } 184 185 prefixFieldClashes(entry.Data) 186 187 f.Do(func() { f.init(entry) }) 188 189 isFormatted := f.ForceFormatting || f.isTerminal 190 191 timestampFormat := f.TimestampFormat 192 if timestampFormat == "" { 193 timestampFormat = defaultTimestampFormat 194 } 195 if isFormatted { 196 isColored := (f.ForceColors || f.isTerminal) && !f.DisableColors 197 var colorScheme *compiledColorScheme 198 if isColored { 199 if f.colorScheme == nil { 200 colorScheme = defaultCompiledColorScheme 201 } else { 202 colorScheme = f.colorScheme 203 } 204 } else { 205 colorScheme = noColorsColorScheme 206 } 207 if err := f.printColored(b, entry, keys, timestampFormat, colorScheme); err != nil { 208 return nil, err 209 } 210 } else { 211 if !f.DisableTimestamp { 212 if err := f.appendKeyValue(b, "time", entry.Time.Format(timestampFormat), true); err != nil { 213 return nil, err 214 } 215 } 216 if err := f.appendKeyValue(b, "level", entry.Level.String(), true); err != nil { 217 return nil, err 218 } 219 if entry.Message != "" { 220 if err := f.appendKeyValue(b, "msg", entry.Message, lastKeyIdx >= 0); err != nil { 221 return nil, err 222 } 223 } 224 for i, key := range keys { 225 if err := f.appendKeyValue(b, key, entry.Data[key], lastKeyIdx != i); err != nil { 226 return nil, err 227 } 228 } 229 } 230 231 if err := b.WriteByte('\n'); err != nil { 232 return nil, err 233 } 234 return b.Bytes(), nil 235 } 236 237 func (f *TextFormatter) printColored(b *bytes.Buffer, entry *logrus.Entry, keys []string, timestampFormat string, colorScheme *compiledColorScheme) (err error) { 238 var levelColor func(string) string 239 var levelText string 240 switch entry.Level { 241 case logrus.InfoLevel: 242 levelColor = colorScheme.InfoLevelColor 243 case logrus.WarnLevel: 244 levelColor = colorScheme.WarnLevelColor 245 case logrus.ErrorLevel: 246 levelColor = colorScheme.ErrorLevelColor 247 case logrus.FatalLevel: 248 levelColor = colorScheme.FatalLevelColor 249 case logrus.PanicLevel: 250 levelColor = colorScheme.PanicLevelColor 251 default: 252 levelColor = colorScheme.DebugLevelColor 253 } 254 255 if entry.Level != logrus.WarnLevel { 256 levelText = entry.Level.String() 257 } else { 258 levelText = "warn" 259 } 260 261 if !f.DisableUppercase { 262 levelText = strings.ToUpper(levelText) 263 } 264 265 level := levelColor(fmt.Sprintf("%5s", levelText)) 266 prefix := "" 267 message := entry.Message 268 269 if prefixValue, ok := entry.Data["prefix"]; ok { 270 prefix = colorScheme.PrefixColor(" " + prefixValue.(string) + ":") 271 } else { 272 prefixValue, trimmedMsg := extractPrefix(entry.Message) 273 if len(prefixValue) > 0 { 274 prefix = colorScheme.PrefixColor(" " + prefixValue + ":") 275 message = trimmedMsg 276 } 277 } 278 279 messageFormat := "%s" 280 if f.SpacePadding != 0 { 281 messageFormat = fmt.Sprintf("%%-%ds", f.SpacePadding) 282 } 283 284 if f.DisableTimestamp { 285 _, err = fmt.Fprintf(b, "%s%s "+messageFormat, level, prefix, message) 286 } else { 287 var timestamp string 288 if !f.FullTimestamp { 289 timestamp = fmt.Sprintf("[%04d]", miniTS()) 290 } else { 291 timestamp = fmt.Sprintf("[%s]", entry.Time.Format(timestampFormat)) 292 } 293 _, err = fmt.Fprintf(b, "%s %s%s "+messageFormat, colorScheme.TimestampColor(timestamp), level, prefix, message) 294 } 295 for _, k := range keys { 296 if k != "prefix" { 297 v := entry.Data[k] 298 299 format := "%+v" 300 if k == logrus.ErrorKey { 301 format = "%v" // To avoid printing stack traces for errors 302 } 303 304 // Sanitize field values to remove new lines and other control characters. 305 s := sanitize(fmt.Sprintf(format, v)) 306 _, err = fmt.Fprintf(b, " %s=%s", levelColor(k), s) 307 } 308 } 309 return 310 } 311 312 func sanitize(s string) string { 313 return strings.Map(func(r rune) rune { 314 if unicode.IsControl(r) { 315 return -1 316 } 317 return r 318 }, s) 319 } 320 321 func (f *TextFormatter) needsQuoting(text string) bool { 322 if f.QuoteEmptyFields && len(text) == 0 { 323 return true 324 } 325 for _, ch := range text { 326 if !((ch >= 'a' && ch <= 'z') || 327 (ch >= 'A' && ch <= 'Z') || 328 (ch >= '0' && ch <= '9') || 329 ch == '-' || ch == '.') { 330 return true 331 } 332 } 333 return false 334 } 335 336 func extractPrefix(msg string) (string, string) { 337 prefix := "" 338 regex := regexp.MustCompile(`^\\[(.*?)\\]`) 339 if regex.MatchString(msg) { 340 match := regex.FindString(msg) 341 prefix, msg = match[1:len(match)-1], strings.TrimSpace(msg[len(match):]) 342 } 343 return prefix, msg 344 } 345 346 func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}, appendSpace bool) error { 347 b.WriteString(key) 348 b.WriteByte('=') 349 if err := f.appendValue(b, value); err != nil { 350 return err 351 } 352 353 if appendSpace { 354 return b.WriteByte(' ') 355 } 356 return nil 357 } 358 359 func (f *TextFormatter) appendValue(b *bytes.Buffer, value interface{}) (err error) { 360 switch value := value.(type) { 361 case string: 362 if !f.needsQuoting(value) { 363 _, err = b.WriteString(value) 364 } else { 365 _, err = fmt.Fprintf(b, "%s%v%s", f.QuoteCharacter, value, f.QuoteCharacter) 366 } 367 case error: 368 errmsg := value.Error() 369 if !f.needsQuoting(errmsg) { 370 _, err = b.WriteString(errmsg) 371 } else { 372 _, err = fmt.Fprintf(b, "%s%v%s", f.QuoteCharacter, errmsg, f.QuoteCharacter) 373 } 374 default: 375 _, err = fmt.Fprint(b, value) 376 } 377 return 378 } 379 380 // This is to not silently overwrite `time`, `msg` and `level` fields when 381 // dumping it. If this code wasn't there doing: 382 // 383 // logrus.WithField("level", 1).Info("hello") 384 // 385 // would just silently drop the user provided level. Instead with this code 386 // it'll be logged as: 387 // 388 // {"level": "info", "fields.level": 1, "msg": "hello", "time": "..."} 389 func prefixFieldClashes(data logrus.Fields) { 390 if t, ok := data["time"]; ok { 391 data["fields.time"] = t 392 } 393 394 if m, ok := data["msg"]; ok { 395 data["fields.msg"] = m 396 } 397 398 if l, ok := data["level"]; ok { 399 data["fields.level"] = l 400 } 401 }