はじめに
go v1.x の 標準erorrはシンプルなゆえに欲しい機能が足りていない事が多く
標準errorをより使いやすくしたpkg/errors
等が存在しますが、それでもerror自体に特定のステータス(status codeやerror levelなど)を保持したい場合等はそれ専用のcustom errorを作る事になると思います。
それ自体は良いのですが、errorが発生した際にそのerrorをSentryに通知したい場合
sentry-go
のCaptureException()
ではStacktraceの取得に以下のpackageを使用する事が前提になっています。
今回はcustom errorを使用してSentryへStacktraceを表示させるための実装を試しました。
sentry-goのソースを読む
sentry-goには大きく以下の3つのCapture方法があり
- CaptureMessage : 文字メッセージの通知
- CaptureException : errorの通知
- CaptureEvent : custom可能なイベントの通知
基本的にはCaptureException
かCaptureMessage
を使用すると思いますが
ソースコードを読んで分かる通り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を持たせたサンプルは以下の通り。
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に詰めなおすには以下のように実装します。
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