Skip to content
This repository has been archived by the owner on Aug 30, 2023. It is now read-only.

Do you support github.com/pkg/errors stacktrace ? #88

Closed
pierrre opened this issue Jul 5, 2016 · 17 comments · May be fixed by #137
Closed

Do you support github.com/pkg/errors stacktrace ? #88

pierrre opened this issue Jul 5, 2016 · 17 comments · May be fixed by #137

Comments

@pierrre
Copy link

pierrre commented Jul 5, 2016

In go, errors don't have a stacktrace.
Errors reported with Sentry/raven-go show the stacktrace of the Capture call, instead of where the errors come from.

The github.com/pkg/errors package adds stack trace to Go errors. (you must wrap all errors with errors.Wrap)
I don't think you currently support the stacktrace format of this package.
Do you plan to support it?

@pierrre
Copy link
Author

pierrre commented Jul 11, 2016

Hello!

Here is the code I plan to use.
It works as a replacement for github.com/getsentry/raven-go.CaptureError*.

If we want to integrate it to raven-go directly, I think we should:

  • write another package that exposes getErrorStackTraceConverted()
  • allow to register an "error to stacktrace" converter

By the way, I also fixed:

  • the "skip" issue Stack trace, wrong skip #91
  • all Capture* methods now return an error or a <- chan error (to be consistent with Client.Capture)
// Package ravenerrors adds support for github.com/pkg/errors's stacktrace to github.com/getsentry/raven-go.
//
// It works as a replacement for github.com/getsentry/raven-go.CaptureError*.
// Stacktraces are extracted from the error if available and replace raven's default behavior.
package ravenerrors

import (
    "runtime"

    "github.com/getsentry/raven-go"
    "github.com/pkg/errors"
)

// Capture is a replacement for github.com/getsentry/raven-go.CaptureError.
func Capture(myerr error, tags map[string]string, interfaces ...raven.Interface) (string, <-chan error) {
    return capture(raven.DefaultClient, myerr, tags, interfaces...)
}

// CaptureWithClient is a replacement for github.com/getsentry/raven-go.Client.CaptureError.
func CaptureWithClient(client *raven.Client, myerr error, tags map[string]string, interfaces ...raven.Interface) (string, <-chan error) {
    return capture(client, myerr, tags, interfaces...)
}

// CaptureAndWait is a replacement for github.com/getsentry/raven-go.CaptureErrorAndWait.
func CaptureAndWait(myerr error, tags map[string]string, interfaces ...raven.Interface) (string, error) {
    eventID, errch := capture(raven.DefaultClient, myerr, tags, interfaces...)
    err := <-errch
    return eventID, err
}

// CaptureAndWaitWithClient is a replacement for github.com/getsentry/raven-go.Client.CaptureErrorAndWait.
func CaptureAndWaitWithClient(client *raven.Client, myerr error, tags map[string]string, interfaces ...raven.Interface) (string, error) {
    eventID, errch := capture(client, myerr, tags, interfaces...)
    err := <-errch
    return eventID, err
}

func capture(client *raven.Client, myerr error, tags map[string]string, interfaces ...raven.Interface) (string, <-chan error) {
    st := getErrorStackTraceConverted(myerr, 3, client.IncludePaths())
    if st == nil {
        st = raven.NewStacktrace(2, 3, client.IncludePaths())
    }
    ex := raven.NewException(myerr, st)
    packet := raven.NewPacket(myerr.Error(), append(interfaces, ex)...)
    return client.Capture(packet, tags)
}

func getErrorStackTraceConverted(err error, context int, appPackagePrefixes []string) *raven.Stacktrace {
    st := getErrorCauseStackTrace(err)
    if st == nil {
        return nil
    }
    return convertStackTrace(st, context, appPackagePrefixes)
}

func getErrorCauseStackTrace(err error) errors.StackTrace {
    // This code is inspired by github.com/pkg/errors.Cause().
    var st errors.StackTrace
    for err != nil {
        s := getErrorStackTrace(err)
        if s != nil {
            st = s
        }
        err = getErrorCause(err)
    }
    return st
}

func getErrorStackTrace(err error) errors.StackTrace {
    ster, ok := err.(interface {
        StackTrace() errors.StackTrace
    })
    if !ok {
        return nil
    }
    return ster.StackTrace()
}

func getErrorCause(err error) error {
    cer, ok := err.(interface {
        Cause() error
    })
    if !ok {
        return nil
    }
    return cer.Cause()
}

