github.com/tacshi/go-ethereum@v0.0.0-20230616113857-84a434e20921/log/format.go (about)

     1  package log
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"math/big"
     8  	"reflect"
     9  	"strconv"
    10  	"strings"
    11  	"sync"
    12  	"sync/atomic"
    13  	"time"
    14  	"unicode/utf8"
    15  )
    16  
    17  const (
    18  	timeFormat        = "2006-01-02T15:04:05-0700"
    19  	termTimeFormat    = "01-02|15:04:05.000"
    20  	floatFormat       = 'f'
    21  	termMsgJust       = 40
    22  	termCtxMaxPadding = 40
    23  )
    24  
    25  // locationTrims are trimmed for display to avoid unwieldy log lines.
    26  var locationTrims = []string{
    27  	"github.com/tacshi/go-ethereum/",
    28  }
    29  
    30  // PrintOrigins sets or unsets log location (file:line) printing for terminal
    31  // format output.
    32  func PrintOrigins(print bool) {
    33  	if print {
    34  		atomic.StoreUint32(&locationEnabled, 1)
    35  	} else {
    36  		atomic.StoreUint32(&locationEnabled, 0)
    37  	}
    38  }
    39  
    40  // locationEnabled is an atomic flag controlling whether the terminal formatter
    41  // should append the log locations too when printing entries.
    42  var locationEnabled uint32
    43  
    44  // locationLength is the maxmimum path length encountered, which all logs are
    45  // padded to to aid in alignment.
    46  var locationLength uint32
    47  
    48  // fieldPadding is a global map with maximum field value lengths seen until now
    49  // to allow padding log contexts in a bit smarter way.
    50  var fieldPadding = make(map[string]int)
    51  
    52  // fieldPaddingLock is a global mutex protecting the field padding map.
    53  var fieldPaddingLock sync.RWMutex
    54  
    55  type Format interface {
    56  	Format(r *Record) []byte
    57  }
    58  
    59  // FormatFunc returns a new Format object which uses
    60  // the given function to perform record formatting.
    61  func FormatFunc(f func(*Record) []byte) Format {
    62  	return formatFunc(f)
    63  }
    64  
    65  type formatFunc func(*Record) []byte
    66  
    67  func (f formatFunc) Format(r *Record) []byte {
    68  	return f(r)
    69  }
    70  
    71  // TerminalStringer is an analogous interface to the stdlib stringer, allowing
    72  // own types to have custom shortened serialization formats when printed to the
    73  // screen.
    74  type TerminalStringer interface {
    75  	TerminalString() string
    76  }
    77  
    78  // TerminalFormat formats log records optimized for human readability on
    79  // a terminal with color-coded level output and terser human friendly timestamp.
    80  // This format should only be used for interactive programs or while developing.
    81  //
    82  //	[LEVEL] [TIME] MESSAGE key=value key=value ...
    83  //
    84  // Example:
    85  //
    86  //	[DBUG] [May 16 20:58:45] remove route ns=haproxy addr=127.0.0.1:50002
    87  func TerminalFormat(usecolor bool) Format {
    88  	return FormatFunc(func(r *Record) []byte {
    89  		msg := escapeMessage(r.Msg)
    90  		var color = 0
    91  		if usecolor {
    92  			switch r.Lvl {
    93  			case LvlCrit:
    94  				color = 35
    95  			case LvlError:
    96  				color = 31
    97  			case LvlWarn:
    98  				color = 33
    99  			case LvlInfo:
   100  				color = 32
   101  			case LvlDebug:
   102  				color = 36
   103  			case LvlTrace:
   104  				color = 34
   105  			}
   106  		}
   107  
   108  		b := &bytes.Buffer{}
   109  		lvl := r.Lvl.AlignedString()
   110  		if atomic.LoadUint32(&locationEnabled) != 0 {
   111  			// Log origin printing was requested, format the location path and line number
   112  			location := fmt.Sprintf("%+v", r.Call)
   113  			for _, prefix := range locationTrims {
   114  				location = strings.TrimPrefix(location, prefix)
   115  			}
   116  			// Maintain the maximum location length for fancyer alignment
   117  			align := int(atomic.LoadUint32(&locationLength))
   118  			if align < len(location) {
   119  				align = len(location)
   120  				atomic.StoreUint32(&locationLength, uint32(align))
   121  			}
   122  			padding := strings.Repeat(" ", align-len(location))
   123  
   124  			// Assemble and print the log heading
   125  			if color > 0 {
   126  				fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s|%s]%s %s ", color, lvl, r.Time.Format(termTimeFormat), location, padding, msg)
   127  			} else {
   128  				fmt.Fprintf(b, "%s[%s|%s]%s %s ", lvl, r.Time.Format(termTimeFormat), location, padding, msg)
   129  			}
   130  		} else {
   131  			if color > 0 {
   132  				fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s] %s ", color, lvl, r.Time.Format(termTimeFormat), msg)
   133  			} else {
   134  				fmt.Fprintf(b, "%s[%s] %s ", lvl, r.Time.Format(termTimeFormat), msg)
   135  			}
   136  		}
   137  		// try to justify the log output for short messages
   138  		length := utf8.RuneCountInString(msg)
   139  		if len(r.Ctx) > 0 && length < termMsgJust {
   140  			b.Write(bytes.Repeat([]byte{' '}, termMsgJust-length))
   141  		}
   142  		// print the keys logfmt style
   143  		logfmt(b, r.Ctx, color, true)
   144  		return b.Bytes()
   145  	})
   146  }
   147  
   148  // LogfmtFormat prints records in logfmt format, an easy machine-parseable but human-readable
   149  // format for key/value pairs.
   150  //
   151  // For more details see: http://godoc.org/github.com/kr/logfmt
   152  func LogfmtFormat() Format {
   153  	return FormatFunc(func(r *Record) []byte {
   154  		common := []interface{}{r.KeyNames.Time, r.Time, r.KeyNames.Lvl, r.Lvl, r.KeyNames.Msg, r.Msg}
   155  		buf := &bytes.Buffer{}
   156  		logfmt(buf, append(common, r.Ctx...), 0, false)
   157  		return buf.Bytes()
   158  	})
   159  }
   160  
   161  func logfmt(buf *bytes.Buffer, ctx []interface{}, color int, term bool) {
   162  	for i := 0; i < len(ctx); i += 2 {
   163  		if i != 0 {
   164  			buf.WriteByte(' ')
   165  		}
   166  
   167  		k, ok := ctx[i].(string)
   168  		v := formatLogfmtValue(ctx[i+1], term)
   169  		if !ok {
   170  			k, v = errorKey, formatLogfmtValue(k, term)
   171  		} else {
   172  			k = escapeString(k)
   173  		}
   174  
   175  		// XXX: we should probably check that all of your key bytes aren't invalid
   176  		fieldPaddingLock.RLock()
   177  		padding := fieldPadding[k]
   178  		fieldPaddingLock.RUnlock()
   179  
   180  		length := utf8.RuneCountInString(v)
   181  		if padding < length && length <= termCtxMaxPadding {
   182  			padding = length
   183  
   184  			fieldPaddingLock.Lock()
   185  			fieldPadding[k] = padding
   186  			fieldPaddingLock.Unlock()
   187  		}
   188  		if color > 0 {
   189  			fmt.Fprintf(buf, "\x1b[%dm%s\x1b[0m=", color, k)
   190  		} else {
   191  			buf.WriteString(k)
   192  			buf.WriteByte('=')
   193  		}
   194  		buf.WriteString(v)
   195  		if i < len(ctx)-2 && padding > length {
   196  			buf.Write(bytes.Repeat([]byte{' '}, padding-length))
   197  		}
   198  	}
   199  	buf.WriteByte('\n')
   200  }
   201  
   202  // JSONFormat formats log records as JSON objects separated by newlines.
   203  // It is the equivalent of JSONFormatEx(false, true).
   204  func JSONFormat() Format {
   205  	return JSONFormatEx(false, true)
   206  }
   207  
   208  // JSONFormatOrderedEx formats log records as JSON arrays. If pretty is true,
   209  // records will be pretty-printed. If lineSeparated is true, records
   210  // will be logged with a new line between each record.
   211  func JSONFormatOrderedEx(pretty, lineSeparated bool) Format {
   212  	jsonMarshal := json.Marshal
   213  	if pretty {
   214  		jsonMarshal = func(v interface{}) ([]byte, error) {
   215  			return json.MarshalIndent(v, "", "    ")
   216  		}
   217  	}
   218  	return FormatFunc(func(r *Record) []byte {
   219  		props := make(map[string]interface{})
   220  
   221  		props[r.KeyNames.Time] = r.Time
   222  		props[r.KeyNames.Lvl] = r.Lvl.String()
   223  		props[r.KeyNames.Msg] = r.Msg
   224  
   225  		ctx := make([]string, len(r.Ctx))
   226  		for i := 0; i < len(r.Ctx); i += 2 {
   227  			k, ok := r.Ctx[i].(string)
   228  			if !ok {
   229  				props[errorKey] = fmt.Sprintf("%+v is not a string key,", r.Ctx[i])
   230  			}
   231  			ctx[i] = k
   232  			ctx[i+1] = formatLogfmtValue(r.Ctx[i+1], true)
   233  		}
   234  		props[r.KeyNames.Ctx] = ctx
   235  
   236  		b, err := jsonMarshal(props)
   237  		if err != nil {
   238  			b, _ = jsonMarshal(map[string]string{
   239  				errorKey: err.Error(),
   240  			})
   241  			return b
   242  		}
   243  		if lineSeparated {
   244  			b = append(b, '\n')
   245  		}
   246  		return b
   247  	})
   248  }
   249  
   250  // JSONFormatEx formats log records as JSON objects. If pretty is true,
   251  // records will be pretty-printed. If lineSeparated is true, records
   252  // will be logged with a new line between each record.
   253  func JSONFormatEx(pretty, lineSeparated bool) Format {
   254  	jsonMarshal := json.Marshal
   255  	if pretty {
   256  		jsonMarshal = func(v interface{}) ([]byte, error) {
   257  			return json.MarshalIndent(v, "", "    ")
   258  		}
   259  	}
   260  
   261  	return FormatFunc(func(r *Record) []byte {
   262  		props := make(map[string]interface{})
   263  
   264  		props[r.KeyNames.Time] = r.Time
   265  		props[r.KeyNames.Lvl] = r.Lvl.String()
   266  		props[r.KeyNames.Msg] = r.Msg
   267  
   268  		for i := 0; i < len(r.Ctx); i += 2 {
   269  			k, ok := r.Ctx[i].(string)
   270  			if !ok {
   271  				props[errorKey] = fmt.Sprintf("%+v is not a string key", r.Ctx[i])
   272  			}
   273  			props[k] = formatJSONValue(r.Ctx[i+1])
   274  		}
   275  
   276  		b, err := jsonMarshal(props)
   277  		if err != nil {
   278  			b, _ = jsonMarshal(map[string]string{
   279  				errorKey: err.Error(),
   280  			})
   281  			return b
   282  		}
   283  
   284  		if lineSeparated {
   285  			b = append(b, '\n')
   286  		}
   287  
   288  		return b
   289  	})
   290  }
   291  
   292  func formatShared(value interface{}) (result interface{}) {
   293  	defer func() {
   294  		if err := recover(); err != nil {
   295  			if v := reflect.ValueOf(value); v.Kind() == reflect.Ptr && v.IsNil() {
   296  				result = "nil"
   297  			} else {
   298  				panic(err)
   299  			}
   300  		}
   301  	}()
   302  
   303  	switch v := value.(type) {
   304  	case time.Time:
   305  		return v.Format(timeFormat)
   306  
   307  	case error:
   308  		return v.Error()
   309  
   310  	case fmt.Stringer:
   311  		return v.String()
   312  
   313  	default:
   314  		return v
   315  	}
   316  }
   317  
   318  func formatJSONValue(value interface{}) interface{} {
   319  	value = formatShared(value)
   320  	switch value.(type) {
   321  	case int, int8, int16, int32, int64, float32, float64, uint, uint8, uint16, uint32, uint64, string:
   322  		return value
   323  	default:
   324  		return fmt.Sprintf("%+v", value)
   325  	}
   326  }
   327  
   328  // formatValue formats a value for serialization
   329  func formatLogfmtValue(value interface{}, term bool) string {
   330  	if value == nil {
   331  		return "nil"
   332  	}
   333  
   334  	switch v := value.(type) {
   335  	case time.Time:
   336  		// Performance optimization: No need for escaping since the provided
   337  		// timeFormat doesn't have any escape characters, and escaping is
   338  		// expensive.
   339  		return v.Format(timeFormat)
   340  
   341  	case *big.Int:
   342  		// Big ints get consumed by the Stringer clause so we need to handle
   343  		// them earlier on.
   344  		if v == nil {
   345  			return "<nil>"
   346  		}
   347  		return formatLogfmtBigInt(v)
   348  	}
   349  	if term {
   350  		if s, ok := value.(TerminalStringer); ok {
   351  			// Custom terminal stringer provided, use that
   352  			return escapeString(s.TerminalString())
   353  		}
   354  	}
   355  	value = formatShared(value)
   356  	switch v := value.(type) {
   357  	case bool:
   358  		return strconv.FormatBool(v)
   359  	case float32:
   360  		return strconv.FormatFloat(float64(v), floatFormat, 3, 64)
   361  	case float64:
   362  		return strconv.FormatFloat(v, floatFormat, 3, 64)
   363  	case int8:
   364  		return strconv.FormatInt(int64(v), 10)
   365  	case uint8:
   366  		return strconv.FormatInt(int64(v), 10)
   367  	case int16:
   368  		return strconv.FormatInt(int64(v), 10)
   369  	case uint16:
   370  		return strconv.FormatInt(int64(v), 10)
   371  	// Larger integers get thousands separators.
   372  	case int:
   373  		return FormatLogfmtInt64(int64(v))
   374  	case int32:
   375  		return FormatLogfmtInt64(int64(v))
   376  	case int64:
   377  		return FormatLogfmtInt64(v)
   378  	case uint:
   379  		return FormatLogfmtUint64(uint64(v))
   380  	case uint32:
   381  		return FormatLogfmtUint64(uint64(v))
   382  	case uint64:
   383  		return FormatLogfmtUint64(v)
   384  	case string:
   385  		return escapeString(v)
   386  	default:
   387  		return escapeString(fmt.Sprintf("%+v", value))
   388  	}
   389  }
   390  
   391  // FormatLogfmtInt64 formats n with thousand separators.
   392  func FormatLogfmtInt64(n int64) string {
   393  	if n < 0 {
   394  		return formatLogfmtUint64(uint64(-n), true)
   395  	}
   396  	return formatLogfmtUint64(uint64(n), false)
   397  }
   398  
   399  // FormatLogfmtUint64 formats n with thousand separators.
   400  func FormatLogfmtUint64(n uint64) string {
   401  	return formatLogfmtUint64(n, false)
   402  }
   403  
   404  func formatLogfmtUint64(n uint64, neg bool) string {
   405  	// Small numbers are fine as is
   406  	if n < 100000 {
   407  		if neg {
   408  			return strconv.Itoa(-int(n))
   409  		} else {
   410  			return strconv.Itoa(int(n))
   411  		}
   412  	}
   413  	// Large numbers should be split
   414  	const maxLength = 26
   415  
   416  	var (
   417  		out   = make([]byte, maxLength)
   418  		i     = maxLength - 1
   419  		comma = 0
   420  	)
   421  	for ; n > 0; i-- {
   422  		if comma == 3 {
   423  			comma = 0
   424  			out[i] = ','
   425  		} else {
   426  			comma++
   427  			out[i] = '0' + byte(n%10)
   428  			n /= 10
   429  		}
   430  	}
   431  	if neg {
   432  		out[i] = '-'
   433  		i--
   434  	}
   435  	return string(out[i+1:])
   436  }
   437  
   438  // formatLogfmtBigInt formats n with thousand separators.
   439  func formatLogfmtBigInt(n *big.Int) string {
   440  	if n.IsUint64() {
   441  		return FormatLogfmtUint64(n.Uint64())
   442  	}
   443  	if n.IsInt64() {
   444  		return FormatLogfmtInt64(n.Int64())
   445  	}
   446  
   447  	var (
   448  		text  = n.String()
   449  		buf   = make([]byte, len(text)+len(text)/3)
   450  		comma = 0
   451  		i     = len(buf) - 1
   452  	)
   453  	for j := len(text) - 1; j >= 0; j, i = j-1, i-1 {
   454  		c := text[j]
   455  
   456  		switch {
   457  		case c == '-':
   458  			buf[i] = c
   459  		case comma == 3:
   460  			buf[i] = ','
   461  			i--
   462  			comma = 0
   463  			fallthrough
   464  		default:
   465  			buf[i] = c
   466  			comma++
   467  		}
   468  	}
   469  	return string(buf[i+1:])
   470  }
   471  
   472  // escapeString checks if the provided string needs escaping/quoting, and
   473  // calls strconv.Quote if needed
   474  func escapeString(s string) string {
   475  	needsQuoting := false
   476  	for _, r := range s {
   477  		// We quote everything below " (0x22) and above~ (0x7E), plus equal-sign
   478  		if r <= '"' || r > '~' || r == '=' {
   479  			needsQuoting = true
   480  			break
   481  		}
   482  	}
   483  	if !needsQuoting {
   484  		return s
   485  	}
   486  	return strconv.Quote(s)
   487  }
   488  
   489  // escapeMessage checks if the provided string needs escaping/quoting, similarly
   490  // to escapeString. The difference is that this method is more lenient: it allows
   491  // for spaces and linebreaks to occur without needing quoting.
   492  func escapeMessage(s string) string {
   493  	needsQuoting := false
   494  	for _, r := range s {
   495  		// Allow CR/LF/TAB. This is to make multi-line messages work.
   496  		if r == '\r' || r == '\n' || r == '\t' {
   497  			continue
   498  		}
   499  		// We quote everything below <space> (0x20) and above~ (0x7E),
   500  		// plus equal-sign
   501  		if r < ' ' || r > '~' || r == '=' {
   502  			needsQuoting = true
   503  			break
   504  		}
   505  	}
   506  	if !needsQuoting {
   507  		return s
   508  	}
   509  	return strconv.Quote(s)
   510  }