github.com/core-coin/go-core/v2@v2.1.9/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/core-coin/go-core/v2/", 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] MESSAGE 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 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 func LogfmtFormat() Format { 151 return FormatFunc(func(r *Record) []byte { 152 common := []interface{}{r.KeyNames.Time, r.Time, r.KeyNames.Lvl, r.Lvl, r.KeyNames.Msg, r.Msg} 153 buf := &bytes.Buffer{} 154 logfmt(buf, append(common, r.Ctx...), 0, false) 155 return buf.Bytes() 156 }) 157 } 158 159 func logfmt(buf *bytes.Buffer, ctx []interface{}, color int, term bool) { 160 for i := 0; i < len(ctx); i += 2 { 161 if i != 0 { 162 buf.WriteByte(' ') 163 } 164 165 k, ok := ctx[i].(string) 166 v := formatLogfmtValue(ctx[i+1], term) 167 if !ok { 168 k, v = errorKey, formatLogfmtValue(k, term) 169 } 170 171 // XXX: we should probably check that all of your key bytes aren't invalid 172 fieldPaddingLock.RLock() 173 padding := fieldPadding[k] 174 fieldPaddingLock.RUnlock() 175 176 length := utf8.RuneCountInString(v) 177 if padding < length && length <= termCtxMaxPadding { 178 padding = length 179 180 fieldPaddingLock.Lock() 181 fieldPadding[k] = padding 182 fieldPaddingLock.Unlock() 183 } 184 if color > 0 { 185 fmt.Fprintf(buf, "\x1b[%dm%s\x1b[0m=", color, k) 186 } else { 187 buf.WriteString(k) 188 buf.WriteByte('=') 189 } 190 buf.WriteString(v) 191 if i < len(ctx)-2 && padding > length { 192 buf.Write(bytes.Repeat([]byte{' '}, padding-length)) 193 } 194 } 195 buf.WriteByte('\n') 196 } 197 198 // JSONFormat formats log records as JSON objects separated by newlines. 199 // It is the equivalent of JSONFormatEx(false, true). 200 func JSONFormat() Format { 201 return JSONFormatEx(false, true) 202 } 203 204 // JSONFormatOrderedEx formats log records as JSON arrays. If pretty is true, 205 // records will be pretty-printed. If lineSeparated is true, records 206 // will be logged with a new line between each record. 207 func JSONFormatOrderedEx(pretty, lineSeparated bool) Format { 208 jsonMarshal := json.Marshal 209 if pretty { 210 jsonMarshal = func(v interface{}) ([]byte, error) { 211 return json.MarshalIndent(v, "", " ") 212 } 213 } 214 return FormatFunc(func(r *Record) []byte { 215 props := make(map[string]interface{}) 216 217 props[r.KeyNames.Time] = r.Time 218 props[r.KeyNames.Lvl] = r.Lvl.String() 219 props[r.KeyNames.Msg] = r.Msg 220 221 ctx := make([]string, len(r.Ctx)) 222 for i := 0; i < len(r.Ctx); i += 2 { 223 k, ok := r.Ctx[i].(string) 224 if !ok { 225 props[errorKey] = fmt.Sprintf("%+v is not a string key,", r.Ctx[i]) 226 } 227 ctx[i] = k 228 ctx[i+1] = formatLogfmtValue(r.Ctx[i+1], true) 229 } 230 props[r.KeyNames.Ctx] = ctx 231 232 b, err := jsonMarshal(props) 233 if err != nil { 234 b, _ = jsonMarshal(map[string]string{ 235 errorKey: err.Error(), 236 }) 237 return b 238 } 239 if lineSeparated { 240 b = append(b, '\n') 241 } 242 return b 243 }) 244 } 245 246 // JSONFormatEx formats log records as JSON objects. If pretty is true, 247 // records will be pretty-printed. If lineSeparated is true, records 248 // will be logged with a new line between each record. 249 func JSONFormatEx(pretty, lineSeparated bool) Format { 250 jsonMarshal := json.Marshal 251 if pretty { 252 jsonMarshal = func(v interface{}) ([]byte, error) { 253 return json.MarshalIndent(v, "", " ") 254 } 255 } 256 257 return FormatFunc(func(r *Record) []byte { 258 props := make(map[string]interface{}) 259 260 props[r.KeyNames.Time] = r.Time 261 props[r.KeyNames.Lvl] = r.Lvl.String() 262 props[r.KeyNames.Msg] = r.Msg 263 264 for i := 0; i < len(r.Ctx); i += 2 { 265 k, ok := r.Ctx[i].(string) 266 if !ok { 267 props[errorKey] = fmt.Sprintf("%+v is not a string key", r.Ctx[i]) 268 } 269 props[k] = formatJSONValue(r.Ctx[i+1]) 270 } 271 272 b, err := jsonMarshal(props) 273 if err != nil { 274 b, _ = jsonMarshal(map[string]string{ 275 errorKey: err.Error(), 276 }) 277 return b 278 } 279 280 if lineSeparated { 281 b = append(b, '\n') 282 } 283 284 return b 285 }) 286 } 287 288 func formatShared(value interface{}) (result interface{}) { 289 defer func() { 290 if err := recover(); err != nil { 291 if v := reflect.ValueOf(value); v.Kind() == reflect.Ptr && v.IsNil() { 292 result = "nil" 293 } else { 294 panic(err) 295 } 296 } 297 }() 298 299 switch v := value.(type) { 300 case time.Time: 301 return v.Format(timeFormat) 302 303 case error: 304 return v.Error() 305 306 case fmt.Stringer: 307 return v.String() 308 309 default: 310 return v 311 } 312 } 313 314 func formatJSONValue(value interface{}) interface{} { 315 value = formatShared(value) 316 switch value.(type) { 317 case int, int8, int16, int32, int64, float32, float64, uint, uint8, uint16, uint32, uint64, string: 318 return value 319 default: 320 return fmt.Sprintf("%+v", value) 321 } 322 } 323 324 // formatValue formats a value for serialization 325 func formatLogfmtValue(value interface{}, term bool) string { 326 if value == nil { 327 return "nil" 328 } 329 330 if t, ok := value.(time.Time); ok { 331 // Performance optimization: No need for escaping since the provided 332 // timeFormat doesn't have any escape characters, and escaping is 333 // expensive. 334 return t.Format(timeFormat) 335 } 336 if term { 337 if s, ok := value.(TerminalStringer); ok { 338 // Custom terminal stringer provided, use that 339 return escapeString(s.TerminalString()) 340 } 341 } 342 value = formatShared(value) 343 switch v := value.(type) { 344 case bool: 345 return strconv.FormatBool(v) 346 case float32: 347 return strconv.FormatFloat(float64(v), floatFormat, 3, 64) 348 case float64: 349 return strconv.FormatFloat(v, floatFormat, 3, 64) 350 case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: 351 return fmt.Sprintf("%d", value) 352 case string: 353 return escapeString(v) 354 default: 355 return escapeString(fmt.Sprintf("%+v", value)) 356 } 357 } 358 359 // escapeString checks if the provided string needs escaping/quoting, and 360 // calls strconv.Quote if needed 361 func escapeString(s string) string { 362 needsQuoting := false 363 for _, r := range s { 364 // We quote everything below " (0x34) and above~ (0x7E), plus equal-sign 365 if r <= '"' || r > '~' || r == '=' { 366 needsQuoting = true 367 break 368 } 369 } 370 if !needsQuoting { 371 return s 372 } 373 return strconv.Quote(s) 374 }