5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[Go]Sentryに対応したcustom errorの作り方

Last updated at Posted at 2020-10-22

はじめに

go v1.x の 標準erorrはシンプルなゆえに欲しい機能が足りていない事が多く
標準errorをより使いやすくしたpkg/errors等が存在しますが、それでもerror自体に特定のステータス(status codeやerror levelなど)を保持したい場合等はそれ専用のcustom errorを作る事になると思います。

それ自体は良いのですが、errorが発生した際にそのerrorをSentryに通知したい場合
sentry-goCaptureException()ではStacktraceの取得に以下のpackageを使用する事が前提になっています。

今回はcustom errorを使用してSentryへStacktraceを表示させるための実装を試しました。

sentry-goのソースを読む

sentry-goには大きく以下の3つのCapture方法があり

基本的にはCaptureExceptionCaptureMessageを使用すると思いますが
ソースコードを読んで分かる通りCaptureException CaptureMessageではEventの作成のみオリジナルの処理で最終的には全てCaptureEventが呼ばれています。

今回Stacktraceをcaptureする処理として重要なのは
CaptureException内でEventのStacktraceを取得しているExtractStacktraceです。

見てわかる通り、reflectionで各error packageのStacktrace実装から決め打ちでStacktraceを取得しています。
要するにcustom errorで各packagesのStacktraceの実装と同じInterfaceを実装すればSentryでStacktraceを取得できるはずです。

custom errorをSentryに対応させる

元々作成していたcustom errorはpkg/errorsをベースに拡張されたものでしたのでpkg/errorsのStacktraceのInterfaceを実装していきます。

custom errorにpkg/errorsのStacktrace methodを実装する

Sentryがreflectionで呼んでいるcustom errorに実装すべきmethodはこちら

// 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はスタックトレースの各フレーム情報を指します。StackTraceはその集合体です。
上記を実装しただけではcustom errorのstackには何も情報を持たないため
errorが発生した際にgolangのruntime情報からFrameを作成する必要があります。

pkg/errorsの関数をそのまま使えればよかったのですがpkg/errorsでStacktraceを取得するcallersはprivateな関数であるためcustom errorにも同様の処理をそのまま実装する必要があります。
error作成時にStacktraceを取得する実装は以下の通り。

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は取得するStacktraceの深さ、runtime.Callers() のパラメータの「4」はStacktraceにerror package内の情報までstackしないようにskipするstack数を示しています。
このskip数はerror packagesの実装により異なるのでcallers()を呼ぶまでのネストの数を調べましょう。

ちなみにGo1.7以上である場合Stacktrace(runtime.Frames)を取得するruntime.CallersFrames()関数が追加されている為そちらを利用する事も可能です。
https://golang.org/pkg/runtime/#example_Frames

Stacktrace実装例として
errorにgprc.statusを持たせたサンプルは以下の通り。

error.go
type CustomError interface {
	Error() string
	Status() *status.Status
}

type customError struct {
	status  *status.Status
	*stack // ここで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()}
}

custom error以外のorigin errorのStacktraceを取得する

そのアプリ内でcustom errorしか使わない場合は上記の実装だけで問題ないですが
実際のアプリでは別のサブシステムやlibrary内で起きたoriginのerrorを保持する必要が出て来ると思います。
その場合、custom errorのstackはoriginのerrorのStacktraceを引き継がないといけません。

今回の場合サブシステム内で使用されているerror packageはpkg/errorsであるとしましょう。
pkg/errorsのStacktraceを取得する方法はpkg/errorsのソースコードを眺めているとコメントに細かく記載されています。
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.

上記を参考にしつつorigin errorであるpkg/errorsのStacktraceを取得してstackに詰めなおすには以下のように実装します。

error.go
type CustomError interface {
	Error() string
	Status() *status.Status
	Origin() error
}

type customError struct {
	status  *status.Status
	origin  error // 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()}
}

origin errorがpkg/errorsだった場合はpkg/errorsのStackTrace実装を呼び出してFrameを取得してからその値をプログラムカウンタの値に一度変換してstackに格納します。
勿論サブシステムがpkg/errors以外のerror packageを使用している場合そのpackage毎にStacktraceの実装は異なるので別途対応が必要です。

おわりに

特定のlibraryを拡張してcustom errorを実装すること自体は割と簡単に行えますが
Sentry等サードパーティ製のlibraryを使用してそのcustom errorを使用する際は多くのerror libraryのお作法に沿って作成してあげないと正常に動作しない事があります。
特にcustom errorを実装する際にはStacktraceをきちんと実装する事を忘れないように気をつけましょう。

おまけ

コード周りもう少し細かく書いたものをこちらに載せています。
https://zenn.dev/tomtwinkle/articles/18447cca3232d07c9f12

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?