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  }