[Go] How to create a custom error for Sentry

Introduction

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.

Read the source of sentry-go

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.

Make custom error correspond to 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).

Implement the pkg / errors Stacktrace method for custom error

Click 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()}
}

Get Stacktrace of origin error other than custom error

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.

in conclusion

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.

bonus

Here is a little more detailed description around the code. https://zenn.dev/tomtwinkle/articles/18447cca3232d07c9f12

Recommended Posts

[Go] How to create a custom error for Sentry
How to create a shortcut command for LINUX
How to create a local repository for Linux OS
How to create a Conda package
How to create a virtual bridge
How to create a Dockerfile (basic)
How to create a config file
How to create a SAS token for Azure IoT Hub
How to create a clone from Github
How to create a git clone folder
How to create * .spec files for pyinstaller.
How to create a label (mask) for segmentation with labelme (semantic segmentation mask)
How to create a repository from media
How to make a Backtrader custom indicator
How to create a Python virtual environment (venv)
How to create a function object from a string
How to create a JSON file in Python
How to write a ShellScript Bash for statement
[Note] How to create a Ruby development environment
How to create a Kivy 1-line input box
How to create a multi-platform app with kivy
How to create a Rest Api in Django
[Go] How to write or call a function
[Note] How to create a Mac development environment
What I learned by implementing how to create a Default Box for SSD
Read the Python-Markdown source: How to create a parser
Create a dataset of images to use for learning
How to create a submenu with the [Blender] plugin
How to deploy a Go application to an ECS instance
How to build a development environment for TensorFlow (1.0.0) (Mac)
How to create a simple TCP server / client script
[Python] How to create a 2D histogram with Matplotlib
How to create a kubernetes pod from python code
How to call a function
How to hack a terminal
How to define Go variables
[Go] How to use "... (3 periods)"
[Go language] Try to create a uselessly multi-threaded line counter
How to manage a README for both github and PyPI
How to create a flow mesh around a cylinder with snappyHexMesh
How to define multiple variables in a python for statement
How to make a Python package (written for an intern)
[Python Kivy] How to create a simple pop up window
How to substitute a numerical value for a partial match (Note 1)
How to write a test for processing that uses BigQuery
I tried to create a bot for PES event notification
I want to create a Dockerfile for the time being.
How to set up WSL2 on Windows 10 and create a study environment for Linux commands
How to make a Japanese-English translation
How to write a Python class
Overview of how to create a server socket and how to establish a client socket
How to put a symbolic link
Steps to create a Django project
How to make a slack bot
How to create your own Transform
How to create an email user
How to make a crawler --Advanced
How to customize U-Boot with OSD335X on a custom board (memo)
How to make a recursive function
[Go] Create a CLI command to change the extension of the image
How to create a heatmap with an arbitrary domain in Python