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