[Go]Sentryに対応したcustom errorの作り方
はじめに
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 : 文字メッセージの通知
// CaptureMessage captures an arbitrary message.
func (client *Client) CaptureMessage(message string, hint *EventHint, scope EventModifier) *EventID {
event := client.eventFromMessage(message, LevelInfo)
return client.CaptureEvent(event, hint, scope)
}
- CaptureException : errorの通知
// CaptureException captures an error.
func (client *Client) CaptureException(exception error, hint *EventHint, scope EventModifier) *EventID {
event := client.eventFromException(exception, LevelError)
return client.CaptureEvent(event, hint, scope)
}
- CaptureEvent : custom可能なイベントの通知
// CaptureEvent captures an event on the currently active client if any.
//
// The event must already be assembled. Typically code would instead use
// the utility methods like CaptureException. The return value is the
// event ID. In case Sentry is disabled or event was dropped, the return value will be nil.
func (client *Client) CaptureEvent(event *Event, hint *EventHint, scope EventModifier) *EventID {
return client.processEvent(event, hint, scope)
}
基本的にはCaptureException
かCaptureMessage
を使用すると思いますが
ソースコードを読んで分かる通りCaptureException
CaptureMessage
ではEventの作成のみオリジナルの処理で最終的には全てCaptureEvent
が呼ばれています。
今回Stacktraceをcaptureする処理として重要なのは
CaptureException
内でEventのStacktraceを取得しているExtractStacktrace
です。
func ExtractStacktrace(err error) *Stacktrace {
// reflectionでStacktraceを取得するmethodを取得
method := extractReflectedStacktraceMethod(err)
var pcs []uintptr
if method.IsValid() {
// reflectionでStacktraceを取得
pcs = extractPcs(method)
} else {
pcs = extractXErrorsPC(err)
}
// 以下省略
}
func extractReflectedStacktraceMethod(err error) reflect.Value {
var method reflect.Value
// https://github.com/pingcap/errors
methodGetStackTracer := reflect.ValueOf(err).MethodByName("GetStackTracer")
// https://github.com/pkg/errors
methodStackTrace := reflect.ValueOf(err).MethodByName("StackTrace")
// https://github.com/go-errors/errors
methodStackFrames := reflect.ValueOf(err).MethodByName("StackFrames")
if methodGetStackTracer.IsValid() {
stacktracer := methodGetStackTracer.Call(make([]reflect.Value, 0))[0]
stacktracerStackTrace := reflect.ValueOf(stacktracer).MethodByName("StackTrace")
if stacktracerStackTrace.IsValid() {
method = stacktracerStackTrace
}
}
if methodStackTrace.IsValid() {
method = methodStackTrace
}
if methodStackFrames.IsValid() {
method = methodStackFrames
}
return method
}
見てわかる通り、reflectionで各error packageのStacktrace実装から決め打ちでStacktraceを取得しています。
要するにcustom errorで各packagesのStacktraceの実装と同じInterfaceを実装すればSentryでStacktraceを取得できるはずです。
custom errorをSentryに対応させる
元々作成していたcustom errorはpkg/errorsをベースに拡張されたものでしたのでpkg/errorsのStacktraceのInterfaceを実装していきます。
pkg/errors
のStacktrace methodを実装する
custom errorに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()
関数が追加されている為そちらを利用する事も可能です。
Stacktrace実装例として
errorにgprc.statusを持たせたサンプルは以下の通り。
package customerror
import (
"github.com/pkg/errors"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/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()}
}
func (e *customError) Error() string {
return fmt.Sprintf("[%s] %v", e.status.Code(), e.status.Message())
}
func (e *customError) Status() *status.Status {
return e.status
}
package customerror
import (
"fmt"
"github.com/pkg/errors"
"runtime"
)
type stack []uintptr
func (s *stack) StackTrace() errors.StackTrace {
f := make([]errors.Frame, len(*s))
for i := 0; i < len(f); i++ {
f[i] = errors.Frame((*s)[i])
}
return f
}
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
}
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のソースコードを眺めているとコメントに細かく記載されています。
// 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に詰めなおすには以下のように実装します。
package customerror
import (
"github.com/pkg/errors"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type CustomError interface {
Error() string
Status() *status.Status
Origin() error
}
type customError struct {
status *status.Status
origin error // origin errorを格納する
*stack
}
func NewCustomError(code codes.Code, message string, args ...interface{}) error {
return newCustomError(nil, code, message, args...))
}
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()}
}
func (e *customError) Error() string {
return fmt.Sprintf("[%s] %v", e.status.Code(), e.status.Message())
}
func (e *customError) Status() *status.Status {
return e.status
}
func (e *customError) Origin() error {
return e.origin
}
origin errorがpkg/errors
だった場合はpkg/errors
のStackTrace実装を呼び出してFrameを取得してからその値をプログラムカウンタの値に一度変換してstackに格納します。
勿論サブシステムがpkg/errors
以外のerror packageを使用している場合そのpackage毎にStacktraceの実装は異なるので別途対応が必要です。
おまけ:custom errorをSentryで使用する
Sentryでstacktraceを出力可能なcustom errorの実装が本記事のメインですので
Sentryでcaptureする部分の実装はおまけですが、実際には以下のように使用します。
func main() {
if err := Hoge(); err != nil {
captureSentryExceptions(err)
}
}
func captureSentryExceptions(err error) {
if customErr, ok := errors.Cause(err).(customerror.CustomError); ok {
sentry.CaptureException(customErr)
} else {
sentry.CaptureException(err)
}
}
ただし上記の実装では仮にorigin errorがあった場合でもSentryのタイトルにはcustomErrorのtypeが表示されてしまうため、origin errorがある場合にそちらのtypeをタイトルに設定したい場合はsentry.CaptureEvent()
を使うと良いでしょう。
func main() {
if err := Hoge(); err != nil {
captureSentryExceptions(err)
}
}
func captureSentryExceptions(err error) {
if customErr, ok := errors.Cause(err).(customerror.CustomError); ok {
var errorType string
if originErr := customErr.Origin(); oe != nil {
// if has origin error, use origin error value and type
errorType = reflect.TypeOf(originErr).String()
} else {
errorType = reflect.TypeOf(customErr).String()
}
event := sentry.NewEvent()
event.Level = sentry.LevelError
event.Exception = []sentry.Exception{{
Value: customErr.Error(),
Type: errorType,
Stacktrace: sentry.ExtractStacktrace(customErr),
}}
sentry.CaptureEvent(event)
} else {
sentry.CaptureException(err)
}
}
おわりに
特定のlibraryを拡張してcustom errorを実装すること自体は割と簡単に行えますが
Sentry等サードパーティ製のlibraryを使用してそのcustom errorを使用する際は多くのerror libraryのお作法に沿って作成してあげないと正常に動作しない事があります。
特にcustom errorを実装する際にはStacktraceをきちんと実装する事を忘れないように気をつけましょう。
Discussion