package log import ( "bytes" "encoding/hex" "fmt" "io" "strings" "sync" "time" kitlog "github.com/go-kit/kit/log" kitlevel "github.com/go-kit/kit/log/level" "github.com/go-logfmt/logfmt" ) type tmfmtEncoder struct { *logfmt.Encoder buf bytes.Buffer } func (l *tmfmtEncoder) Reset() { l.Encoder.Reset() l.buf.Reset() } var tmfmtEncoderPool = sync.Pool{ New: func() interface{} { var enc tmfmtEncoder enc.Encoder = logfmt.NewEncoder(&enc.buf) return &enc }, } type tmfmtLogger struct { w io.Writer } // NewTMFmtLogger returns a logger that encodes keyvals to the Writer in // Tendermint custom format. Note complex types (structs, maps, slices) // formatted as "%+v". // // Each log event produces no more than one call to w.Write. // The passed Writer must be safe for concurrent use by multiple goroutines if // the returned Logger will be used concurrently. func NewTMFmtLogger(w io.Writer) kitlog.Logger { return &tmfmtLogger{w} } func (l tmfmtLogger) Log(keyvals ...interface{}) error { enc := tmfmtEncoderPool.Get().(*tmfmtEncoder) enc.Reset() defer tmfmtEncoderPool.Put(enc) const unknown = "unknown" lvl := "none" msg := unknown module := unknown // indexes of keys to skip while encoding later excludeIndexes := make([]int, 0) for i := 0; i < len(keyvals)-1; i += 2 { // Extract level switch keyvals[i] { case kitlevel.Key(): excludeIndexes = append(excludeIndexes, i) switch keyvals[i+1].(type) { // nolint:gocritic case string: lvl = keyvals[i+1].(string) case kitlevel.Value: lvl = keyvals[i+1].(kitlevel.Value).String() default: panic(fmt.Sprintf("level value of unknown type %T", keyvals[i+1])) } // and message case msgKey: excludeIndexes = append(excludeIndexes, i) msg = keyvals[i+1].(string) // and module (could be multiple keyvals; if such case last keyvalue wins) case moduleKey: excludeIndexes = append(excludeIndexes, i) module = keyvals[i+1].(string) } // Print []byte as a hexadecimal string (uppercased) if b, ok := keyvals[i+1].([]byte); ok { keyvals[i+1] = strings.ToUpper(hex.EncodeToString(b)) } } // Form a custom Tendermint line // // Example: // D[2016-05-02|11:06:44.322] Stopping AddrBook (ignoring: already stopped) // // Description: // D - first character of the level, uppercase (ASCII only) // [2016-05-02|11:06:44.322] - our time format (see https://golang.org/src/time/format.go) // Stopping ... - message fmt.Fprintf(&enc.buf, "%c[%s] %-44s ", lvl[0]-32, time.Now().Format("2006-01-02|15:04:05.000"), msg) if module != unknown { enc.buf.WriteString("module=" + module + " ") } KeyvalueLoop: for i := 0; i < len(keyvals)-1; i += 2 { for _, j := range excludeIndexes { if i == j { continue KeyvalueLoop } } err := enc.EncodeKeyval(keyvals[i], keyvals[i+1]) if err == logfmt.ErrUnsupportedValueType { enc.EncodeKeyval(keyvals[i], fmt.Sprintf("%+v", keyvals[i+1])) //nolint:errcheck // no need to check error again } else if err != nil { return err } } // Add newline to the end of the buffer if err := enc.EndRecord(); err != nil { return err } // The Logger interface requires implementations to be safe for concurrent // use by multiple goroutines. For this implementation that means making // only one call to l.w.Write() for each call to Log. if _, err := l.w.Write(enc.buf.Bytes()); err != nil { return err } return nil }