package prefixed import ( "bytes" "fmt" "io" "os" "regexp" "runtime" "sort" "strings" "sync" "time" "github.com/mgutz/ansi" "github.com/sirupsen/logrus" "golang.org/x/crypto/ssh/terminal" ) const defaultTimestampFormat = time.RFC3339 var ( baseTimestamp time.Time = time.Now() defaultColorScheme *ColorScheme = &ColorScheme{ InfoLevelStyle: "green", WarnLevelStyle: "yellow", ErrorLevelStyle: "red", FatalLevelStyle: "red", PanicLevelStyle: "red", DebugLevelStyle: "blue", PrefixStyle: "cyan", TimestampStyle: "black+h", } noColorsColorScheme *compiledColorScheme = &compiledColorScheme{ InfoLevelColor: ansi.ColorFunc(""), WarnLevelColor: ansi.ColorFunc(""), ErrorLevelColor: ansi.ColorFunc(""), FatalLevelColor: ansi.ColorFunc(""), PanicLevelColor: ansi.ColorFunc(""), DebugLevelColor: ansi.ColorFunc(""), PrefixColor: ansi.ColorFunc(""), TimestampColor: ansi.ColorFunc(""), } defaultCompiledColorScheme *compiledColorScheme = compileColorScheme(defaultColorScheme) ) func miniTS() int { return int(time.Since(baseTimestamp) / time.Second) } type ColorScheme struct { InfoLevelStyle string WarnLevelStyle string ErrorLevelStyle string FatalLevelStyle string PanicLevelStyle string DebugLevelStyle string PrefixStyle string TimestampStyle string } type compiledColorScheme struct { InfoLevelColor func(string) string WarnLevelColor func(string) string ErrorLevelColor func(string) string FatalLevelColor func(string) string PanicLevelColor func(string) string DebugLevelColor func(string) string PrefixColor func(string) string TimestampColor func(string) string } type TextFormatter struct { // Set to true to bypass checking for a TTY before outputting colors. ForceColors bool // Force disabling colors. For a TTY colors are enabled by default. DisableColors bool // Force formatted layout, even for non-TTY output. ForceFormatting bool // Disable timestamp logging. useful when output is redirected to logging // system that already adds timestamps. DisableTimestamp bool // Disable the conversion of the log levels to uppercase DisableUppercase bool // Enable logging the full timestamp when a TTY is attached instead of just // the time passed since beginning of execution. FullTimestamp bool // Timestamp format to use for display when a full timestamp is printed. TimestampFormat string // The fields are sorted by default for a consistent output. For applications // that log extremely frequently and don't use the JSON formatter this may not // be desired. DisableSorting bool // Wrap empty fields in quotes if true. QuoteEmptyFields bool // Can be set to the override the default quoting character " // with something else. For example: ', or `. QuoteCharacter string // Pad msg field with spaces on the right for display. // The value for this parameter will be the size of padding. // Its default value is zero, which means no padding will be applied for msg. SpacePadding int // Pad prefix field with spaces on the right for display. // The value for this parameter will be the size of padding. // Its default value is zero, which means no padding will be applied for prefix. PrefixPadding int // Color scheme to use. colorScheme *compiledColorScheme // Whether the logger's out is to a terminal. isTerminal bool // CallerPrettyfier can be set by the user to modify the content // of the function and file keys in the data when ReportCaller is // activated. If any of the returned value is the empty string the // corresponding key will be removed from fields. CallerPrettyfier func(*runtime.Frame) (function string, file string) CallerFormatter func(function, file string) string sync.Once } func getCompiledColor(main string, fallback string) func(string) string { var style string if main != "" { style = main } else { style = fallback } return ansi.ColorFunc(style) } func compileColorScheme(s *ColorScheme) *compiledColorScheme { return &compiledColorScheme{ InfoLevelColor: getCompiledColor(s.InfoLevelStyle, defaultColorScheme.InfoLevelStyle), WarnLevelColor: getCompiledColor(s.WarnLevelStyle, defaultColorScheme.WarnLevelStyle), ErrorLevelColor: getCompiledColor(s.ErrorLevelStyle, defaultColorScheme.ErrorLevelStyle), FatalLevelColor: getCompiledColor(s.FatalLevelStyle, defaultColorScheme.FatalLevelStyle), PanicLevelColor: getCompiledColor(s.PanicLevelStyle, defaultColorScheme.PanicLevelStyle), DebugLevelColor: getCompiledColor(s.DebugLevelStyle, defaultColorScheme.DebugLevelStyle), PrefixColor: getCompiledColor(s.PrefixStyle, defaultColorScheme.PrefixStyle), TimestampColor: getCompiledColor(s.TimestampStyle, defaultColorScheme.TimestampStyle), } } func (f *TextFormatter) init(entry *logrus.Entry) { if len(f.QuoteCharacter) == 0 { f.QuoteCharacter = "\"" } if entry.Logger != nil { f.isTerminal = f.checkIfTerminal(entry.Logger.Out) } } func (f *TextFormatter) checkIfTerminal(w io.Writer) bool { switch v := w.(type) { case *os.File: return terminal.IsTerminal(int(v.Fd())) default: return false } } func (f *TextFormatter) SetColorScheme(colorScheme *ColorScheme) { f.colorScheme = compileColorScheme(colorScheme) } func (f *TextFormatter) Format(entry *logrus.Entry) ([]byte, error) { var b *bytes.Buffer var keys []string = make([]string, 0, len(entry.Data)) for k := range entry.Data { keys = append(keys, k) } lastKeyIdx := len(keys) - 1 if !f.DisableSorting { sort.Strings(keys) } if entry.Buffer != nil { b = entry.Buffer } else { b = &bytes.Buffer{} } prefixFieldClashes(entry.Data) f.Do(func() { f.init(entry) }) isFormatted := f.ForceFormatting || f.isTerminal timestampFormat := f.TimestampFormat if timestampFormat == "" { timestampFormat = defaultTimestampFormat } if isFormatted { isColored := (f.ForceColors || f.isTerminal) && !f.DisableColors var colorScheme *compiledColorScheme if isColored { if f.colorScheme == nil { colorScheme = defaultCompiledColorScheme } else { colorScheme = f.colorScheme } } else { colorScheme = noColorsColorScheme } f.printColored(b, entry, keys, timestampFormat, colorScheme) } else { if !f.DisableTimestamp { f.appendKeyValue(b, "time", entry.Time.Format(timestampFormat), true) } f.appendKeyValue(b, "level", entry.Level.String(), true) if entry.Message != "" { f.appendKeyValue(b, "msg", entry.Message, lastKeyIdx >= 0) } if entry.HasCaller() { var funcVal, fileVal string if f.CallerPrettyfier != nil { funcVal, fileVal = f.CallerPrettyfier(entry.Caller) } else { funcVal, fileVal = extractCallerInfo(entry.Caller) } if funcVal != "" { f.appendKeyValue(b, "func", funcVal, true) } if fileVal != "" { f.appendKeyValue(b, "file", fileVal, true) } } for i, key := range keys { f.appendKeyValue(b, key, entry.Data[key], lastKeyIdx != i) } } b.WriteByte('\n') return b.Bytes(), nil } func (f *TextFormatter) printColored(b *bytes.Buffer, entry *logrus.Entry, keys []string, timestampFormat string, colorScheme *compiledColorScheme) { var levelColor func(string) string var levelText string switch entry.Level { case logrus.InfoLevel: levelColor = colorScheme.InfoLevelColor case logrus.WarnLevel: levelColor = colorScheme.WarnLevelColor case logrus.ErrorLevel: levelColor = colorScheme.ErrorLevelColor case logrus.FatalLevel: levelColor = colorScheme.FatalLevelColor case logrus.PanicLevel: levelColor = colorScheme.PanicLevelColor default: levelColor = colorScheme.DebugLevelColor } if entry.Level != logrus.WarnLevel { levelText = entry.Level.String() } else { levelText = "warn" } if !f.DisableUppercase { levelText = strings.ToUpper(levelText) } level := levelColor(fmt.Sprintf("%5s", levelText)) prefix := "" message := entry.Message adjustedPrefixPadding := f.PrefixPadding //compensate for ANSI color sequences if prefixValue, ok := entry.Data["prefix"]; ok { rawPrefixLength := len(prefixValue.(string)) prefix = colorScheme.PrefixColor(" " + prefixValue.(string) + ":") adjustedPrefixPadding = f.PrefixPadding + (len(prefix) - rawPrefixLength - 1) } else { prefixValue, trimmedMsg := extractPrefix(entry.Message) rawPrefixLength := len(prefixValue) if len(prefixValue) > 0 { prefix = colorScheme.PrefixColor(" " + prefixValue + ":") message = trimmedMsg } adjustedPrefixPadding = f.PrefixPadding + (len(prefix) - rawPrefixLength - 1) } prefixFormat := "%s" if f.PrefixPadding != 0 { prefixFormat = fmt.Sprintf("%%-%ds%%s", adjustedPrefixPadding) } messageFormat := "%s" if f.SpacePadding != 0 { messageFormat = fmt.Sprintf("%%-%ds", f.SpacePadding) } caller := "" if entry.HasCaller() { var funcVal, fileVal string if f.CallerPrettyfier != nil { funcVal, fileVal = f.CallerPrettyfier(entry.Caller) } else { funcVal, fileVal = extractCallerInfo(entry.Caller) } if f.CallerFormatter != nil { caller = f.CallerFormatter(funcVal, fileVal) } else { caller = fmt.Sprintf(" (%s: %s)", fileVal, funcVal) } } if f.DisableTimestamp { fmt.Fprintf(b, "%s"+prefixFormat+" "+messageFormat, level, prefix, caller, message) } else { var timestamp string if !f.FullTimestamp { timestamp = fmt.Sprintf("[%04d]", miniTS()) } else { timestamp = fmt.Sprintf("[%s]", entry.Time.Format(timestampFormat)) } fmt.Fprintf(b, "%s %s"+prefixFormat+" "+messageFormat, colorScheme.TimestampColor(timestamp), level, prefix, caller, message) } for _, k := range keys { if k != "prefix" { v := entry.Data[k] fmt.Fprintf(b, " %s=%+v", levelColor(k), v) } } } func (f *TextFormatter) needsQuoting(text string) bool { if f.QuoteEmptyFields && len(text) == 0 { return true } for _, ch := range text { if !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '-' || ch == '.') { return true } } return false } func extractCallerInfo(caller *runtime.Frame) (string, string) { funcVal := caller.Function fileVal := fmt.Sprintf("%s:%d", caller.File, caller.Line) return funcVal, fileVal } func extractPrefix(msg string) (string, string) { prefix := "" regex := regexp.MustCompile("^\\[(.*?)\\]") if regex.MatchString(msg) { match := regex.FindString(msg) prefix, msg = match[1:len(match)-1], strings.TrimSpace(msg[len(match):]) } return prefix, msg } func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}, appendSpace bool) { b.WriteString(key) b.WriteByte('=') f.appendValue(b, value) if appendSpace { b.WriteByte(' ') } } func (f *TextFormatter) appendValue(b *bytes.Buffer, value interface{}) { switch value := value.(type) { case string: if !f.needsQuoting(value) { b.WriteString(value) } else { fmt.Fprintf(b, "%s%v%s", f.QuoteCharacter, value, f.QuoteCharacter) } case error: errmsg := value.Error() if !f.needsQuoting(errmsg) { b.WriteString(errmsg) } else { fmt.Fprintf(b, "%s%v%s", f.QuoteCharacter, errmsg, f.QuoteCharacter) } default: fmt.Fprint(b, value) } } // This is to not silently overwrite `time`, `msg` and `level` fields when // dumping it. If this code wasn't there doing: // // logrus.WithField("level", 1).Info("hello") // // would just silently drop the user provided level. Instead with this code // it'll be logged as: // // {"level": "info", "fields.level": 1, "msg": "hello", "time": "..."} func prefixFieldClashes(data logrus.Fields) { if t, ok := data["time"]; ok { data["fields.time"] = t } if m, ok := data["msg"]; ok { data["fields.msg"] = m } if l, ok := data["level"]; ok { data["fields.level"] = l } }