The standard erorr of go v1.x is so simple that it often lacks the features you want.
There are pkg / errors
etc. that make standard error easier to use, but the error itself still has a specific status (status code, error level, etc.). If you want to keep it, you will need to create a custom error for it.
That's fine in itself, but if you want to notify Sentry of an error when it occurs
In sentry-go
's CaptureException ()
, it is assumed that the following package is used to get the Stacktrace. It has become.
This time I tried the implementation to display Stacktrace in Sentry using custom error.
sentry-go has the following three capture methods
-CaptureMessage: Notification of text message -CaptureException: Error notification -CaptureEvent: Customizable event notification
I think you basically use CaptureException
or CaptureMessage
As you can see by reading the source code, CaptureException`` CaptureMessage
only creates Event and is the original process. CaptureEvent
is called.
What is important as the process to capture Stacktrace this time
It is [ʻExtractStacktrace](https://github.com/getsentry/sentry-go/blob/v0.7.0/stacktrace.go#L50) that is getting the Stacktrace of the Event in
CaptureException`.
As you can see, reflection gets the Stacktrace from the Stacktrace implementation of each error package. In short, if you implement the same Interface as the Stacktrace implementation of each package with a custom error, you should be able to get the Stacktrace in Sentry.
The custom error that was originally created was extended based on pkg / errors, so pkg/errors We will implement the Stacktrace Interface of pkg / errors).
pkg / errors
Stacktrace method for custom errorClick here for the method to be implemented for the custom error that Sentry calls in reflection
// Frame represents a program counter inside a stack frame.
// For historical reasons if Frame is interpreted as a uintptr
// its value represents the program counter + 1.
type Frame uintptr
// StackTrace is stack of Frames from innermost (newest) to outermost (oldest).
type StackTrace []Frame
// stack represents a stack of program counters.
type stack []uintptr
func (s *stack) StackTrace() StackTrace {
f := make([]Frame, len(*s))
for i := 0; i < len(f); i++ {
f[i] = Frame((*s)[i])
}
return f
}
Frame refers to each frame information of the stack trace. StackTrace is the collection. Just implementing the above doesn't have any information in the custom error stack It is necessary to create a Frame from the runtime information of golang when an error occurs.
I wish I could use the function of pkg / errors
as it is, but since callers
which gets Stacktrace with pkg / errors
is a private function, it is necessary to implement the same processing for custom error as it is.
The implementation to get Stacktrace when creating an error is as follows.
func callers() *stack {
const depth = 32
const skip = 4
var pcs [depth]uintptr
n := runtime.Callers(skip, pcs[:])
var st stack = pcs[0:n]
return &st
}
depth
indicates the depth of the Stacktrace to be acquired, and the parameter" 4 "ofruntime.Callers ()
indicates the number of stacks to be skipped to Stacktrace so that the information in the error package is not stacked.
This number of skips depends on the implementation of error packages, so check the number of nests before calling callers ().
By the way, if you have Go 1.7 or above, you can also use the runtime.CallersFrames ()
function that gets Stacktrace (runtime.Frames) because it has been added.
https://golang.org/pkg/runtime/#example_Frames
As an example of Stacktrace implementation The sample with gprc.status in error is as follows.
error.go
type CustomError interface {
Error() string
Status() *status.Status
}
type customError struct {
status *status.Status
*stack //The point here is to implement the Stacktrace method
}
func NewCustomError(code codes.Code, message string, args ...interface{}) error {
return newCustomError(nil, code, message, args...))
}
func newCustomError(code codes.Code, message string, args ...interface{}) error {
s := status.Newf(code, message, args...)
return &customError{s, callers()}
}
If you use only custom error in the app, the above implementation is fine. In a real app, you'll probably need to keep the origin error that occurred in another subsystem or library. In that case, the custom error stack must inherit the origin error Stacktrace.
In this case, let's say the error package used in the subsystem is pkg / errors. To get the Stacktrace for pkg / errors, look at the source code for pkg / errors. It is described in detail in the comment. https://github.com/pkg/errors/blob/v0.9.1/errors.go#L66
// Retrieving the stack trace of an error or wrapper
//
// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are
// invoked. This information can be retrieved with the following interface:
//
// type stackTracer interface {
// StackTrace() errors.StackTrace
// }
//
// The returned errors.StackTrace type is defined as
//
// type StackTrace []Frame
//
// The Frame type represents a call site in the stack trace. Frame supports
// the fmt.Formatter interface that can be used for printing information about
// the stack trace of this error. For example:
//
// if err, ok := err.(stackTracer); ok {
// for _, f := range err.StackTrace() {
// fmt.Printf("%+s:%d\n", f, f)
// }
// }
//
// Although the stackTracer interface is not exported by this package, it is
// considered a part of its stable public interface.
While referring to the above, to get the Stacktrace of pkg / errors which is the origin error and repack it in the stack, implement as follows.
error.go
type CustomError interface {
Error() string
Status() *status.Status
Origin() error
}
type customError struct {
status *status.Status
origin error //Store origin error
*stack
}
func NewCustomErrorFrom(origin error, code codes.Code, message string, args ...interface{}) error {
return newCustomError(origin, code, message, args...))
}
func newCustomError(origin error, code codes.Code, message string, args ...interface{}) error {
s := status.Newf(code, message, args...)
if origin != nil {
// https://github.com/pkg/errors
type stackTracer interface {
StackTrace() errors.StackTrace
}
if e, ok := origin.(stackTracer); ok {
originStack := make([]uintptr, len(e.StackTrace()))
for _, f := range e.StackTrace() {
originStack = append(originStack, uintptr(f))
}
var stack stack = originStack
return &applicationError{s, origin, &stack}
}
}
return &CustomError{s, origin, callers()}
}
If the origin error is pkg / errors
, call the StackTrace implementation of pkg / errors
to get the Frame, then convert the value to the value of the program counter once and store it in the stack.
Of course, if the subsystem uses an error package other than pkg / errors
, the Stacktrace implementation will be different for each package, so you need to take additional measures.
It's fairly easy to extend a particular library and implement a custom error, When using the custom error using a third party library such as Sentry, it may not work properly unless it is created according to the manners of many error libraries. Be careful not to forget to implement Stacktrace properly, especially when implementing custom errors.
Here is a little more detailed description around the code. https://zenn.dev/tomtwinkle/articles/18447cca3232d07c9f12
Recommended Posts