github.1485827954.workers.dev/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 }