func convertStackTrace(st errors.StackTrace, context int, appPackagePrefixes []string) *raven.Stacktrace {
    // This code is borrowed from github.com/getsentry/raven-go.NewStacktrace().
    var frames []*raven.StacktraceFrame
    for _, f := range st {
        frame := convertFrame(f, context, appPackagePrefixes)
        if frame != nil {
            frames = append(frames, frame)
        }
    }
    if len(frames) == 0 {
        return nil
    }
    for i, j := 0, len(frames)-1; i < j; i, j = i+1, j-1 {
        frames[i], frames[j] = frames[j], frames[i]
    }
    return &raven.Stacktrace{Frames: frames}
}

func convertFrame(f errors.Frame, context int, appPackagePrefixes []string) *raven.StacktraceFrame {
    // This code is borrowed from github.com/pkg/errors.Frame.
    pc := uintptr(f) - 1
    fn := runtime.FuncForPC(pc)
    var file string
    var line int
    if fn != nil {
        file, line = fn.FileLine(pc)
    } else {
        file = "unknown"
    }
    return raven.NewStacktraceFrame(pc, file, line, context, appPackagePrefixes)
}

@thoas
Copy link

thoas commented Dec 23, 2016

@pierrre is definitely right on this issue, if we don't have a good stacktrace on Sentry, it's completely useless to use it or to pay for it since you can't investigate properly with the stacktrace shown on Sentry.

@mattrobenolt are you planning to support pkg/errors or accepting PRs?

@pierrre
Copy link
Author

pierrre commented Dec 23, 2016

FYI, the latest version of my wrapper:

// Package ravenerrors adds support for github.com/pkg/errors's stacktrace to github.com/getsentry/raven-go.
//
// It works as a replacement for github.com/getsentry/raven-go.CaptureError*.
// Stacktraces are extracted from the error if available and replace raven's default behavior.
package ravenerrors

import (
	"runtime"

	"github.com/getsentry/raven-go"
	"github.com/pkg/errors"
)

// CaptureAndWait is a replacement for github.com/getsentry/raven-go.CaptureErrorAndWait.
func CaptureAndWait(myerr error, tags map[string]string, interfaces ...raven.Interface) (string, error) {
	return captureAndWait(raven.DefaultClient, myerr, 1, tags, interfaces...)
}

// CaptureAndWaitWithClient is a replacement for github.com/getsentry/raven-go.Client.CaptureErrorAndWait.
func CaptureAndWaitWithClient(client *raven.Client, myerr error, tags map[string]string, interfaces ...raven.Interface) (string, error) {
	return captureAndWait(client, myerr, 1, tags, interfaces...)
}

func captureAndWait(client *raven.Client, myerr error, skip int, tags map[string]string, interfaces ...raven.Interface) (string, error) {
	eventID, errch := capture(client, myerr, skip+1, tags, interfaces...)
	err := <-errch
	return eventID, err
}

// Capture is a replacement for github.com/getsentry/raven-go.CaptureError.
func Capture(myerr error, tags map[string]string, interfaces ...raven.Interface) (string, <-chan error) {
	return capture(raven.DefaultClient, myerr, 1, tags, interfaces...)
}

// CaptureWithClient is a replacement for github.com/getsentry/raven-go.Client.CaptureError.
func CaptureWithClient(client *raven.Client, myerr error, tags map[string]string, interfaces ...raven.Interface) (string, <-chan error) {
	return capture(client, myerr, 1, tags, interfaces...)
}

func capture(client *raven.Client, myerr error, skip int, tags map[string]string, interfaces ...raven.Interface) (string, <-chan error) {
	p := newPacket(client, myerr, skip+1, interfaces...)
	return raven.Capture(p, tags)
}

// NewPacket is a replacement for github.com/getsentry/raven-go.Client.NewPacket.
func NewPacket(myerr error, interfaces ...raven.Interface) *raven.Packet {
	return newPacket(raven.DefaultClient, myerr, 1, interfaces...)
}

// NewPacketWithClient is a replacement for github.com/getsentry/raven-go.NewPacket.
func NewPacketWithClient(client *raven.Client, myerr error, interfaces ...raven.Interface) *raven.Packet {
	return newPacket(client, myerr, 1, interfaces...)
}

func newPacket(client *raven.Client, myerr error, skip int, interfaces ...raven.Interface) *raven.Packet {
	ex := newException(client, myerr, skip+1)
	return raven.NewPacket(myerr.Error(), append(interfaces, ex)...)
}

