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 }