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