github.com/SmartMeshFoundation/Spectrum@v0.0.0-20220621030607-452a266fee1e/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 ( 25 locationTrims = []string{ 26 "github.com/SmartMeshFoundation/Spectrum/", 27 } 28 is_debug = false 29 ) 30 31 func IsDebug() bool { 32 return is_debug 33 } 34 // PrintOrigins sets or unsets log location (file:line) printing for terminal 35 // format output. 36 func PrintOrigins(print bool) { 37 is_debug = print 38 if print { 39 atomic.StoreUint32(&locationEnabled, 1) 40 } else { 41 atomic.StoreUint32(&locationEnabled, 0) 42 } 43 } 44 45 // locationEnabled is an atomic flag controlling whether the terminal formatter 46 // should append the log locations too when printing entries. 47 var locationEnabled uint32 48 49 // locationLength is the maxmimum path length encountered, which all logs are 50 // padded to to aid in alignment. 51 var locationLength uint32 52 53 // fieldPadding is a global map with maximum field value lengths seen until now 54 // to allow padding log contexts in a bit smarter way. 55 var fieldPadding = make(map[string]int) 56 57 // fieldPaddingLock is a global mutex protecting the field padding map. 58 var fieldPaddingLock sync.RWMutex 59 60 type Format interface { 61 Format(r *Record) []byte 62 } 63 64 // FormatFunc returns a new Format object which uses 65 // the given function to perform record formatting. 66 func FormatFunc(f func(*Record) []byte) Format { 67 return formatFunc(f) 68 } 69 70 type formatFunc func(*Record) []byte 71 72 func (f formatFunc) Format(r *Record) []byte { 73 return f(r) 74 } 75 76 // TerminalStringer is an analogous interface to the stdlib stringer, allowing 77 // own types to have custom shortened serialization formats when printed to the 78 // screen. 79 type TerminalStringer interface { 80 TerminalString() string 81 } 82 83 // TerminalFormat formats log records optimized for human readability on 84 // a terminal with color-coded level output and terser human friendly timestamp. 85 // This format should only be used for interactive programs or while developing. 86 // 87 // [TIME] [LEVEL] MESAGE key=value key=value ... 88 // 89 // Example: 90 // 91 // [May 16 20:58:45] [DBUG] remove route ns=haproxy addr=127.0.0.1:50002 92 // 93 func TerminalFormat(usecolor bool) Format { 94 return FormatFunc(func(r *Record) []byte { 95 var color = 0 96 if usecolor { 97 switch r.Lvl { 98 case LvlCrit: 99 color = 35 100 case LvlError: 101 color = 31 102 case LvlWarn: 103 color = 33 104 case LvlInfo: 105 color = 32 106 case LvlDebug: 107 color = 36 108 case LvlTrace: 109 color = 34 110 } 111 } 112 113 b := &bytes.Buffer{} 114 lvl := r.Lvl.AlignedString() 115 if atomic.LoadUint32(&locationEnabled) != 0 { 116 // Log origin printing was requested, format the location path and line number 117 location := fmt.Sprintf("%+v", r.Call) 118 for _, prefix := range locationTrims { 119 location = strings.TrimPrefix(location, prefix) 120 } 121 // Maintain the maximum location length for fancyer alignment 122 align := int(atomic.LoadUint32(&locationLength)) 123 if align < len(location) { 124 align = len(location) 125 atomic.StoreUint32(&locationLength, uint32(align)) 126 } 127 padding := strings.Repeat(" ", align-len(location)) 128 129 // Assemble and print the log heading 130 if color > 0 { 131 fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s|%s]%s %s ", color, lvl, r.Time.Format(termTimeFormat), location, padding, r.Msg) 132 } else { 133 fmt.Fprintf(b, "%s[%s|%s]%s %s ", lvl, r.Time.Format(termTimeFormat), location, padding, r.Msg) 134 } 135 } else { 136 if color > 0 { 137 fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s] %s ", color, lvl, r.Time.Format(termTimeFormat), r.Msg) 138 } else { 139 fmt.Fprintf(b, "%s[%s] %s ", lvl, r.Time.Format(termTimeFormat), r.Msg) 140 } 141 } 142 // try to justify the log output for short messages 143 length := utf8.RuneCountInString(r.Msg) 144 if len(r.Ctx) > 0 && length < termMsgJust { 145 b.Write(bytes.Repeat([]byte{' '}, termMsgJust-length)) 146 } 147 // print the keys logfmt style 148 logfmt(b, r.Ctx, color, true) 149 return b.Bytes() 150 }) 151 } 152 153 // LogfmtFormat prints records in logfmt format, an easy machine-parseable but human-readable 154 // format for key/value pairs. 155 // 156 // For more details see: http://godoc.org/github.com/kr/logfmt 157 // 158 func LogfmtFormat() Format { 159 return FormatFunc(func(r *Record) []byte { 160 common := []interface{}{r.KeyNames.Time, r.Time, r.KeyNames.Lvl, r.Lvl, r.KeyNames.Msg, r.Msg} 161 buf := &bytes.Buffer{} 162 logfmt(buf, append(common, r.Ctx...), 0, false) 163 return buf.Bytes() 164 }) 165 } 166 167 func logfmt(buf *bytes.Buffer, ctx []interface{}, color int, term bool) { 168 for i := 0; i < len(ctx); i += 2 { 169 if i != 0 { 170 buf.WriteByte(' ') 171 } 172 173 k, ok := ctx[i].(string) 174 v := formatLogfmtValue(ctx[i+1], term) 175 if !ok { 176 k, v = errorKey, formatLogfmtValue(k, term) 177 } 178 179 // XXX: we should probably check that all of your key bytes aren't invalid 180 fieldPaddingLock.RLock() 181 padding := fieldPadding[k] 182 fieldPaddingLock.RUnlock() 183 184 length := utf8.RuneCountInString(v) 185 if padding < length { 186 padding = length 187 188 fieldPaddingLock.Lock() 189 fieldPadding[k] = padding 190 fieldPaddingLock.Unlock() 191 } 192 if color > 0 { 193 fmt.Fprintf(buf, "\x1b[%dm%s\x1b[0m=", color, k) 194 } else { 195 buf.WriteString(k) 196 buf.WriteByte('=') 197 } 198 buf.WriteString(v) 199 if i < len(ctx)-2 { 200 buf.Write(bytes.Repeat([]byte{' '}, padding-length)) 201 } 202 } 203 buf.WriteByte('\n') 204 } 205 206 // JsonFormat formats log records as JSON objects separated by newlines. 207 // It is the equivalent of JsonFormatEx(false, true). 208 func JsonFormat() Format { 209 return JsonFormatEx(false, true) 210 } 211 212 // JsonFormatEx formats log records as JSON objects. If pretty is true, 213 // records will be pretty-printed. If lineSeparated is true, records 214 // will be logged with a new line between each record. 215 func JsonFormatEx(pretty, lineSeparated bool) Format { 216 jsonMarshal := json.Marshal 217 if pretty { 218 jsonMarshal = func(v interface{}) ([]byte, error) { 219 return json.MarshalIndent(v, "", " ") 220 } 221 } 222 223 return FormatFunc(func(r *Record) []byte { 224 props := make(map[string]interface{}) 225 226 props[r.KeyNames.Time] = r.Time 227 props[r.KeyNames.Lvl] = r.Lvl.String() 228 props[r.KeyNames.Msg] = r.Msg 229 230 for i := 0; i < len(r.Ctx); i += 2 { 231 k, ok := r.Ctx[i].(string) 232 if !ok { 233 props[errorKey] = fmt.Sprintf("%+v is not a string key", r.Ctx[i]) 234 } 235 props[k] = formatJsonValue(r.Ctx[i+1]) 236 } 237 238 b, err := jsonMarshal(props) 239 if err != nil { 240 b, _ = jsonMarshal(map[string]string{ 241 errorKey: err.Error(), 242 }) 243 return b 244 } 245 246 if lineSeparated { 247 b = append(b, '\n') 248 } 249 250 return b 251 }) 252 } 253 254 func formatShared(value interface{}) (result interface{}) { 255 defer func() { 256 if err := recover(); err != nil { 257 if v := reflect.ValueOf(value); v.Kind() == reflect.Ptr && v.IsNil() { 258 result = "nil" 259 } else { 260 panic(err) 261 } 262 } 263 }() 264 265 switch v := value.(type) { 266 case time.Time: 267 return v.Format(timeFormat) 268 269 case error: 270 return v.Error() 271 272 case fmt.Stringer: 273 return v.String() 274 275 default: 276 return v 277 } 278 } 279 280 func formatJsonValue(value interface{}) interface{} { 281 value = formatShared(value) 282 switch value.(type) { 283 case int, int8, int16, int32, int64, float32, float64, uint, uint8, uint16, uint32, uint64, string: 284 return value 285 default: 286 return fmt.Sprintf("%+v", value) 287 } 288 } 289 290 // formatValue formats a value for serialization 291 func formatLogfmtValue(value interface{}, term bool) string { 292 if value == nil { 293 return "nil" 294 } 295 296 if t, ok := value.(time.Time); ok { 297 // Performance optimization: No need for escaping since the provided 298 // timeFormat doesn't have any escape characters, and escaping is 299 // expensive. 300 return t.Format(timeFormat) 301 } 302 if term { 303 if s, ok := value.(TerminalStringer); ok { 304 // Custom terminal stringer provided, use that 305 return escapeString(s.TerminalString()) 306 } 307 } 308 value = formatShared(value) 309 switch v := value.(type) { 310 case bool: 311 return strconv.FormatBool(v) 312 case float32: 313 return strconv.FormatFloat(float64(v), floatFormat, 3, 64) 314 case float64: 315 return strconv.FormatFloat(v, floatFormat, 3, 64) 316 case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: 317 return fmt.Sprintf("%d", value) 318 case string: 319 return escapeString(v) 320 default: 321 return escapeString(fmt.Sprintf("%+v", value)) 322 } 323 } 324 325 var stringBufPool = sync.Pool{ 326 New: func() interface{} { return new(bytes.Buffer) }, 327 } 328 329 func escapeString(s string) string { 330 needsQuotes := false 331 needsEscape := false 332 for _, r := range s { 333 if r <= ' ' || r == '=' || r == '"' { 334 needsQuotes = true 335 } 336 if r == '\\' || r == '"' || r == '\n' || r == '\r' || r == '\t' { 337 needsEscape = true 338 } 339 } 340 if !needsEscape && !needsQuotes { 341 return s 342 } 343 e := stringBufPool.Get().(*bytes.Buffer) 344 e.WriteByte('"') 345 for _, r := range s { 346 switch r { 347 case '\\', '"': 348 e.WriteByte('\\') 349 e.WriteByte(byte(r)) 350 case '\n': 351 e.WriteString("\\n") 352 case '\r': 353 e.WriteString("\\r") 354 case '\t': 355 e.WriteString("\\t") 356 default: 357 e.WriteRune(r) 358 } 359 } 360 e.WriteByte('"') 361 var ret string 362 if needsQuotes { 363 ret = e.String() 364 } else { 365 ret = string(e.Bytes()[1: e.Len()-1]) 366 } 367 e.Reset() 368 stringBufPool.Put(e) 369 return ret 370 }