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 }