github.com/ethw3/go-ethereuma@v0.0.0-20221013053120-c14602a4c23c/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/ethw3/go-ethereuma/", 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 // 88 func TerminalFormat(usecolor bool) Format { 89 return FormatFunc(func(r *Record) []byte { 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, r.Msg) 127 } else { 128 fmt.Fprintf(b, "%s[%s|%s]%s %s ", lvl, r.Time.Format(termTimeFormat), location, padding, r.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), r.Msg) 133 } else { 134 fmt.Fprintf(b, "%s[%s] %s ", lvl, r.Time.Format(termTimeFormat), r.Msg) 135 } 136 } 137 // try to justify the log output for short messages 138 length := utf8.RuneCountInString(r.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 // 153 func LogfmtFormat() Format { 154 return FormatFunc(func(r *Record) []byte { 155 common := []interface{}{r.KeyNames.Time, r.Time, r.KeyNames.Lvl, r.Lvl, r.KeyNames.Msg, r.Msg} 156 buf := &bytes.Buffer{} 157 logfmt(buf, append(common, r.Ctx...), 0, false) 158 return buf.Bytes() 159 }) 160 } 161 162 func logfmt(buf *bytes.Buffer, ctx []interface{}, color int, term bool) { 163 for i := 0; i < len(ctx); i += 2 { 164 if i != 0 { 165 buf.WriteByte(' ') 166 } 167 168 k, ok := ctx[i].(string) 169 v := formatLogfmtValue(ctx[i+1], term) 170 if !ok { 171 k, v = errorKey, formatLogfmtValue(k, term) 172 } 173 174 // XXX: we should probably check that all of your key bytes aren't invalid 175 fieldPaddingLock.RLock() 176 padding := fieldPadding[k] 177 fieldPaddingLock.RUnlock() 178 179 length := utf8.RuneCountInString(v) 180 if padding < length && length <= termCtxMaxPadding { 181 padding = length 182 183 fieldPaddingLock.Lock() 184 fieldPadding[k] = padding 185 fieldPaddingLock.Unlock() 186 } 187 if color > 0 { 188 fmt.Fprintf(buf, "\x1b[%dm%s\x1b[0m=", color, k) 189 } else { 190 buf.WriteString(k) 191 buf.WriteByte('=') 192 } 193 buf.WriteString(v) 194 if i < len(ctx)-2 && padding > length { 195 buf.Write(bytes.Repeat([]byte{' '}, padding-length)) 196 } 197 } 198 buf.WriteByte('\n') 199 } 200 201 // JSONFormat formats log records as JSON objects separated by newlines. 202 // It is the equivalent of JSONFormatEx(false, true). 203 func JSONFormat() Format { 204 return JSONFormatEx(false, true) 205 } 206 207 // JSONFormatOrderedEx formats log records as JSON arrays. If pretty is true, 208 // records will be pretty-printed. If lineSeparated is true, records 209 // will be logged with a new line between each record. 210 func JSONFormatOrderedEx(pretty, lineSeparated bool) Format { 211 jsonMarshal := json.Marshal 212 if pretty { 213 jsonMarshal = func(v interface{}) ([]byte, error) { 214 return json.MarshalIndent(v, "", " ") 215 } 216 } 217 return FormatFunc(func(r *Record) []byte { 218 props := make(map[string]interface{}) 219 220 props[r.KeyNames.Time] = r.Time 221 props[r.KeyNames.Lvl] = r.Lvl.String() 222 props[r.KeyNames.Msg] = r.Msg 223 224 ctx := make([]string, len(r.Ctx)) 225 for i := 0; i < len(r.Ctx); i += 2 { 226 k, ok := r.Ctx[i].(string) 227 if !ok { 228 props[errorKey] = fmt.Sprintf("%+v is not a string key,", r.Ctx[i]) 229 } 230 ctx[i] = k 231 ctx[i+1] = formatLogfmtValue(r.Ctx[i+1], true) 232 } 233 props[r.KeyNames.Ctx] = ctx 234 235 b, err := jsonMarshal(props) 236 if err != nil { 237 b, _ = jsonMarshal(map[string]string{ 238 errorKey: err.Error(), 239 }) 240 return b 241 } 242 if lineSeparated { 243 b = append(b, '\n') 244 } 245 return b 246 }) 247 } 248 249 // JSONFormatEx formats log records as JSON objects. If pretty is true, 250 // records will be pretty-printed. If lineSeparated is true, records 251 // will be logged with a new line between each record. 252 func JSONFormatEx(pretty, lineSeparated bool) Format { 253 jsonMarshal := json.Marshal 254 if pretty { 255 jsonMarshal = func(v interface{}) ([]byte, error) { 256 return json.MarshalIndent(v, "", " ") 257 } 258 } 259 260 return FormatFunc(func(r *Record) []byte { 261 props := make(map[string]interface{}) 262 263 props[r.KeyNames.Time] = r.Time 264 props[r.KeyNames.Lvl] = r.Lvl.String() 265 props[r.KeyNames.Msg] = r.Msg 266 267 for i := 0; i < len(r.Ctx); i += 2 { 268 k, ok := r.Ctx[i].(string) 269 if !ok { 270 props[errorKey] = fmt.Sprintf("%+v is not a string key", r.Ctx[i]) 271 } 272 props[k] = formatJSONValue(r.Ctx[i+1]) 273 } 274 275 b, err := jsonMarshal(props) 276 if err != nil { 277 b, _ = jsonMarshal(map[string]string{ 278 errorKey: err.Error(), 279 }) 280 return b 281 } 282 283 if lineSeparated { 284 b = append(b, '\n') 285 } 286 287 return b 288 }) 289 } 290 291 func formatShared(value interface{}) (result interface{}) { 292 defer func() { 293 if err := recover(); err != nil { 294 if v := reflect.ValueOf(value); v.Kind() == reflect.Ptr && v.IsNil() { 295 result = "nil" 296 } else { 297 panic(err) 298 } 299 } 300 }() 301 302 switch v := value.(type) { 303 case time.Time: 304 return v.Format(timeFormat) 305 306 case error: 307 return v.Error() 308 309 case fmt.Stringer: 310 return v.String() 311 312 default: 313 return v 314 } 315 } 316 317 func formatJSONValue(value interface{}) interface{} { 318 value = formatShared(value) 319 switch value.(type) { 320 case int, int8, int16, int32, int64, float32, float64, uint, uint8, uint16, uint32, uint64, string: 321 return value 322 default: 323 return fmt.Sprintf("%+v", value) 324 } 325 } 326 327 // formatValue formats a value for serialization 328 func formatLogfmtValue(value interface{}, term bool) string { 329 if value == nil { 330 return "nil" 331 } 332 333 switch v := value.(type) { 334 case time.Time: 335 // Performance optimization: No need for escaping since the provided 336 // timeFormat doesn't have any escape characters, and escaping is 337 // expensive. 338 return v.Format(timeFormat) 339 340 case *big.Int: 341 // Big ints get consumed by the Stringer clause so we need to handle 342 // them earlier on. 343 if v == nil { 344 return "<nil>" 345 } 346 return formatLogfmtBigInt(v) 347 } 348 if term { 349 if s, ok := value.(TerminalStringer); ok { 350 // Custom terminal stringer provided, use that 351 return escapeString(s.TerminalString()) 352 } 353 } 354 value = formatShared(value) 355 switch v := value.(type) { 356 case bool: 357 return strconv.FormatBool(v) 358 case float32: 359 return strconv.FormatFloat(float64(v), floatFormat, 3, 64) 360 case float64: 361 return strconv.FormatFloat(v, floatFormat, 3, 64) 362 case int8: 363 return strconv.FormatInt(int64(v), 10) 364 case uint8: 365 return strconv.FormatInt(int64(v), 10) 366 case int16: 367 return strconv.FormatInt(int64(v), 10) 368 case uint16: 369 return strconv.FormatInt(int64(v), 10) 370 // Larger integers get thousands separators. 371 case int: 372 return FormatLogfmtInt64(int64(v)) 373 case int32: 374 return FormatLogfmtInt64(int64(v)) 375 case int64: 376 return FormatLogfmtInt64(v) 377 case uint: 378 return FormatLogfmtUint64(uint64(v)) 379 case uint32: 380 return FormatLogfmtUint64(uint64(v)) 381 case uint64: 382 return FormatLogfmtUint64(v) 383 case string: 384 return escapeString(v) 385 default: 386 return escapeString(fmt.Sprintf("%+v", value)) 387 } 388 } 389 390 // FormatLogfmtInt64 formats n with thousand separators. 391 func FormatLogfmtInt64(n int64) string { 392 if n < 0 { 393 return formatLogfmtUint64(uint64(-n), true) 394 } 395 return formatLogfmtUint64(uint64(n), false) 396 } 397 398 // FormatLogfmtUint64 formats n with thousand separators. 399 func FormatLogfmtUint64(n uint64) string { 400 return formatLogfmtUint64(n, false) 401 } 402 403 func formatLogfmtUint64(n uint64, neg bool) string { 404 // Small numbers are fine as is 405 if n < 100000 { 406 if neg { 407 return strconv.Itoa(-int(n)) 408 } else { 409 return strconv.Itoa(int(n)) 410 } 411 } 412 // Large numbers should be split 413 const maxLength = 26 414 415 var ( 416 out = make([]byte, maxLength) 417 i = maxLength - 1 418 comma = 0 419 ) 420 for ; n > 0; i-- { 421 if comma == 3 { 422 comma = 0 423 out[i] = ',' 424 } else { 425 comma++ 426 out[i] = '0' + byte(n%10) 427 n /= 10 428 } 429 } 430 if neg { 431 out[i] = '-' 432 i-- 433 } 434 return string(out[i+1:]) 435 } 436 437 // formatLogfmtBigInt formats n with thousand separators. 438 func formatLogfmtBigInt(n *big.Int) string { 439 if n.IsUint64() { 440 return FormatLogfmtUint64(n.Uint64()) 441 } 442 if n.IsInt64() { 443 return FormatLogfmtInt64(n.Int64()) 444 } 445 446 var ( 447 text = n.String() 448 buf = make([]byte, len(text)+len(text)/3) 449 comma = 0 450 i = len(buf) - 1 451 ) 452 for j := len(text) - 1; j >= 0; j, i = j-1, i-1 { 453 c := text[j] 454 455 switch { 456 case c == '-': 457 buf[i] = c 458 case comma == 3: 459 buf[i] = ',' 460 i-- 461 comma = 0 462 fallthrough 463 default: 464 buf[i] = c 465 comma++ 466 } 467 } 468 return string(buf[i+1:]) 469 } 470 471 // escapeString checks if the provided string needs escaping/quoting, and 472 // calls strconv.Quote if needed 473 func escapeString(s string) string { 474 needsQuoting := false 475 for _, r := range s { 476 // We quote everything below " (0x34) and above~ (0x7E), plus equal-sign 477 if r <= '"' || r > '~' || r == '=' { 478 needsQuoting = true 479 break 480 } 481 } 482 if !needsQuoting { 483 return s 484 } 485 return strconv.Quote(s) 486 }