github.com/ethereum/go-ethereum@v1.14.3/log/format.go (about)

     1  package log
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"log/slog"
     7  	"math/big"
     8  	"reflect"
     9  	"strconv"
    10  	"time"
    11  	"unicode/utf8"
    12  
    13  	"github.com/holiman/uint256"
    14  )
    15  
    16  const (
    17  	timeFormat        = "2006-01-02T15:04:05-0700"
    18  	floatFormat       = 'f'
    19  	termMsgJust       = 40
    20  	termCtxMaxPadding = 40
    21  )
    22  
    23  // 40 spaces
    24  var spaces = []byte("                                        ")
    25  
    26  // TerminalStringer is an analogous interface to the stdlib stringer, allowing
    27  // own types to have custom shortened serialization formats when printed to the
    28  // screen.
    29  type TerminalStringer interface {
    30  	TerminalString() string
    31  }
    32  
    33  func (h *TerminalHandler) format(buf []byte, r slog.Record, usecolor bool) []byte {
    34  	msg := escapeMessage(r.Message)
    35  	var color = ""
    36  	if usecolor {
    37  		switch r.Level {
    38  		case LevelCrit:
    39  			color = "\x1b[35m"
    40  		case slog.LevelError:
    41  			color = "\x1b[31m"
    42  		case slog.LevelWarn:
    43  			color = "\x1b[33m"
    44  		case slog.LevelInfo:
    45  			color = "\x1b[32m"
    46  		case slog.LevelDebug:
    47  			color = "\x1b[36m"
    48  		case LevelTrace:
    49  			color = "\x1b[34m"
    50  		}
    51  	}
    52  	if buf == nil {
    53  		buf = make([]byte, 0, 30+termMsgJust)
    54  	}
    55  	b := bytes.NewBuffer(buf)
    56  
    57  	if color != "" { // Start color
    58  		b.WriteString(color)
    59  		b.WriteString(LevelAlignedString(r.Level))
    60  		b.WriteString("\x1b[0m")
    61  	} else {
    62  		b.WriteString(LevelAlignedString(r.Level))
    63  	}
    64  	b.WriteString("[")
    65  	writeTimeTermFormat(b, r.Time)
    66  	b.WriteString("] ")
    67  	b.WriteString(msg)
    68  
    69  	// try to justify the log output for short messages
    70  	//length := utf8.RuneCountInString(msg)
    71  	length := len(msg)
    72  	if (r.NumAttrs()+len(h.attrs)) > 0 && length < termMsgJust {
    73  		b.Write(spaces[:termMsgJust-length])
    74  	}
    75  	// print the attributes
    76  	h.formatAttributes(b, r, color)
    77  
    78  	return b.Bytes()
    79  }
    80  
    81  func (h *TerminalHandler) formatAttributes(buf *bytes.Buffer, r slog.Record, color string) {
    82  	writeAttr := func(attr slog.Attr, first, last bool) {
    83  		buf.WriteByte(' ')
    84  
    85  		if color != "" {
    86  			buf.WriteString(color)
    87  			buf.Write(appendEscapeString(buf.AvailableBuffer(), attr.Key))
    88  			buf.WriteString("\x1b[0m=")
    89  		} else {
    90  			buf.Write(appendEscapeString(buf.AvailableBuffer(), attr.Key))
    91  			buf.WriteByte('=')
    92  		}
    93  		val := FormatSlogValue(attr.Value, buf.AvailableBuffer())
    94  
    95  		padding := h.fieldPadding[attr.Key]
    96  
    97  		length := utf8.RuneCount(val)
    98  		if padding < length && length <= termCtxMaxPadding {
    99  			padding = length
   100  			h.fieldPadding[attr.Key] = padding
   101  		}
   102  		buf.Write(val)
   103  		if !last && padding > length {
   104  			buf.Write(spaces[:padding-length])
   105  		}
   106  	}
   107  	var n = 0
   108  	var nAttrs = len(h.attrs) + r.NumAttrs()
   109  	for _, attr := range h.attrs {
   110  		writeAttr(attr, n == 0, n == nAttrs-1)
   111  		n++
   112  	}
   113  	r.Attrs(func(attr slog.Attr) bool {
   114  		writeAttr(attr, n == 0, n == nAttrs-1)
   115  		n++
   116  		return true
   117  	})
   118  	buf.WriteByte('\n')
   119  }
   120  
   121  // FormatSlogValue formats a slog.Value for serialization to terminal.
   122  func FormatSlogValue(v slog.Value, tmp []byte) (result []byte) {
   123  	var value any
   124  	defer func() {
   125  		if err := recover(); err != nil {
   126  			if v := reflect.ValueOf(value); v.Kind() == reflect.Ptr && v.IsNil() {
   127  				result = []byte("<nil>")
   128  			} else {
   129  				panic(err)
   130  			}
   131  		}
   132  	}()
   133  
   134  	switch v.Kind() {
   135  	case slog.KindString:
   136  		return appendEscapeString(tmp, v.String())
   137  	case slog.KindInt64: // All int-types (int8, int16 etc) wind up here
   138  		return appendInt64(tmp, v.Int64())
   139  	case slog.KindUint64: // All uint-types (uint8, uint16 etc) wind up here
   140  		return appendUint64(tmp, v.Uint64(), false)
   141  	case slog.KindFloat64:
   142  		return strconv.AppendFloat(tmp, v.Float64(), floatFormat, 3, 64)
   143  	case slog.KindBool:
   144  		return strconv.AppendBool(tmp, v.Bool())
   145  	case slog.KindDuration:
   146  		value = v.Duration()
   147  	case slog.KindTime:
   148  		// Performance optimization: No need for escaping since the provided
   149  		// timeFormat doesn't have any escape characters, and escaping is
   150  		// expensive.
   151  		return v.Time().AppendFormat(tmp, timeFormat)
   152  	default:
   153  		value = v.Any()
   154  	}
   155  	if value == nil {
   156  		return []byte("<nil>")
   157  	}
   158  	switch v := value.(type) {
   159  	case *big.Int: // Need to be before fmt.Stringer-clause
   160  		return appendBigInt(tmp, v)
   161  	case *uint256.Int: // Need to be before fmt.Stringer-clause
   162  		return appendU256(tmp, v)
   163  	case error:
   164  		return appendEscapeString(tmp, v.Error())
   165  	case TerminalStringer:
   166  		return appendEscapeString(tmp, v.TerminalString())
   167  	case fmt.Stringer:
   168  		return appendEscapeString(tmp, v.String())
   169  	}
   170  
   171  	// We can use the 'tmp' as a scratch-buffer, to first format the
   172  	// value, and in a second step do escaping.
   173  	internal := fmt.Appendf(tmp, "%+v", value)
   174  	return appendEscapeString(tmp, string(internal))
   175  }
   176  
   177  // appendInt64 formats n with thousand separators and writes into buffer dst.
   178  func appendInt64(dst []byte, n int64) []byte {
   179  	if n < 0 {
   180  		return appendUint64(dst, uint64(-n), true)
   181  	}
   182  	return appendUint64(dst, uint64(n), false)
   183  }
   184  
   185  // appendUint64 formats n with thousand separators and writes into buffer dst.
   186  func appendUint64(dst []byte, n uint64, neg bool) []byte {
   187  	// Small numbers are fine as is
   188  	if n < 100000 {
   189  		if neg {
   190  			return strconv.AppendInt(dst, -int64(n), 10)
   191  		} else {
   192  			return strconv.AppendInt(dst, int64(n), 10)
   193  		}
   194  	}
   195  	// Large numbers should be split
   196  	const maxLength = 26
   197  
   198  	var (
   199  		out   = make([]byte, maxLength)
   200  		i     = maxLength - 1
   201  		comma = 0
   202  	)
   203  	for ; n > 0; i-- {
   204  		if comma == 3 {
   205  			comma = 0
   206  			out[i] = ','
   207  		} else {
   208  			comma++
   209  			out[i] = '0' + byte(n%10)
   210  			n /= 10
   211  		}
   212  	}
   213  	if neg {
   214  		out[i] = '-'
   215  		i--
   216  	}
   217  	return append(dst, out[i+1:]...)
   218  }
   219  
   220  // FormatLogfmtUint64 formats n with thousand separators.
   221  func FormatLogfmtUint64(n uint64) string {
   222  	return string(appendUint64(nil, n, false))
   223  }
   224  
   225  // appendBigInt formats n with thousand separators and writes to dst.
   226  func appendBigInt(dst []byte, n *big.Int) []byte {
   227  	if n.IsUint64() {
   228  		return appendUint64(dst, n.Uint64(), false)
   229  	}
   230  	if n.IsInt64() {
   231  		return appendInt64(dst, n.Int64())
   232  	}
   233  
   234  	var (
   235  		text  = n.String()
   236  		buf   = make([]byte, len(text)+len(text)/3)
   237  		comma = 0
   238  		i     = len(buf) - 1
   239  	)
   240  	for j := len(text) - 1; j >= 0; j, i = j-1, i-1 {
   241  		c := text[j]
   242  
   243  		switch {
   244  		case c == '-':
   245  			buf[i] = c
   246  		case comma == 3:
   247  			buf[i] = ','
   248  			i--
   249  			comma = 0
   250  			fallthrough
   251  		default:
   252  			buf[i] = c
   253  			comma++
   254  		}
   255  	}
   256  	return append(dst, buf[i+1:]...)
   257  }
   258  
   259  // appendU256 formats n with thousand separators.
   260  func appendU256(dst []byte, n *uint256.Int) []byte {
   261  	if n.IsUint64() {
   262  		return appendUint64(dst, n.Uint64(), false)
   263  	}
   264  	res := []byte(n.PrettyDec(','))
   265  	return append(dst, res...)
   266  }
   267  
   268  // appendEscapeString writes the string s to the given writer, with
   269  // escaping/quoting if needed.
   270  func appendEscapeString(dst []byte, s string) []byte {
   271  	needsQuoting := false
   272  	needsEscaping := false
   273  	for _, r := range s {
   274  		// If it contains spaces or equal-sign, we need to quote it.
   275  		if r == ' ' || r == '=' {
   276  			needsQuoting = true
   277  			continue
   278  		}
   279  		// We need to escape it, if it contains
   280  		// - character " (0x22) and lower (except space)
   281  		// - characters above ~ (0x7E), plus equal-sign
   282  		if r <= '"' || r > '~' {
   283  			needsEscaping = true
   284  			break
   285  		}
   286  	}
   287  	if needsEscaping {
   288  		return strconv.AppendQuote(dst, s)
   289  	}
   290  	// No escaping needed, but we might have to place within quote-marks, in case
   291  	// it contained a space
   292  	if needsQuoting {
   293  		dst = append(dst, '"')
   294  		dst = append(dst, []byte(s)...)
   295  		return append(dst, '"')
   296  	}
   297  	return append(dst, []byte(s)...)
   298  }
   299  
   300  // escapeMessage checks if the provided string needs escaping/quoting, similarly
   301  // to escapeString. The difference is that this method is more lenient: it allows
   302  // for spaces and linebreaks to occur without needing quoting.
   303  func escapeMessage(s string) string {
   304  	needsQuoting := false
   305  	for _, r := range s {
   306  		// Allow CR/LF/TAB. This is to make multi-line messages work.
   307  		if r == '\r' || r == '\n' || r == '\t' {
   308  			continue
   309  		}
   310  		// We quote everything below <space> (0x20) and above~ (0x7E),
   311  		// plus equal-sign
   312  		if r < ' ' || r > '~' || r == '=' {
   313  			needsQuoting = true
   314  			break
   315  		}
   316  	}
   317  	if !needsQuoting {
   318  		return s
   319  	}
   320  	return strconv.Quote(s)
   321  }
   322  
   323  // writeTimeTermFormat writes on the format "01-02|15:04:05.000"
   324  func writeTimeTermFormat(buf *bytes.Buffer, t time.Time) {
   325  	_, month, day := t.Date()
   326  	writePosIntWidth(buf, int(month), 2)
   327  	buf.WriteByte('-')
   328  	writePosIntWidth(buf, day, 2)
   329  	buf.WriteByte('|')
   330  	hour, min, sec := t.Clock()
   331  	writePosIntWidth(buf, hour, 2)
   332  	buf.WriteByte(':')
   333  	writePosIntWidth(buf, min, 2)
   334  	buf.WriteByte(':')
   335  	writePosIntWidth(buf, sec, 2)
   336  	ns := t.Nanosecond()
   337  	buf.WriteByte('.')
   338  	writePosIntWidth(buf, ns/1e6, 3)
   339  }
   340  
   341  // writePosIntWidth writes non-negative integer i to the buffer, padded on the left
   342  // by zeroes to the given width. Use a width of 0 to omit padding.
   343  // Adapted from pkg.go.dev/log/slog/internal/buffer
   344  func writePosIntWidth(b *bytes.Buffer, i, width int) {
   345  	// Cheap integer to fixed-width decimal ASCII.
   346  	// Copied from log/log.go.
   347  	if i < 0 {
   348  		panic("negative int")
   349  	}
   350  	// Assemble decimal in reverse order.
   351  	var bb [20]byte
   352  	bp := len(bb) - 1
   353  	for i >= 10 || width > 1 {
   354  		width--
   355  		q := i / 10
   356  		bb[bp] = byte('0' + i - q*10)
   357  		bp--
   358  		i = q
   359  	}
   360  	// i < 10
   361  	bb[bp] = byte('0' + i)
   362  	b.Write(bb[bp:])
   363  }