github.com/Tenderly/zkevm-erigon@v1.9.7/log/format.go (about) 1 package log 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "reflect" 8 "strconv" 9 "strings" 10 "sync" 11 "sync/atomic" 12 "time" 13 "unicode/utf8" 14 ) 15 16 const ( 17 timeFormat = "2006-01-02T15:04:05-0700" 18 termTimeFormat = "01-02|15:04:05.000" 19 floatFormat = 'f' 20 termMsgJust = 40 21 termCtxMaxPadding = 40 22 ) 23 24 // locationTrims are trimmed for display to avoid unwieldy log lines. 25 var locationTrims = []string{ 26 "github.com/ethereum/go-ethereum/", 27 } 28 29 // PrintOrigins sets or unsets log location (file:line) printing for terminal 30 // format output. 31 func PrintOrigins(print bool) { 32 if print { 33 atomic.StoreUint32(&locationEnabled, 1) 34 } else { 35 atomic.StoreUint32(&locationEnabled, 0) 36 } 37 } 38 39 // locationEnabled is an atomic flag controlling whether the terminal formatter 40 // should append the log locations too when printing entries. 41 var locationEnabled uint32 42 43 // locationLength is the maxmimum path length encountered, which all logs are 44 // padded to to aid in alignment. 45 var locationLength uint32 46 47 // fieldPadding is a global map with maximum field value lengths seen until now 48 // to allow padding log contexts in a bit smarter way. 49 var fieldPadding = make(map[string]int) 50 51 // fieldPaddingLock is a global mutex protecting the field padding map. 52 var fieldPaddingLock sync.RWMutex 53 54 type Format interface { 55 Format(r *Record) []byte 56 } 57 58 // FormatFunc returns a new Format object which uses 59 // the given function to perform record formatting. 60 func FormatFunc(f func(*Record) []byte) Format { 61 return formatFunc(f) 62 } 63 64 type formatFunc func(*Record) []byte 65 66 func (f formatFunc) Format(r *Record) []byte { 67 return f(r) 68 } 69 70 // TerminalStringer is an analogous interface to the stdlib stringer, allowing 71 // own types to have custom shortened serialization formats when printed to the 72 // screen. 73 type TerminalStringer interface { 74 TerminalString() string 75 } 76 77 // TerminalFormat formats log records optimized for human readability on 78 // a terminal with color-coded level output and terser human friendly timestamp. 79 // This format should only be used for interactive programs or while developing. 80 // 81 // [LEVEL] [TIME] MESAGE key=value key=value ... 82 // 83 // Example: 84 // 85 // [DBUG] [May 16 20:58:45] remove route ns=haproxy addr=127.0.0.1:50002 86 // 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 // 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 } 172 173 // XXX: we should probably check that all of your key bytes aren't invalid 174 fieldPaddingLock.RLock() 175 padding := fieldPadding[k] 176 fieldPaddingLock.RUnlock() 177 178 length := utf8.RuneCountInString(v) 179 if padding < length && length <= termCtxMaxPadding { 180 padding = length 181 182 fieldPaddingLock.Lock() 183 fieldPadding[k] = padding 184 fieldPaddingLock.Unlock() 185 } 186 if color > 0 { 187 fmt.Fprintf(buf, "\x1b[%dm%s\x1b[0m=", color, k) 188 } else { 189 buf.WriteString(k) 190 buf.WriteByte('=') 191 } 192 buf.WriteString(v) 193 if i < len(ctx)-2 && padding > length { 194 buf.Write(bytes.Repeat([]byte{' '}, padding-length)) 195 } 196 } 197 buf.WriteByte('\n') 198 } 199 200 // JSONFormat formats log records as JSON objects separated by newlines. 201 // It is the equivalent of JSONFormatEx(false, true). 202 func JSONFormat() Format { 203 return JSONFormatEx(false, true) 204 } 205 206 // JSONFormatOrderedEx formats log records as JSON arrays. If pretty is true, 207 // records will be pretty-printed. If lineSeparated is true, records 208 // will be logged with a new line between each record. 209 func JSONFormatOrderedEx(pretty, lineSeparated bool) Format { 210 jsonMarshal := json.Marshal 211 if pretty { 212 jsonMarshal = func(v interface{}) ([]byte, error) { 213 return json.MarshalIndent(v, "", " ") 214 } 215 } 216 return FormatFunc(func(r *Record) []byte { 217 props := make(map[string]interface{}) 218 219 props[r.KeyNames.Time] = r.Time 220 props[r.KeyNames.Lvl] = r.Lvl.String() 221 props[r.KeyNames.Msg] = r.Msg 222 223 ctx := make([]string, len(r.Ctx)) 224 for i := 0; i < len(r.Ctx); i += 2 { 225 k, ok := r.Ctx[i].(string) 226 if !ok { 227 props[errorKey] = fmt.Sprintf("%+v is not a string key,", r.Ctx[i]) 228 } 229 ctx[i] = k 230 ctx[i+1] = formatLogfmtValue(r.Ctx[i+1], true) 231 } 232 props[r.KeyNames.Ctx] = ctx 233 234 b, err := jsonMarshal(props) 235 if err != nil { 236 b, _ = jsonMarshal(map[string]string{ 237 errorKey: err.Error(), 238 }) 239 return b 240 } 241 if lineSeparated { 242 b = append(b, '\n') 243 } 244 return b 245 }) 246 } 247 248 // JSONFormatEx formats log records as JSON objects. If pretty is true, 249 // records will be pretty-printed. If lineSeparated is true, records 250 // will be logged with a new line between each record. 251 func JSONFormatEx(pretty, lineSeparated bool) Format { 252 jsonMarshal := json.Marshal 253 if pretty { 254 jsonMarshal = func(v interface{}) ([]byte, error) { 255 return json.MarshalIndent(v, "", " ") 256 } 257 } 258 259 return FormatFunc(func(r *Record) []byte { 260 props := make(map[string]interface{}) 261 262 props[r.KeyNames.Time] = r.Time 263 props[r.KeyNames.Lvl] = r.Lvl.String() 264 props[r.KeyNames.Msg] = r.Msg 265 266 for i := 0; i < len(r.Ctx); i += 2 { 267 k, ok := r.Ctx[i].(string) 268 if !ok { 269 props[errorKey] = fmt.Sprintf("%+v is not a string key", r.Ctx[i]) 270 } 271 props[k] = formatJSONValue(r.Ctx[i+1]) 272 } 273 274 b, err := jsonMarshal(props) 275 if err != nil { 276 b, _ = jsonMarshal(map[string]string{ 277 errorKey: err.Error(), 278 }) 279 return b 280 } 281 282 if lineSeparated { 283 b = append(b, '\n') 284 } 285 286 return b 287 }) 288 } 289 290 func formatShared(value interface{}) (result interface{}) { 291 defer func() { 292 if err := recover(); err != nil { 293 if v := reflect.ValueOf(value); v.Kind() == reflect.Ptr && v.IsNil() { 294 result = "nil" 295 } else { 296 panic(err) 297 } 298 } 299 }() 300 301 switch v := value.(type) { 302 case time.Time: 303 return v.Format(timeFormat) 304 305 case error: 306 return v.Error() 307 308 case fmt.Stringer: 309 return v.String() 310 311 default: 312 return v 313 } 314 } 315 316 func formatJSONValue(value interface{}) interface{} { 317 value = formatShared(value) 318 switch value.(type) { 319 case int, int8, int16, int32, int64, float32, float64, uint, uint8, uint16, uint32, uint64, string: 320 return value 321 default: 322 return fmt.Sprintf("%+v", value) 323 } 324 } 325 326 // formatValue formats a value for serialization 327 func formatLogfmtValue(value interface{}, term bool) string { 328 if value == nil { 329 return "nil" 330 } 331 332 if t, ok := value.(time.Time); ok { 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 t.Format(timeFormat) 337 } 338 if term { 339 if s, ok := value.(TerminalStringer); ok { 340 // Custom terminal stringer provided, use that 341 return escapeString(s.TerminalString()) 342 } 343 } 344 value = formatShared(value) 345 switch v := value.(type) { 346 case bool: 347 return strconv.FormatBool(v) 348 case float32: 349 return strconv.FormatFloat(float64(v), floatFormat, 3, 64) 350 case float64: 351 return strconv.FormatFloat(v, floatFormat, 3, 64) 352 case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: 353 return fmt.Sprintf("%d", value) 354 case string: 355 return escapeString(v) 356 default: 357 return escapeString(fmt.Sprintf("%+v", value)) 358 } 359 } 360 361 var stringBufPool = sync.Pool{ 362 New: func() interface{} { return new(bytes.Buffer) }, 363 } 364 365 func escapeString(s string) string { 366 needsQuotes := false 367 needsEscape := false 368 for _, r := range s { 369 if r <= ' ' || r == '=' || r == '"' { 370 needsQuotes = true 371 } 372 if r == '\\' || r == '"' || r == '\n' || r == '\r' || r == '\t' { 373 needsEscape = true 374 } 375 } 376 if !needsEscape && !needsQuotes { 377 return s 378 } 379 e := stringBufPool.Get().(*bytes.Buffer) 380 e.WriteByte('"') 381 for _, r := range s { 382 switch r { 383 case '\\', '"': 384 e.WriteByte('\\') 385 e.WriteByte(byte(r)) 386 case '\n': 387 e.WriteString("\\n") 388 case '\r': 389 e.WriteString("\\r") 390 case '\t': 391 e.WriteString("\\t") 392 default: 393 e.WriteRune(r) 394 } 395 } 396 e.WriteByte('"') 397 var ret string 398 if needsQuotes { 399 ret = e.String() 400 } else { 401 ret = string(e.Bytes()[1 : e.Len()-1]) 402 } 403 e.Reset() 404 stringBufPool.Put(e) 405 return ret 406 }