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