github.com/klaytn/klaytn@v1.12.1/log/format.go (about) 1 // Copyright 2018 The klaytn Authors 2 // 3 // This file is derived from log/format.go (2018/06/04). 4 // See LICENSE in the top directory for the original copyright and license. 5 6 package log 7 8 import ( 9 "bytes" 10 "encoding/json" 11 "fmt" 12 "reflect" 13 "strconv" 14 "strings" 15 "sync" 16 "sync/atomic" 17 "time" 18 "unicode/utf8" 19 ) 20 21 const ( 22 timeFormat = "1999-05-02T15:04:05-0700" 23 termTimeFormat = "01-02|15:04:05" 24 logTimeFormat = "2006-01-02T15:04:05.000 Z07" 25 simpleLogTimeFormat = "01/02,15:04:05 Z07" 26 floatFormat = 'f' 27 termMsgJust = 40 28 ) 29 30 // locationTrims are trimmed for display to avoid unwieldy log lines. 31 var locationTrims = []string{ 32 "github.com/klaytn/klaytn/", 33 } 34 35 // PrintOrigins sets or unsets log location (file:line) printing for terminal 36 // format output. 37 func PrintOrigins(print bool) { 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 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 module := "" 114 if r.Ctx[0].(string) == "module" { 115 module = fmt.Sprintf("%v", r.Ctx[1]) 116 } 117 118 b := &bytes.Buffer{} 119 lvl := r.Lvl.AlignedString() 120 if atomic.LoadUint32(&locationEnabled) != 0 { 121 // Log origin printing was requested, format the location path and line number 122 location := fmt.Sprintf("%+v", r.Call) 123 for _, prefix := range locationTrims { 124 location = strings.TrimPrefix(location, prefix) 125 } 126 // Maintain the maximum location length for fancyer alignment 127 align := int(atomic.LoadUint32(&locationLength)) 128 if align < len(location) { 129 align = len(location) 130 atomic.StoreUint32(&locationLength, uint32(align)) 131 } 132 padding := strings.Repeat(" ", align-len(location)) 133 134 // Assemble and print the log heading 135 if color > 0 { 136 fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s] [%s|%s] %s %s ", color, lvl, r.Time.Format(simpleLogTimeFormat), module, location, padding, r.Msg) 137 } else { 138 fmt.Fprintf(b, "%s[%s] [%s|%s] %s %s ", lvl, r.Time.Format(simpleLogTimeFormat), module, location, padding, r.Msg) 139 } 140 } else { 141 if color > 0 { 142 fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s] [%s] %s ", color, lvl, r.Time.Format(simpleLogTimeFormat), module, r.Msg) 143 } else { 144 fmt.Fprintf(b, "%s[%s] [%s] %s ", lvl, r.Time.Format(simpleLogTimeFormat), module, r.Msg) 145 } 146 } 147 // try to justify the log output for short messages 148 length := utf8.RuneCountInString(r.Msg) 149 if len(r.Ctx) > 0 && length < termMsgJust { 150 b.Write(bytes.Repeat([]byte{' '}, termMsgJust-length)) 151 } 152 // print the keys logfmt style 153 logfmt(b, r.Ctx, color, true) 154 return b.Bytes() 155 }) 156 } 157 158 // LogfmtFormat prints records in logfmt format, an easy machine-parseable but human-readable 159 // format for key/value pairs. 160 // 161 // For more details see: http://godoc.org/github.com/kr/logfmt 162 // 163 func LogfmtFormat() Format { 164 return FormatFunc(func(r *Record) []byte { 165 common := []interface{}{r.KeyNames.Time, r.Time, r.KeyNames.Lvl, r.Lvl, r.KeyNames.Msg, r.Msg} 166 buf := &bytes.Buffer{} 167 logfmt(buf, append(common, r.Ctx...), 0, false) 168 return buf.Bytes() 169 }) 170 } 171 172 func logfmt(buf *bytes.Buffer, ctx []interface{}, color int, term bool) { 173 for i := 2; i < len(ctx); i += 2 { 174 buf.WriteByte(' ') 175 176 k, ok := ctx[i].(string) 177 v := formatLogfmtValue(ctx[i+1], term) 178 if !ok { 179 k, v = errorKey, formatLogfmtValue(k, term) 180 } 181 182 // XXX: we should probably check that all of your key bytes aren't invalid 183 fieldPaddingLock.RLock() 184 padding := fieldPadding[k] 185 fieldPaddingLock.RUnlock() 186 187 length := utf8.RuneCountInString(v) 188 if padding < length { 189 padding = length 190 191 fieldPaddingLock.Lock() 192 fieldPadding[k] = padding 193 fieldPaddingLock.Unlock() 194 } 195 if color > 0 { 196 fmt.Fprintf(buf, "\x1b[%dm%s\x1b[0m=", color, k) 197 } else { 198 buf.WriteString(k) 199 buf.WriteByte('=') 200 } 201 buf.WriteString(v) 202 if i < len(ctx)-2 { 203 buf.Write(bytes.Repeat([]byte{' '}, padding-length)) 204 } 205 } 206 buf.WriteByte('\n') 207 } 208 209 // JsonFormat formats log records as JSON objects separated by newlines. 210 // It is the equivalent of JsonFormatEx(false, true). 211 func JsonFormat() Format { 212 return JsonFormatEx(false, true) 213 } 214 215 // JsonFormatEx formats log records as JSON objects. If pretty is true, 216 // records will be pretty-printed. If lineSeparated is true, records 217 // will be logged with a new line between each record. 218 func JsonFormatEx(pretty, lineSeparated bool) Format { 219 jsonMarshal := json.Marshal 220 if pretty { 221 jsonMarshal = func(v interface{}) ([]byte, error) { 222 return json.MarshalIndent(v, "", " ") 223 } 224 } 225 226 return FormatFunc(func(r *Record) []byte { 227 props := make(map[string]interface{}) 228 229 props[r.KeyNames.Time] = r.Time 230 props[r.KeyNames.Lvl] = r.Lvl.String() 231 props[r.KeyNames.Msg] = r.Msg 232 233 for i := 0; i < len(r.Ctx); i += 2 { 234 k, ok := r.Ctx[i].(string) 235 if !ok { 236 props[errorKey] = fmt.Sprintf("%+v is not a string key", r.Ctx[i]) 237 } 238 props[k] = formatJsonValue(r.Ctx[i+1]) 239 } 240 241 b, err := jsonMarshal(props) 242 if err != nil { 243 b, _ = jsonMarshal(map[string]string{ 244 errorKey: err.Error(), 245 }) 246 return b 247 } 248 249 if lineSeparated { 250 b = append(b, '\n') 251 } 252 253 return b 254 }) 255 } 256 257 func formatShared(value interface{}) (result interface{}) { 258 defer func() { 259 if err := recover(); err != nil { 260 if v := reflect.ValueOf(value); v.Kind() == reflect.Ptr && v.IsNil() { 261 result = "nil" 262 } else { 263 panic(err) 264 } 265 } 266 }() 267 268 switch v := value.(type) { 269 case time.Time: 270 return v.Format(timeFormat) 271 272 case error: 273 return v.Error() 274 275 case fmt.Stringer: 276 return v.String() 277 278 default: 279 return v 280 } 281 } 282 283 func formatJsonValue(value interface{}) interface{} { 284 value = formatShared(value) 285 switch value.(type) { 286 case int, int8, int16, int32, int64, float32, float64, uint, uint8, uint16, uint32, uint64, string: 287 return value 288 default: 289 return fmt.Sprintf("%+v", value) 290 } 291 } 292 293 // formatValue formats a value for serialization 294 func formatLogfmtValue(value interface{}, term bool) string { 295 if value == nil { 296 return "nil" 297 } 298 299 if t, ok := value.(time.Time); ok { 300 // Performance optimization: No need for escaping since the provided 301 // timeFormat doesn't have any escape characters, and escaping is 302 // expensive. 303 return t.Format(timeFormat) 304 } 305 if term { 306 if s, ok := value.(TerminalStringer); ok { 307 // Custom terminal stringer provided, use that 308 return escapeString(s.TerminalString()) 309 } 310 } 311 value = formatShared(value) 312 switch v := value.(type) { 313 case bool: 314 return strconv.FormatBool(v) 315 case float32: 316 return strconv.FormatFloat(float64(v), floatFormat, 3, 64) 317 case float64: 318 return strconv.FormatFloat(v, floatFormat, 3, 64) 319 case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: 320 return fmt.Sprintf("%d", value) 321 case string: 322 return escapeString(v) 323 default: 324 return escapeString(fmt.Sprintf("%+v", value)) 325 } 326 } 327 328 var stringBufPool = sync.Pool{ 329 New: func() interface{} { return new(bytes.Buffer) }, 330 } 331 332 func escapeString(s string) string { 333 needsQuotes := false 334 needsEscape := false 335 for _, r := range s { 336 if r <= ' ' || r == '=' || r == '"' { 337 needsQuotes = true 338 } 339 if r == '\\' || r == '"' || r == '\n' || r == '\r' || r == '\t' { 340 needsEscape = true 341 } 342 } 343 if !needsEscape && !needsQuotes { 344 return s 345 } 346 e := stringBufPool.Get().(*bytes.Buffer) 347 e.WriteByte('"') 348 for _, r := range s { 349 switch r { 350 case '\\', '"': 351 e.WriteByte('\\') 352 e.WriteByte(byte(r)) 353 case '\n': 354 e.WriteString("\\n") 355 case '\r': 356 e.WriteString("\\r") 357 case '\t': 358 e.WriteString("\\t") 359 default: 360 e.WriteRune(r) 361 } 362 } 363 e.WriteByte('"') 364 var ret string 365 if needsQuotes { 366 ret = e.String() 367 } else { 368 ret = string(e.Bytes()[1 : e.Len()-1]) 369 } 370 e.Reset() 371 stringBufPool.Put(e) 372 return ret 373 }