github.com/lukso-network/go-ethereum@v1.8.22/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 ) 22 23 // locationTrims are trimmed for display to avoid unwieldy log lines. 24 var locationTrims = []string{ 25 "github.com/ethereum/go-ethereum/", 26 } 27 28 // PrintOrigins sets or unsets log location (file:line) printing for terminal 29 // format output. 30 func PrintOrigins(print bool) { 31 if print { 32 atomic.StoreUint32(&locationEnabled, 1) 33 } else { 34 atomic.StoreUint32(&locationEnabled, 0) 35 } 36 } 37 38 // locationEnabled is an atomic flag controlling whether the terminal formatter 39 // should append the log locations too when printing entries. 40 var locationEnabled uint32 41 42 // locationLength is the maxmimum path length encountered, which all logs are 43 // padded to to aid in alignment. 44 var locationLength uint32 45 46 // fieldPadding is a global map with maximum field value lengths seen until now 47 // to allow padding log contexts in a bit smarter way. 48 var fieldPadding = make(map[string]int) 49 50 // fieldPaddingLock is a global mutex protecting the field padding map. 51 var fieldPaddingLock sync.RWMutex 52 53 type Format interface { 54 Format(r *Record) []byte 55 } 56 57 // FormatFunc returns a new Format object which uses 58 // the given function to perform record formatting. 59 func FormatFunc(f func(*Record) []byte) Format { 60 return formatFunc(f) 61 } 62 63 type formatFunc func(*Record) []byte 64 65 func (f formatFunc) Format(r *Record) []byte { 66 return f(r) 67 } 68 69 // TerminalStringer is an analogous interface to the stdlib stringer, allowing 70 // own types to have custom shortened serialization formats when printed to the 71 // screen. 72 type TerminalStringer interface { 73 TerminalString() string 74 } 75 76 // TerminalFormat formats log records optimized for human readability on 77 // a terminal with color-coded level output and terser human friendly timestamp. 78 // This format should only be used for interactive programs or while developing. 79 // 80 // [LEVEL] [TIME] MESAGE key=value key=value ... 81 // 82 // Example: 83 // 84 // [DBUG] [May 16 20:58:45] remove route ns=haproxy addr=127.0.0.1:50002 85 // 86 func TerminalFormat(usecolor bool) Format { 87 return FormatFunc(func(r *Record) []byte { 88 var color = 0 89 if usecolor { 90 switch r.Lvl { 91 case LvlCrit: 92 color = 35 93 case LvlError: 94 color = 31 95 case LvlWarn: 96 color = 33 97 case LvlInfo: 98 color = 32 99 case LvlDebug: 100 color = 36 101 case LvlTrace: 102 color = 34 103 } 104 } 105 106 b := &bytes.Buffer{} 107 lvl := r.Lvl.AlignedString() 108 if atomic.LoadUint32(&locationEnabled) != 0 { 109 // Log origin printing was requested, format the location path and line number 110 location := fmt.Sprintf("%+v", r.Call) 111 for _, prefix := range locationTrims { 112 location = strings.TrimPrefix(location, prefix) 113 } 114 // Maintain the maximum location length for fancyer alignment 115 align := int(atomic.LoadUint32(&locationLength)) 116 if align < len(location) { 117 align = len(location) 118 atomic.StoreUint32(&locationLength, uint32(align)) 119 } 120 padding := strings.Repeat(" ", align-len(location)) 121 122 // Assemble and print the log heading 123 if color > 0 { 124 fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s|%s]%s %s ", color, lvl, r.Time.Format(termTimeFormat), location, padding, r.Msg) 125 } else { 126 fmt.Fprintf(b, "%s[%s|%s]%s %s ", lvl, r.Time.Format(termTimeFormat), location, padding, r.Msg) 127 } 128 } else { 129 if color > 0 { 130 fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s] %s ", color, lvl, r.Time.Format(termTimeFormat), r.Msg) 131 } else { 132 fmt.Fprintf(b, "%s[%s] %s ", lvl, r.Time.Format(termTimeFormat), r.Msg) 133 } 134 } 135 // try to justify the log output for short messages 136 length := utf8.RuneCountInString(r.Msg) 137 if len(r.Ctx) > 0 && length < termMsgJust { 138 b.Write(bytes.Repeat([]byte{' '}, termMsgJust-length)) 139 } 140 // print the keys logfmt style 141 logfmt(b, r.Ctx, color, true) 142 return b.Bytes() 143 }) 144 } 145 146 // LogfmtFormat prints records in logfmt format, an easy machine-parseable but human-readable 147 // format for key/value pairs. 148 // 149 // For more details see: http://godoc.org/github.com/kr/logfmt 150 // 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 { 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 { 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 if t, ok := value.(time.Time); ok { 332 // Performance optimization: No need for escaping since the provided 333 // timeFormat doesn't have any escape characters, and escaping is 334 // expensive. 335 return t.Format(timeFormat) 336 } 337 if term { 338 if s, ok := value.(TerminalStringer); ok { 339 // Custom terminal stringer provided, use that 340 return escapeString(s.TerminalString()) 341 } 342 } 343 value = formatShared(value) 344 switch v := value.(type) { 345 case bool: 346 return strconv.FormatBool(v) 347 case float32: 348 return strconv.FormatFloat(float64(v), floatFormat, 3, 64) 349 case float64: 350 return strconv.FormatFloat(v, floatFormat, 3, 64) 351 case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: 352 return fmt.Sprintf("%d", value) 353 case string: 354 return escapeString(v) 355 default: 356 return escapeString(fmt.Sprintf("%+v", value)) 357 } 358 } 359 360 var stringBufPool = sync.Pool{ 361 New: func() interface{} { return new(bytes.Buffer) }, 362 } 363 364 func escapeString(s string) string { 365 needsQuotes := false 366 needsEscape := false 367 for _, r := range s { 368 if r <= ' ' || r == '=' || r == '"' { 369 needsQuotes = true 370 } 371 if r == '\\' || r == '"' || r == '\n' || r == '\r' || r == '\t' { 372 needsEscape = true 373 } 374 } 375 if !needsEscape && !needsQuotes { 376 return s 377 } 378 e := stringBufPool.Get().(*bytes.Buffer) 379 e.WriteByte('"') 380 for _, r := range s { 381 switch r { 382 case '\\', '"': 383 e.WriteByte('\\') 384 e.WriteByte(byte(r)) 385 case '\n': 386 e.WriteString("\\n") 387 case '\r': 388 e.WriteString("\\r") 389 case '\t': 390 e.WriteString("\\t") 391 default: 392 e.WriteRune(r) 393 } 394 } 395 e.WriteByte('"') 396 var ret string 397 if needsQuotes { 398 ret = e.String() 399 } else { 400 ret = string(e.Bytes()[1 : e.Len()-1]) 401 } 402 e.Reset() 403 stringBufPool.Put(e) 404 return ret 405 }