From be14ffa17b1a12ecf5e7e7fb0b8372d6e8d84ffb Mon Sep 17 00:00:00 2001 From: Jonathan Hall Date: Sun, 27 Nov 2016 07:56:47 +0100 Subject: [PATCH] Add support for github.com/pkg/errors interfaces (#23) * Spelling correction in variable name * Add support for github.com/pkg/errors.causer interface * Add support for github.com/pkg/errors.stackTracer interface When this interface is satisfied, we can now fetch the stack trace from the error, same as the Stacktracer interface. --- sentry.go | 56 +++++++++++++++++++++++++++++++++++++++++++++++--- sentry_test.go | 40 ++++++++++++++++++++++++++++++++---- 2 files changed, 89 insertions(+), 7 deletions(-) diff --git a/sentry.go b/sentry.go index f0f0a94..525d100 100644 --- a/sentry.go +++ b/sentry.go @@ -4,10 +4,12 @@ import ( "encoding/json" "fmt" "net/http" + "runtime" "time" "github.com/Sirupsen/logrus" "github.com/getsentry/raven-go" + "github.com/pkg/errors" ) var ( @@ -36,10 +38,19 @@ type SentryHook struct { extraFilters map[string]func(interface{}) interface{} } +// The Stacktracer interface allows an error type to return a raven.Stacktrace. type Stacktracer interface { GetStacktrace() *raven.Stacktrace } +type causer interface { + Cause() error +} + +type pkgErrorStackTracer interface { + StackTrace() errors.StackTrace +} + // StackTraceConfiguration allows for configuring stacktraces type StackTraceConfiguration struct { // whether stacktraces should be enabled @@ -130,9 +141,8 @@ func (hook *SentryHook) Fire(entry *logrus.Entry) error { if stConfig.Enable && entry.Level <= stConfig.Level { if err, ok := getAndDelError(d, logrus.ErrorKey); ok { var currentStacktrace *raven.Stacktrace - if stacktracer, ok := err.(Stacktracer); ok { - currentStacktrace = stacktracer.GetStacktrace() - } else { + currentStacktrace, err = hook.findStacktraceAndCause(err) + if currentStacktrace == nil { currentStacktrace = raven.NewStacktrace(stConfig.Skip, stConfig.Context, stConfig.InAppPrefixes) } exc := raven.NewException(err, currentStacktrace) @@ -167,6 +177,46 @@ func (hook *SentryHook) Fire(entry *logrus.Entry) error { return nil } +func (hook *SentryHook) findStacktraceAndCause(err error) (*raven.Stacktrace, error) { + errCause := errors.Cause(err) + var stacktrace *raven.Stacktrace + var stackErr errors.StackTrace + for err != nil { + // Find the earliest *raven.Stacktrace, or error.StackTrace + if tracer, ok := err.(Stacktracer); ok { + stacktrace = tracer.GetStacktrace() + stackErr = nil + } else if tracer, ok := err.(pkgErrorStackTracer); ok { + stacktrace = nil + stackErr = tracer.StackTrace() + } + if cause, ok := err.(causer); ok { + err = cause.Cause() + } else { + break + } + } + if stackErr != nil { + stacktrace = hook.convertStackTrace(stackErr) + } + return stacktrace, errCause +} + +// convertStackTrace converts an errors.StackTrace into a natively consumable +// *raven.Stacktrace +func (hook *SentryHook) convertStackTrace(st errors.StackTrace) *raven.Stacktrace { + stConfig := &hook.StacktraceConfiguration + stFrames := []errors.Frame(st) + frames := make([]*raven.StacktraceFrame, 0, len(stFrames)) + for i := range stFrames { + pc := uintptr(stFrames[i]) + fn := runtime.FuncForPC(pc) + file, line := fn.FileLine(pc) + frames = append(frames, raven.NewStacktraceFrame(pc, file, line, stConfig.Context, stConfig.InAppPrefixes)) + } + return &raven.Stacktrace{Frames: frames} +} + // Levels returns the available logging levels. func (hook *SentryHook) Levels() []logrus.Level { return hook.levels diff --git a/sentry_test.go b/sentry_test.go index e30684d..53f4be0 100644 --- a/sentry_test.go +++ b/sentry_test.go @@ -17,6 +17,7 @@ import ( "github.com/Sirupsen/logrus" "github.com/getsentry/raven-go" + pkgerrors "github.com/pkg/errors" ) const ( @@ -196,7 +197,7 @@ func TestSentryStacktrace(t *testing.T) { hook.StacktraceConfiguration.Enable = true logger.Error(message) // this is the call that the last frame of stacktrace should capture - expectedLineno := 198 //this should be the line number of the previous line + expectedLineno := 199 //this should be the line number of the previous line packet = <-pch stacktraceSize = len(packet.Stacktrace.Frames) @@ -240,9 +241,40 @@ func TestSentryStacktrace(t *testing.T) { if packet.Exception.Stacktrace != nil { frames = packet.Exception.Stacktrace.Frames } - if len(frames) != 1 || frames[0].Filename != escpectedStackFrameFilename { + if len(frames) != 1 || frames[0].Filename != expectedStackFrameFilename { t.Error("Stacktrace should be taken from err if it implements the Stacktracer interface") } + + logger.WithError(pkgerrors.Wrap(myStacktracerError{}, "wrapped")).Error(message) // use an error that wraps a Stacktracer + packet = <-pch + if packet.Exception.Stacktrace != nil { + frames = packet.Exception.Stacktrace.Frames + } + expectedCulprit := "myStacktracerError!" + if packet.Culprit != expectedCulprit { + t.Errorf("Expected culprit of '%s', got '%s'", expectedCulprit, packet.Culprit) + } + if len(frames) != 1 || frames[0].Filename != expectedStackFrameFilename { + t.Error("Stacktrace should be taken from err if it implements the Stacktracer interface") + } + + logger.WithError(pkgerrors.New("errorX")).Error(message) // use an error that implements pkgErrorStackTracer + packet = <-pch + if packet.Exception.Stacktrace != nil { + frames = packet.Exception.Stacktrace.Frames + } + expectedPkgErrorsStackTraceFilename := "github.com/evalphobia/logrus_sentry/sentry_test.go" + expectedFrameCount := 5 + expectedCulprit = "errorX" + if packet.Culprit != expectedCulprit { + t.Errorf("Expected culprit of '%s', got '%s'", expectedCulprit, packet.Culprit) + } + if len(frames) != expectedFrameCount { + t.Errorf("Expected %d frames, got %d", expectedFrameCount, len(frames)) + } + if frames[0].Filename != expectedPkgErrorsStackTraceFilename { + t.Error("Stacktrace should be taken from err if it implements the pkgErrorStackTracer interface") + } }) } @@ -408,12 +440,12 @@ type myStacktracerError struct{} func (myStacktracerError) Error() string { return "myStacktracerError!" } -const escpectedStackFrameFilename = "errorFile.go" +const expectedStackFrameFilename = "errorFile.go" func (myStacktracerError) GetStacktrace() *raven.Stacktrace { return &raven.Stacktrace{ Frames: []*raven.StacktraceFrame{ - {Filename: escpectedStackFrameFilename}, + {Filename: expectedStackFrameFilename}, }, } }