From aa55558e4cb2e8f05cd079342d430f77e946d00a Mon Sep 17 00:00:00 2001 From: Olivier Poitrey Date: Thu, 3 Jan 2019 11:04:23 -0800 Subject: [PATCH] Add support for stack trace extration of error fields (#35) --- context.go | 14 ++++++++ event.go | 48 ++++++++++++++++++++------ globals.go | 11 ++++++ pkgerrors/stacktrace.go | 65 ++++++++++++++++++++++++++++++++++++ pkgerrors/stacktrace_test.go | 41 +++++++++++++++++++++++ 5 files changed, 169 insertions(+), 10 deletions(-) create mode 100644 pkgerrors/stacktrace.go create mode 100644 pkgerrors/stacktrace_test.go diff --git a/context.go b/context.go index b843179..f807802 100644 --- a/context.go +++ b/context.go @@ -362,6 +362,20 @@ func (c Context) Caller() Context { return c } +type stackTraceHook struct{} + +func (sh stackTraceHook) Run(e *Event, level Level, msg string) { + e.Stack() +} + +var sh = stackTraceHook{} + +// Stack enables stack trace printing for the error passed to Err(). +func (c Context) Stack() Context { + c.l = c.l.Hook(sh) + return c +} + // IPAddr adds IPv4 or IPv6 Address to the context func (c Context) IPAddr(key string, ip net.IP) Context { c.l.context = enc.AppendIPAddr(enc.AppendKey(c.l.context, key), ip) diff --git a/event.go b/event.go index a0e090a..d9462b0 100644 --- a/event.go +++ b/event.go @@ -18,11 +18,6 @@ var eventPool = &sync.Pool{ }, } -// ErrorMarshalFunc allows customization of global error marshaling -var ErrorMarshalFunc = func(err error) interface{} { - return err -} - // Event represents a log event. It is instanced by one of the level method of // Logger and finalized by the Msg or Msgf method. type Event struct { @@ -30,6 +25,7 @@ type Event struct { w LevelWriter level Level done func(msg string) + stack bool // enable error stack trace ch []Hook // hooks from context } @@ -271,8 +267,10 @@ func (e *Event) RawJSON(key string, b []byte) *Event { // AnErr adds the field key with serialized err to the *Event context. // If err is nil, no field is added. func (e *Event) AnErr(key string, err error) *Event { - marshaled := ErrorMarshalFunc(err) - switch m := marshaled.(type) { + if e == nil { + return e + } + switch m := ErrorMarshalFunc(err).(type) { case nil: return e case LogObjectMarshaler: @@ -292,11 +290,9 @@ func (e *Event) Errs(key string, errs []error) *Event { if e == nil { return e } - arr := Arr() for _, err := range errs { - marshaled := ErrorMarshalFunc(err) - switch m := marshaled.(type) { + switch m := ErrorMarshalFunc(err).(type) { case LogObjectMarshaler: arr = arr.Object(m) case error: @@ -314,10 +310,42 @@ func (e *Event) Errs(key string, errs []error) *Event { // Err adds the field "error" with serialized err to the *Event context. // If err is nil, no field is added. // To customize the key name, change zerolog.ErrorFieldName. +// +// To customize the key name, change zerolog.ErrorFieldName. +// +// If Stack() has been called before and zerolog.ErrorStackMarshaler is defined, +// the err is passed to ErrorStackMarshaler and the result is appended to the +// zerolog.ErrorStackFieldName. func (e *Event) Err(err error) *Event { + if e == nil { + return e + } + if e.stack && ErrorStackMarshaler != nil { + switch m := ErrorStackMarshaler(err).(type) { + case nil: + case LogObjectMarshaler: + e.Object(ErrorStackFieldName, m) + case error: + e.Str(ErrorStackFieldName, m.Error()) + case string: + e.Str(ErrorStackFieldName, m) + default: + e.Interface(ErrorStackFieldName, m) + } + } return e.AnErr(ErrorFieldName, err) } +// Stack enables stack trace printing for the error passed to Err(). +// +// ErrorStackMarshaler must be set for this method to do something. +func (e *Event) Stack() *Event { + if e != nil { + e.stack = true + } + return e +} + // Bool adds the field key with val as a bool to the *Event context. func (e *Event) Bool(key string, b bool) *Event { if e == nil { diff --git a/globals.go b/globals.go index 1c66904..e9d3e77 100644 --- a/globals.go +++ b/globals.go @@ -22,6 +22,17 @@ var ( // CallerSkipFrameCount is the number of stack frames to skip to find the caller. CallerSkipFrameCount = 2 + // ErrorStackFieldName is the field name used for error stacks. + ErrorStackFieldName = "stack" + + // ErrorStackMarshaler extract the stack from err if any. + ErrorStackMarshaler func(err error) interface{} + + // ErrorMarshalFunc allows customization of global error marshaling + ErrorMarshalFunc = func(err error) interface{} { + return err + } + // TimeFieldFormat defines the time format of the Time field type. // If set to an empty string, the time is formatted as an UNIX timestamp // as integer. diff --git a/pkgerrors/stacktrace.go b/pkgerrors/stacktrace.go new file mode 100644 index 0000000..01420e6 --- /dev/null +++ b/pkgerrors/stacktrace.go @@ -0,0 +1,65 @@ +package pkgerrors + +import ( + "github.com/pkg/errors" +) + +var ( + StackSourceFileName = "source" + StackSourceLineName = "line" + StackSourceFunctionName = "func" +) + +type state struct { + b []byte +} + +// Write implement fmt.Formatter interface. +func (s *state) Write(b []byte) (n int, err error) { + s.b = b + return len(b), nil +} + +// Width implement fmt.Formatter interface. +func (s *state) Width() (wid int, ok bool) { + return 0, false +} + +// Precision implement fmt.Formatter interface. +func (s *state) Precision() (prec int, ok bool) { + return 0, false +} + +// Flag implement fmt.Formatter interface. +func (s *state) Flag(c int) bool { + return false +} + +func frameField(f errors.Frame, s *state, c rune) string { + f.Format(s, c) + return string(s.b) +} + +// MarshalStack implements pkg/errors stack trace marshaling. +// +// zerolog.ErrorStackMarshaler = MarshalStack +func MarshalStack(err error) interface{} { + type stackTracer interface { + StackTrace() errors.StackTrace + } + sterr, ok := err.(stackTracer) + if !ok { + return nil + } + st := sterr.StackTrace() + s := &state{} + out := make([]map[string]string, 0, len(st)) + for _, frame := range st { + out = append(out, map[string]string{ + StackSourceFileName: frameField(frame, s, 's'), + StackSourceLineName: frameField(frame, s, 'd'), + StackSourceFunctionName: frameField(frame, s, 'n'), + }) + } + return out +} diff --git a/pkgerrors/stacktrace_test.go b/pkgerrors/stacktrace_test.go new file mode 100644 index 0000000..e771317 --- /dev/null +++ b/pkgerrors/stacktrace_test.go @@ -0,0 +1,41 @@ +// +build !binary_log + +package pkgerrors + +import ( + "bytes" + "regexp" + "testing" + + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +func TestLogStack(t *testing.T) { + zerolog.ErrorStackMarshaler = MarshalStack + + out := &bytes.Buffer{} + log := zerolog.New(out) + + err := errors.Wrap(errors.New("error message"), "from error") + log.Log().Stack().Err(err).Msg("") + + got := out.String() + want := `\{"stack":\[\{"func":"TestLogStack","line":"20","source":"stacktrace_test.go"\},.*\],"error":"from error: error message"\}\n` + if ok, _ := regexp.MatchString(want, got); !ok { + t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want) + } +} + +func BenchmarkLogStack(b *testing.B) { + zerolog.ErrorStackMarshaler = MarshalStack + out := &bytes.Buffer{} + log := zerolog.New(out) + err := errors.Wrap(errors.New("error message"), "from error") + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + log.Log().Stack().Err(err).Msg("") + out.Reset() + } +}