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