// NewException is a replacement for github.com/getsentry/raven-go.NewException.
func NewException(myerr error) *raven.Exception {
	return newException(raven.DefaultClient, myerr, 1)
}

// NewExceptionWithClient is a replacement for github.com/getsentry/raven-go.NewException.
func NewExceptionWithClient(client *raven.Client, myerr error) *raven.Exception {
	return newException(client, myerr, 1)
}

func newException(client *raven.Client, myerr error, skip int) *raven.Exception {
	st := newStackTrace(client, myerr, skip+1)
	return raven.NewException(myerr, st)
}

// NewStackTrace is a replacement for github.com/getsentry/raven-go.NewStackTrace.
func NewStackTrace(myerr error) *raven.Stacktrace {
	return newStackTrace(raven.DefaultClient, myerr, 1)
}

// NewStackTraceWithClient is a replacement for github.com/getsentry/raven-go.NewStackTrace.
func NewStackTraceWithClient(client *raven.Client, myerr error) *raven.Stacktrace {
	return newStackTrace(client, myerr, 1)
}

func newStackTrace(client *raven.Client, myerr error, skip int) *raven.Stacktrace {
	st := getErrorStackTraceConverted(myerr, 3, client.IncludePaths())
	if st == nil {
		st = raven.NewStacktrace(skip+1, 3, client.IncludePaths())
	}
	return st
}

func getErrorStackTraceConverted(err error, context int, appPackagePrefixes []string) *raven.Stacktrace {
	st := getErrorCauseStackTrace(err)
	if st == nil {
		return nil
	}
	return convertStackTrace(st, context, appPackagePrefixes)
}

func getErrorCauseStackTrace(err error) errors.StackTrace {
	// This code is inspired by github.com/pkg/errors.Cause().
	var st errors.StackTrace
	for err != nil {
		s := getErrorStackTrace(err)
		if s != nil {
			st = s
		}
		err = getErrorCause(err)
	}
	return st
}

func getErrorStackTrace(err error) errors.StackTrace {
	ster, ok := err.(interface {
		StackTrace() errors.StackTrace
	})
	if !ok {
		return nil
	}
	return ster.StackTrace()
}

func getErrorCause(err error) error {
	cer, ok := err.(interface {
		Cause() error
	})
	if !ok {
		return nil
	}
	return cer.Cause()
}

func convertStackTrace(st errors.StackTrace, context int, appPackagePrefixes []string) *raven.Stacktrace {
	// This code is borrowed from github.com/getsentry/raven-go.NewStacktrace().
	var frames []*raven.StacktraceFrame
	for _, f := range st {
		frame := convertFrame(f, context, appPackagePrefixes)
		if frame != nil {
			frames = append(frames, frame)
		}
	}
	if len(frames) == 0 {
		return nil
	}
	for i, j := 0, len(frames)-1; i < j; i, j = i+1, j-1 {
		frames[i], frames[j] = frames[j], frames[i]
	}
	return &raven.Stacktrace{Frames: frames}
}

func convertFrame(f errors.Frame, context int, appPackagePrefixes []string) *raven.StacktraceFrame {
	// This code is borrowed from github.com/pkg/errors.Frame.
	pc := uintptr(f) - 1
	fn := runtime.FuncForPC(pc)
	var file string
	var line int
	if fn != nil {
		file, line = fn.FileLine(pc)
	} else {
		file = "unknown"
	}
	return raven.NewStacktraceFrame(pc, file, line, context, appPackagePrefixes)
}

@choonkeat
Copy link

Super thanks @pierrre I've updated your code to provide root error to Sentry instead. This allows the issue title to reflect the correct error type, e.g. *.os.PathError, instead of *errors.withStack

https://gist.github.com/choonkeat/f6d45c31ff4e9f84cdfb9c1c0d54f1de

+// https://github.com/getsentry/raven-go/issues/88#issuecomment-269002948
+//
 // Package ravenerrors adds support for github.com/pkg/errors's stacktrace to github.com/getsentry/raven-go.
 //
 // It works as a replacement for github.com/getsentry/raven-go.CaptureError*.
@@ -69,7 +71,7 @@ func NewExceptionWithClient(client *raven.Client, myerr error) *raven.Exception
 
 func newException(client *raven.Client, myerr error, skip int) *raven.Exception {
        st := newStackTrace(client, myerr, skip+1)
-       return raven.NewException(myerr, st)
+       return raven.NewException(errors.Cause(myerr), st)
 }
 
 // NewStackTrace is a replacement for github.com/getsentry/raven-go.NewStackTrace.

@pierrre
Copy link
Author

