mirror of
				https://github.com/42wim/matterbridge.git
				synced 2025-10-31 13:57:25 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			383 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			383 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package prefixed
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"os"
 | |
| 	"regexp"
 | |
| 	"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
 | |
| 
 | |
| 	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)
 | |
| 		}
 | |
| 		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", adjustedPrefixPadding)
 | |
| 	}
 | |
| 
 | |
| 	messageFormat := "%s"
 | |
| 	if f.SpacePadding != 0 {
 | |
| 		messageFormat = fmt.Sprintf("%%-%ds", f.SpacePadding)
 | |
| 	}
 | |
| 
 | |
| 	if f.DisableTimestamp {
 | |
| 		fmt.Fprintf(b, "%s"+prefixFormat+" "+messageFormat, level, prefix, 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, 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 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
 | |
| 	}
 | |
| }
 | 
