github.com/kaydxh/golang@v0.0.131/pkg/logs/logrus/glog_formatter.go (about)

     1  /*
     2   *Copyright (c) 2022, kaydxh
     3   *
     4   *Permission is hereby granted, free of charge, to any person obtaining a copy
     5   *of this software and associated documentation files (the "Software"), to deal
     6   *in the Software without restriction, including without limitation the rights
     7   *to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     8   *copies of the Software, and to permit persons to whom the Software is
     9   *furnished to do so, subject to the following conditions:
    10   *
    11   *The above copyright notice and this permission notice shall be included in all
    12   *copies or substantial portions of the Software.
    13   *
    14   *THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    15   *IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    16   *FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    17   *AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    18   *LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    19   *OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    20   *SOFTWARE.
    21   */
    22  package logrus
    23  
    24  import (
    25  	"bytes"
    26  	"fmt"
    27  	"os"
    28  	"runtime"
    29  	"sort"
    30  	"strconv"
    31  	"strings"
    32  	"sync"
    33  	"time"
    34  	"unicode/utf8"
    35  
    36  	runtime_ "github.com/kaydxh/golang/go/runtime"
    37  	"github.com/sirupsen/logrus"
    38  )
    39  
    40  const (
    41  	red    = 31
    42  	yellow = 33
    43  	blue   = 36
    44  	gray   = 37
    45  )
    46  
    47  var (
    48  	baseTimestamp time.Time
    49  	pid           int
    50  )
    51  
    52  func init() {
    53  	baseTimestamp = time.Now()
    54  	pid = os.Getpid()
    55  }
    56  
    57  /*
    58  https://medium.com/technical-tips/google-log-glog-output-format-7eb31b3f0ce5
    59  [IWEF]yyyymmdd hh:mm:ss.uuuuuu threadid file:line] msg
    60  IWEF — Log Levels, I for INFO, W for WARNING, E for ERROR and `F` for FATAL.
    61  yyyymmdd — Year, Month and Date.
    62  hh:mm:ss.uuuuuu — Hours, Minutes, Seconds and Microseconds.
    63  threadid — PID/TID of the process/thread.
    64  file:line — File name and line number.
    65  msg — Actual user-specified log message.
    66  */
    67  
    68  // GlogFormatter formats logs into text
    69  type GlogFormatter struct {
    70  	// Set to true to bypass checking for a TTY before outputting colors.
    71  	ForceColors bool
    72  
    73  	// Force disabling colors.
    74  	DisableColors bool
    75  
    76  	// Force quoting of all values
    77  	ForceQuote bool
    78  
    79  	// DisableQuote disables quoting for all values.
    80  	// DisableQuote will have a lower priority than ForceQuote.
    81  	// If both of them are set to true, quote will be forced on all values.
    82  	DisableQuote bool
    83  
    84  	// Override coloring based on CLICOLOR and CLICOLOR_FORCE. - https://bixense.com/clicolors/
    85  	EnvironmentOverrideColors bool
    86  
    87  	// Disable timestamp logging. useful when output is redirected to logging
    88  	// system that already adds timestamps.
    89  	DisableTimestamp bool
    90  
    91  	// Enable logging the full timestamp when a TTY is attached instead of just
    92  	// the time passed since beginning of execution.
    93  	FullTimestamp bool
    94  
    95  	// TimestampFormat to use for display when a full timestamp is printed
    96  	TimestampFormat string
    97  
    98  	// The fields are sorted by default for a consistent output. For applications
    99  	// that log extremely frequently and don't use the JSON formatter this may not
   100  	// be desired.
   101  	DisableSorting bool
   102  
   103  	// The keys sorting function, when uninitialized it uses sort.Strings.
   104  	SortingFunc func([]string)
   105  
   106  	// Disables the truncation of the level text to 4 characters.
   107  	DisableLevelTruncation bool
   108  
   109  	// PadLevelText Adds padding the level text so that all the levels output at the same length
   110  	// PadLevelText is a superset of the DisableLevelTruncation option
   111  	PadLevelText bool
   112  
   113  	// QuoteEmptyFields will wrap empty fields in quotes if true
   114  	QuoteEmptyFields bool
   115  
   116  	// Whether the logger's out is to a terminal
   117  	isTerminal bool
   118  
   119  	// FieldMap allows users to customize the names of keys for default fields.
   120  	// As an example:
   121  	// formatter := &TextFormatter{
   122  	//     FieldMap: FieldMap{
   123  	//         FieldKeyTime:  "@timestamp",
   124  	//         FieldKeyLevel: "@level",
   125  	//         FieldKeyMsg:   "@message"}}
   126  	FieldMap FieldMap
   127  
   128  	// CallerPrettyfier can be set by the user to modify the content
   129  	// of the function and file keys in the data when ReportCaller is
   130  	// activated. If any of the returned value is the empty string the
   131  	// corresponding key will be removed from fields.
   132  	CallerPrettyfier func(*runtime.Frame) (function string, file string)
   133  
   134  	terminalInitOnce sync.Once
   135  
   136  	// The max length of the level text, generated dynamically on init
   137  	levelTextMaxLength int
   138  
   139  	// Disables the glog style :[IWEF]yyyymmdd hh:mm:ss.uuuuuu threadid file:line] msg msg...
   140  	// replace with :[IWEF] [yyyymmdd] [hh:mm:ss.uuuuuu] [threadid] [file:line] msg msg...
   141  	//EnablePrettyLog   bool
   142  	EnableGoroutineId bool
   143  }
   144  
   145  func (f *GlogFormatter) init(entry *logrus.Entry) {
   146  	if entry.Logger != nil {
   147  		f.isTerminal = checkIfTerminal(entry.Logger.Out)
   148  	}
   149  	// Get the max length of the level text
   150  	for _, level := range logrus.AllLevels {
   151  		levelTextLength := utf8.RuneCount([]byte(level.String()))
   152  		if levelTextLength > f.levelTextMaxLength {
   153  			f.levelTextMaxLength = levelTextLength
   154  		}
   155  	}
   156  }
   157  
   158  func (f *GlogFormatter) isColored() bool {
   159  	isColored := f.ForceColors || (f.isTerminal && (runtime.GOOS != "windows"))
   160  
   161  	if f.EnvironmentOverrideColors {
   162  		switch force, ok := os.LookupEnv("CLICOLOR_FORCE"); {
   163  		case ok && force != "0":
   164  			isColored = true
   165  		case ok && force == "0", os.Getenv("CLICOLOR") == "0":
   166  			isColored = false
   167  		}
   168  	}
   169  
   170  	return isColored && !f.DisableColors
   171  }
   172  
   173  // Format renders a single log entry
   174  func (f *GlogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
   175  	data := make(logrus.Fields)
   176  	for k, v := range entry.Data {
   177  		data[k] = v
   178  	}
   179  	prefixFieldClashes(data, f.FieldMap, entry.HasCaller())
   180  	keys := make([]string, 0, len(data))
   181  	for k := range data {
   182  		keys = append(keys, k)
   183  	}
   184  
   185  	fixedKeys := make([]string, 0, 4+len(data))
   186  	if entry.Message != "" {
   187  		fixedKeys = append(fixedKeys, f.FieldMap.resolve(logrus.FieldKeyMsg))
   188  	}
   189  
   190  	if !f.DisableSorting {
   191  		if f.SortingFunc == nil {
   192  			sort.Strings(keys)
   193  			fixedKeys = append(fixedKeys, keys...)
   194  		} else {
   195  			if !f.isColored() {
   196  				fixedKeys = append(fixedKeys, keys...)
   197  				f.SortingFunc(fixedKeys)
   198  			} else {
   199  				f.SortingFunc(keys)
   200  			}
   201  		}
   202  	} else {
   203  		fixedKeys = append(fixedKeys, keys...)
   204  	}
   205  
   206  	var b *bytes.Buffer
   207  	if entry.Buffer != nil {
   208  		b = entry.Buffer
   209  	} else {
   210  		b = &bytes.Buffer{}
   211  	}
   212  
   213  	f.terminalInitOnce.Do(func() { f.init(entry) })
   214  
   215  	timestampFormat := f.TimestampFormat
   216  	if timestampFormat == "" {
   217  		timestampFormat = defaultTimestampFormat
   218  	}
   219  
   220  	levelText := f.level(entry)
   221  	b.Write(f.formatHeader(entry, levelText))
   222  
   223  	for _, key := range fixedKeys {
   224  		var value interface{}
   225  		switch {
   226  		case key == f.FieldMap.resolve(FieldKeyMsg):
   227  			value = entry.Message
   228  			f.appendValue(b, value)
   229  			//continue means msg not need call f.appendKeyValue(b, key, value)
   230  			//or will duplicate write message
   231  			continue
   232  		case key == f.FieldMap.resolve(fieldKey(logrus.ErrorKey)):
   233  			value = data[logrus.ErrorKey]
   234  		default:
   235  			value = data[key]
   236  		}
   237  		key = fmt.Sprintf("%s", key)
   238  		f.appendKeyValue(b, key, value)
   239  	}
   240  
   241  	b.WriteByte('\n')
   242  	return b.Bytes(), nil
   243  }
   244  
   245  func (f *GlogFormatter) level(entry *logrus.Entry) string {
   246  	levelText := strings.ToUpper(entry.Level.String())
   247  	if !f.DisableLevelTruncation && !f.PadLevelText {
   248  		levelText = levelText[0:4]
   249  	}
   250  	if f.PadLevelText {
   251  		// Generates the format string used in the next line, for example "%-6s" or "%-7s".
   252  		// Based on the max level text length.
   253  		formatString := "%-" + strconv.Itoa(f.levelTextMaxLength) + "s"
   254  		// Formats the level text by appending spaces up to the max length, for example:
   255  		// 	- "INFO   "
   256  		//	- "WARNING"
   257  		levelText = fmt.Sprintf(formatString, levelText)
   258  	}
   259  
   260  	return levelText
   261  }
   262  
   263  func (f *GlogFormatter) needsQuoting(text string) bool {
   264  	if f.ForceQuote {
   265  		return true
   266  	}
   267  	if f.QuoteEmptyFields && len(text) == 0 {
   268  		return true
   269  	}
   270  	if f.DisableQuote {
   271  		return false
   272  	}
   273  	for _, ch := range text {
   274  		if !((ch >= 'a' && ch <= 'z') ||
   275  			(ch >= 'A' && ch <= 'Z') ||
   276  			(ch >= '0' && ch <= '9') ||
   277  			ch == '-' || ch == '.' || ch == '_' || ch == '/' || ch == '@' || ch == '^' || ch == '+') {
   278  			return true
   279  		}
   280  	}
   281  	return false
   282  }
   283  
   284  func (f *GlogFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) {
   285  	if b.Len() > 0 {
   286  		b.WriteByte(' ')
   287  	}
   288  	b.WriteString(key)
   289  	b.WriteByte('=')
   290  	f.appendValue(b, value)
   291  }
   292  
   293  func (f *GlogFormatter) appendValue(b *bytes.Buffer, value interface{}) {
   294  	stringVal, ok := value.(string)
   295  	if !ok {
   296  		stringVal = fmt.Sprint(value)
   297  	}
   298  
   299  	if !f.needsQuoting(stringVal) {
   300  		b.WriteString(stringVal)
   301  	} else {
   302  		b.WriteString(fmt.Sprintf("%q", stringVal))
   303  	}
   304  }
   305  
   306  // Log line format: [IWEF]yyyymmdd hh:mm:ss.uuuuuu threadid file:line] msg
   307  func (f *GlogFormatter) formatHeader(entry *logrus.Entry, levelText string) []byte {
   308  
   309  	var buf bytes.Buffer
   310  	switch {
   311  	case f.DisableTimestamp:
   312  		buf.WriteString(fmt.Sprintf("[%s]", levelText))
   313  	case !f.FullTimestamp:
   314  		buf.WriteString(fmt.Sprintf("[%s] [%04d]", levelText, int(entry.Time.Sub(baseTimestamp)/time.Second)))
   315  	default:
   316  		buf.WriteString(fmt.Sprintf("[%s] [%s]", levelText,
   317  			entry.Time.Format(f.TimestampFormat),
   318  		))
   319  	}
   320  
   321  	if f.EnableGoroutineId {
   322  		buf.WriteString(fmt.Sprintf(" [%d]", runtime_.GoroutineID()))
   323  	} else {
   324  		//use pid instead of goroutine id
   325  		buf.WriteString(fmt.Sprintf(" [%d]", pid))
   326  	}
   327  
   328  	var (
   329  		function string
   330  		fileline string = "???:-1"
   331  	)
   332  
   333  	if entry.HasCaller() {
   334  		if f.CallerPrettyfier != nil {
   335  			function, fileline = f.CallerPrettyfier(entry.Caller)
   336  		} else {
   337  			function = entry.Caller.Function
   338  			fileline = fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
   339  		}
   340  
   341  		buf.WriteString(fmt.Sprintf(" [%s](%s)", fileline, function))
   342  	}
   343  
   344  	//split head and body
   345  	buf.WriteString(" ")
   346  
   347  	return buf.Bytes()
   348  }