pierrre commented Mar 6, 2017

@choonkeat thank you ! :)
I'll update the code in my private repo.

BTW, I just saw this https://docs.google.com/presentation/d/1Wcblp3jpfeKwA0Y4FOmj63PW52M_qmNqlQkNaLj0P5o/edit#slide=id.g1b2157b5d1_3_229

@pierrre
Copy link
Author

pierrre commented Mar 6, 2017

@choonkeat actually, I disagree
here is the code for NewException:

func NewException(err error, stacktrace *Stacktrace) *Exception {
	msg := err.Error()
	ex := &Exception{
		Stacktrace: stacktrace,
		Value:      msg,
		Type:       reflect.TypeOf(err).String(),
	}
	if m := errorMsgPattern.FindStringSubmatch(msg); m != nil {
		ex.Module, ex.Value = m[1], m[2]
	}
	return ex
}

If you call raven.NewException(errors.Cause(myerr), st), the error message is truncated to the first error.
If I have to choose between a full error message and a correct error type, I choose the full error message.

Edit: Maybe I'm wrong, because raven.NewPacket is called with myerr.Error().
What is the purpose of the exception message ?

Edit2: OK, with my current code, the packet and exception messages are the same.
That's why the message is displayed twice on the web interface !
I'll try your code.

@choonkeat
Copy link

@pierrre My bad, didn't notice the "message". I've modified the gist to perform errors.Cause only at reflect.TypeOf

@blinsay
Copy link

blinsay commented Mar 13, 2017

+1 this would be really useful

@martinheidegger
Copy link

Note that there is pkg/errors#113 open that would help getting access to the stack frames!

@hiroshi
Copy link

hiroshi commented May 19, 2017

Hi, I created a WIP PR to address this issue.
#137
If you only use raven.CaptureError(), you can use it just replace import path from github.com/sentry/raven-go to github.com/nota/raven-go.

The code in this issue by @pierrre helped me lot. Thanks.

@alikhajeh1
Copy link

We're using CapturePanicAndWait and CaptureErrorAndWait so we couldn't use the gist or the fork mentioned above. We worked around it just now by getting the stack trace and sending it via CaptureMessage - not ideal but works for us until Sentry supports github.com/pkg/errors

  message := fmt.Sprintf("%+v", err)
  raven.CaptureMessageAndWait(message, map[string]string{"key": value})

@kamilogorek
Copy link
Contributor

Go SDK is now "error-package-agnostic" due to 238ebd8

@OGKevin
Copy link

OGKevin commented Feb 26, 2019

Im using the master branch but my stack traces are still not good. My stack traces only include the line of the code that is calling capture error. :(

e.g.

func (e *ErrorSentry) Report(entry errorreporting.Entry) {
	select {
	case <-e.ctx.Done():
		_ = e.Close()
		return
	default:
		if entry.Req != nil {
			e.c.SetHttpContext(raven.NewHttp(entry.Req))
		}
		if entry.User != "" {
			e.c.SetUserContext(&raven.User{ID: entry.User})
		}
		e.c.CaptureError(entry.Error, nil)
		e.c.ClearContext()
	}
}

my stack trace is always

  File "..../error.go", line 85, in Report

eg of code calling this method

	ctxEnc, err := base64.StdEncoding.DecodeString(user.Context)
	if err != nil {
		errEntry.Error = errors.Wrap(err, "could not decode user context")
		a.ErrReporter.Report(errEntry)
		return nil
	}

This is really annoying as almost all my events are merged into 1 even tho none of the errors/events have nothing to do with each other only that the stack trace is the same.

Does this have to do with the fact that im using a dockerized go binary without the source code?

@pierrre
Copy link
Author

pierrre commented Feb 26, 2019

I don't understand how the current code can work.
Which type is implementing this interface ?

	type stackTracer interface {
		StackTrace() []runtime.Frame
	}

I don't see anything like that in github.com/pkg/errors.

@OGKevin
Copy link

OGKevin commented Feb 26, 2019

@pierrre does #88 (comment) still work ? If it does than I would have to look into using it. Its a shame that the actual SDK doesn't work :(

@pierrre
Copy link
Author

pierrre commented Feb 26, 2019

This code should still work, but I've updated it to use the new runtime.Frames.
I can't give it, because it's from my company private source code. (and it has some dependencies to other internal packages)

@OGKevin
Copy link

OGKevin commented Mar 2, 2019

@pierrre Thanks! Your wrapper is providing me with stack traces! Thanks a lot :)

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants