github.com/luckypickle/go-ethereum-vet@v1.14.2/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/luckypickle/go-ethereum-vet/", 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 func TerminalFormat(usecolor bool) Format { 86 return FormatFunc(func(r *Record) []byte { 87 var color = 0 88 if usecolor { 89 switch r.Lvl { 90 case LvlCrit: 91 color = 35 92 case LvlError: 93 color = 31 94 case LvlWarn: 95 color = 33 96 case LvlInfo: 97 color = 32 98 case LvlDebug: 99 color = 36 100 case LvlTrace: 101 color = 34 102 } 103 } 104 105 b := &bytes.Buffer{} 106 lvl := r.Lvl.AlignedString() 107 if atomic.LoadUint32(&locationEnabled) != 0 { 108 // Log origin printing was requested, format the location path and line number 109 location := fmt.Sprintf("%+v", r.Call) 110 for _, prefix := range locationTrims { 111 location = strings.TrimPrefix(location, prefix) 112 } 113 // Maintain the maximum location length for fancyer alignment 114 align := int(atomic.LoadUint32(&locationLength)) 115 if align < len(location) { 116 align = len(location) 117 atomic.StoreUint32(&locationLength, uint32(align)) 118 } 119 padding := strings.Repeat(" ", align-len(location)) 120 121 // Assemble and print the log heading 122 if color > 0 { 123 fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s|%s]%s %s ", color, lvl, r.Time.Format(termTimeFormat), location, padding, r.Msg) 124 } else { 125 fmt.Fprintf(b, "%s[%s|%s]%s %s ", lvl, r.Time.Format(termTimeFormat), location, padding, r.Msg) 126 } 127 } else { 128 if color > 0 { 129 fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s] %s ", color, lvl, r.Time.Format(termTimeFormat), r.Msg) 130 } else { 131 fmt.Fprintf(b, "%s[%s] %s ", lvl, r.Time.Format(termTimeFormat), r.Msg) 132 } 133 } 134 // try to justify the log output for short messages 135 length := utf8.RuneCountInString(r.Msg) 136 if len(r.Ctx) > 0 && length < termMsgJust { 137 b.Write(bytes.Repeat([]byte{' '}, termMsgJust-length)) 138 } 139 // print the keys logfmt style 140 logfmt(b, r.Ctx, color, true) 141 return b.Bytes() 142 }) 143 } 144 145 // LogfmtFormat prints records in logfmt format, an easy machine-parseable but human-readable 146 // format for key/value pairs. 147 // 148 // For more details see: http://godoc.org/github.com/kr/logfmt 149 func LogfmtFormat() Format { 150 return FormatFunc(func(r *Record) []byte { 151 common := []interface{}{r.KeyNames.Time, r.Time, r.KeyNames.Lvl, r.Lvl, r.KeyNames.Msg, r.Msg} 152 buf := &bytes.Buffer{} 153 logfmt(buf, append(common, r.Ctx...), 0, false) 154 return buf.Bytes() 155 }) 156 } 157 158 func logfmt(buf *bytes.Buffer, ctx []interface{}, color int, term bool) { 159 for i := 0; i < len(ctx); i += 2 { 160 if i != 0 { 161 buf.WriteByte(' ') 162 } 163 164 k, ok := ctx[i].(string) 165 v := formatLogfmtValue(ctx[i+1], term) 166 if !ok { 167 k, v = errorKey, formatLogfmtValue(k, term) 168 } 169 170 // XXX: we should probably check that all of your key bytes aren't invalid 171 fieldPaddingLock.RLock() 172 padding := fieldPadding[k] 173 fieldPaddingLock.RUnlock() 174 175 length := utf8.RuneCountInString(v) 176 if padding < length { 177 padding = length 178 179 fieldPaddingLock.Lock() 180 fieldPadding[k] = padding 181 fieldPaddingLock.Unlock() 182 } 183 if color > 0 { 184 fmt.Fprintf(buf, "\x1b[%dm%s\x1b[0m=", color, k) 185 } else { 186 buf.WriteString(k) 187 buf.WriteByte('=') 188 } 189 buf.WriteString(v) 190 if i < len(ctx)-2 { 191 buf.Write(bytes.Repeat([]byte{' '}, padding-length)) 192 } 193 } 194 buf.WriteByte('\n') 195 } 196 197 // JSONFormat formats log records as JSON objects separated by newlines. 198 // It is the equivalent of JSONFormatEx(false, true). 199 func JSONFormat() Format { 200 return JSONFormatEx(false, true) 201 } 202 203 // JSONFormatOrderedEx formats log records as JSON arrays. If pretty is true, 204 // records will be pretty-printed. If lineSeparated is true, records 205 // will be logged with a new line between each record. 206 func JSONFormatOrderedEx(pretty, lineSeparated bool) Format { 207 jsonMarshal := json.Marshal 208 if pretty { 209 jsonMarshal = func(v interface{}) ([]byte, error) { 210 return json.MarshalIndent(v, "", " ") 211 } 212 } 213 return FormatFunc(func(r *Record) []byte { 214 props := make(map[string]interface{}) 215 216 props[r.KeyNames.Time] = r.Time 217 props[r.KeyNames.Lvl] = r.Lvl.String() 218 props[r.KeyNames.Msg] = r.Msg 219 220 ctx := make([]string, len(r.Ctx)) 221 for i := 0; i < len(r.Ctx); i += 2 { 222 k, ok := r.Ctx[i].(string) 223 if !ok { 224 props[errorKey] = fmt.Sprintf("%+v is not a string key,", r.Ctx[i]) 225 } 226 ctx[i] = k 227 ctx[i+1] = formatLogfmtValue(r.Ctx[i+1], true) 228 } 229 props[r.KeyNames.Ctx] = ctx 230 231 b, err := jsonMarshal(props) 232 if err != nil { 233 b, _ = jsonMarshal(map[string]string{ 234 errorKey: err.Error(), 235 }) 236 return b 237 } 238 if lineSeparated { 239 b = append(b, '\n') 240 } 241 return b 242 }) 243 } 244 245 // JSONFormatEx formats log records as JSON objects. If pretty is true, 246 // records will be pretty-printed. If lineSeparated is true, records 247 // will be logged with a new line between each record. 248 func JSONFormatEx(pretty, lineSeparated bool) Format { 249 jsonMarshal := json.Marshal 250 if pretty { 251 jsonMarshal = func(v interface{}) ([]byte, error) { 252 return json.MarshalIndent(v, "", " ") 253 } 254 } 255 256 return FormatFunc(func(r *Record) []byte { 257 props := make(map[string]interface{}) 258 259 props[r.KeyNames.Time] = r.Time 260 props[r.KeyNames.Lvl] = r.Lvl.String() 261 props[r.KeyNames.Msg] = r.Msg 262 263 for i := 0; i < len(r.Ctx); i += 2 { 264 k, ok := r.Ctx[i].(string) 265 if !ok { 266 props[errorKey] = fmt.Sprintf("%+v is not a string key", r.Ctx[i]) 267 } 268 props[k] = formatJSONValue(r.Ctx[i+1]) 269 } 270 271 b, err := jsonMarshal(props) 272 if err != nil { 273 b, _ = jsonMarshal(map[string]string{ 274 errorKey: err.Error(), 275 }) 276 return b 277 } 278 279 if lineSeparated { 280 b = append(b, '\n') 281 } 282 283 return b 284 }) 285 } 286 287 func formatShared(value interface{}) (result interface{}) { 288 defer func() { 289 if err := recover(); err != nil { 290 if v := reflect.ValueOf(value); v.Kind() == reflect.Ptr && v.IsNil() { 291 result = "nil" 292 } else { 293 panic(err) 294 } 295 } 296 }() 297 298 switch v := value.(type) { 299 case time.Time: 300 return v.Format(timeFormat) 301 302 case error: 303 return v.Error() 304 305 case fmt.Stringer: 306 return v.String() 307 308 default: 309 return v 310 } 311 } 312 313 func formatJSONValue(value interface{}) interface{} { 314 value = formatShared(value) 315 switch value.(type) { 316 case int, int8, int16, int32, int64, float32, float64, uint, uint8, uint16, uint32, uint64, string: 317 return value 318 default: 319 return fmt.Sprintf("%+v", value) 320 } 321 } 322 323 // formatValue formats a value for serialization 324 func formatLogfmtValue(value interface{}, term bool) string { 325 if value == nil { 326 return "nil" 327 } 328 329 if t, ok := value.(time.Time); ok { 330 // Performance optimization: No need for escaping since the provided 331 // timeFormat doesn't have any escape characters, and escaping is 332 // expensive. 333 return t.Format(timeFormat) 334 } 335 if term { 336 if s, ok := value.(TerminalStringer); ok { 337 // Custom terminal stringer provided, use that 338 return escapeString(s.TerminalString()) 339 } 340 } 341 value = formatShared(value) 342 switch v := value.(type) { 343 case bool: 344 return strconv.FormatBool(v) 345 case float32: 346 return strconv.FormatFloat(float64(v), floatFormat, 3, 64) 347 case float64: 348 return strconv.FormatFloat(v, floatFormat, 3, 64) 349 case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: 350 return fmt.Sprintf("%d", value) 351 case string: 352 return escapeString(v) 353 default: 354 return escapeString(fmt.Sprintf("%+v", value)) 355 } 356 } 357 358 var stringBufPool = sync.Pool{ 359 New: func() interface{} { return new(bytes.Buffer) }, 360 } 361 362 func escapeString(s string) string { 363 needsQuotes := false 364 needsEscape := false 365 for _, r := range s { 366 if r <= ' ' || r == '=' || r == '"' { 367 needsQuotes = true 368 } 369 if r == '\\' || r == '"' || r == '\n' || r == '\r' || r == '\t' { 370 needsEscape = true 371 } 372 } 373 if !needsEscape && !needsQuotes { 374 return s 375 } 376 e := stringBufPool.Get().(*bytes.Buffer) 377 e.WriteByte('"') 378 for _, r := range s { 379 switch r { 380 case '\\', '"': 381 e.WriteByte('\\') 382 e.WriteByte(byte(r)) 383 case '\n': 384 e.WriteString("\\n") 385 case '\r': 386 e.WriteString("\\r") 387 case '\t': 388 e.WriteString("\\t") 389 default: 390 e.WriteRune(r) 391 } 392 } 393 e.WriteByte('"') 394 var ret string 395 if needsQuotes { 396 ret = e.String() 397 } else { 398 ret = string(e.Bytes()[1 : e.Len()-1]) 399 } 400 e.Reset() 401 stringBufPool.Put(e) 402 return ret 403 }