github.com/bcskill/bcschain/v3@v3.4.9-beta2/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" 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/bcskill/bcschain/v3/", 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 // [TIME] [LEVEL] MESAGE key=value key=value ... 81 // 82 // Example: 83 // 84 // [May 16 20:58:45] [DBUG] 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 var stackdriverKeyNames = RecordKeyNames{ 200 Time: "timestamp", 201 Msg: "message", 202 Lvl: "severity", 203 } 204 205 func StackdriverFormat() Format { 206 jf := JsonFormat() 207 return FormatFunc(func(r *Record) []byte { 208 r.KeyNames = stackdriverKeyNames 209 return jf.Format(r) 210 }) 211 } 212 213 // JsonFormat formats log records as JSON objects separated by newlines. 214 // It is the equivalent of JsonFormatEx(false, true). 215 func JsonFormat() Format { 216 return JsonFormatEx(false, true) 217 } 218 219 // JsonFormatEx formats log records as JSON objects. If pretty is true, 220 // records will be pretty-printed. If lineSeparated is true, records 221 // will be logged with a new line between each record. 222 func JsonFormatEx(pretty, lineSeparated bool) Format { 223 jsonMarshal := json.Marshal 224 if pretty { 225 jsonMarshal = func(v interface{}) ([]byte, error) { 226 return json.MarshalIndent(v, "", " ") 227 } 228 } 229 230 return FormatFunc(func(r *Record) []byte { 231 props := make(map[string]interface{}) 232 233 props[r.KeyNames.Time] = r.Time 234 props[r.KeyNames.Lvl] = r.Lvl.String() 235 props[r.KeyNames.Msg] = r.Msg 236 237 for i := 0; i < len(r.Ctx); i += 2 { 238 k, ok := r.Ctx[i].(string) 239 if !ok { 240 props[errorKey] = fmt.Sprintf("%+v is not a string key", r.Ctx[i]) 241 } 242 props[k] = formatJsonValue(r.Ctx[i+1]) 243 } 244 245 b, err := jsonMarshal(props) 246 if err != nil { 247 b, _ = jsonMarshal(map[string]string{ 248 errorKey: err.Error(), 249 }) 250 return b 251 } 252 253 if lineSeparated { 254 b = append(b, '\n') 255 } 256 257 return b 258 }) 259 } 260 261 func formatShared(value interface{}) (result interface{}) { 262 defer func() { 263 if err := recover(); err != nil { 264 if v := reflect.ValueOf(value); v.Kind() == reflect.Ptr && v.IsNil() { 265 result = "nil" 266 } else { 267 panic(err) 268 } 269 } 270 }() 271 272 switch v := value.(type) { 273 case time.Time: 274 return v.Format(timeFormat) 275 276 case error: 277 return v.Error() 278 279 case fmt.Stringer: 280 return v.String() 281 282 default: 283 return v 284 } 285 } 286 287 func formatJsonValue(value interface{}) interface{} { 288 value = formatShared(value) 289 switch value.(type) { 290 case int, int8, int16, int32, int64, float32, float64, uint, uint8, uint16, uint32, uint64, string: 291 return value 292 default: 293 return fmt.Sprintf("%+v", value) 294 } 295 } 296 297 // formatValue formats a value for serialization 298 func formatLogfmtValue(value interface{}, term bool) string { 299 if value == nil { 300 return "nil" 301 } 302 303 if t, ok := value.(time.Time); ok { 304 // Performance optimization: No need for escaping since the provided 305 // timeFormat doesn't have any escape characters, and escaping is 306 // expensive. 307 return t.Format(timeFormat) 308 } 309 if term { 310 if s, ok := value.(TerminalStringer); ok { 311 // Custom terminal stringer provided, use that 312 return escapeString(s.TerminalString()) 313 } 314 } 315 value = formatShared(value) 316 switch v := value.(type) { 317 case bool: 318 return strconv.FormatBool(v) 319 case float32: 320 return strconv.FormatFloat(float64(v), floatFormat, 3, 64) 321 case float64: 322 return strconv.FormatFloat(v, floatFormat, 3, 64) 323 case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: 324 return fmt.Sprintf("%d", value) 325 case string: 326 return escapeString(v) 327 default: 328 return escapeString(fmt.Sprintf("%+v", value)) 329 } 330 } 331 332 var stringBufPool = sync.Pool{ 333 New: func() interface{} { return new(bytes.Buffer) }, 334 } 335 336 func escapeString(s string) string { 337 needsQuotes := false 338 needsEscape := false 339 for _, r := range s { 340 if r <= ' ' || r == '=' || r == '"' { 341 needsQuotes = true 342 } 343 if r == '\\' || r == '"' || r == '\n' || r == '\r' || r == '\t' { 344 needsEscape = true 345 } 346 } 347 if !needsEscape && !needsQuotes { 348 return s 349 } 350 e := stringBufPool.Get().(*bytes.Buffer) 351 e.WriteByte('"') 352 for _, r := range s { 353 switch r { 354 case '\\', '"': 355 e.WriteByte('\\') 356 e.WriteByte(byte(r)) 357 case '\n': 358 e.WriteString("\\n") 359 case '\r': 360 e.WriteString("\\r") 361 case '\t': 362 e.WriteString("\\t") 363 default: 364 e.WriteRune(r) 365 } 366 } 367 e.WriteByte('"') 368 var ret string 369 if needsQuotes { 370 ret = e.String() 371 } else { 372 ret = string(e.Bytes()[1 : e.Len()-1]) 373 } 374 e.Reset() 375 stringBufPool.Put(e) 376 return ret 377 }