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

# はじめに

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

それ自体は良いのですが、errorが発生した際にそのerrorをSentryに通知したい場合
`sentry-go`の[`CaptureException()`](https://godoc.org/github.com/getsentry/sentry-go#Client.CaptureException)ではStacktraceの取得に以下のpackageを使用する事が前提になっています。
- [pingcap/errors](https://github.com/pingcap/errors)
- [pkg/errors](https://github.com/pkg/errors)
- [go-errors/errors](https://github.com/go-errors/errors)

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

# sentry-goのソースを読む

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

- [CaptureMessage](https://github.com/getsentry/sentry-go/blob/v0.7.0/client.go#L260) : 文字メッセージの通知
- [CaptureException](https://github.com/getsentry/sentry-go/blob/v0.7.0/client.go#L266) : errorの通知
- [CaptureEvent](https://github.com/getsentry/sentry-go/blob/v0.7.0/client.go#L276) : custom可能なイベントの通知

基本的には`CaptureException`か`CaptureMessage`を使用すると思いますが
ソースコードを読んで分かる通り`CaptureException` `CaptureMessage`では[Event](https://godoc.org/github.com/getsentry/sentry-go#Event)の作成のみオリジナルの処理で最終的には全て`CaptureEvent`が呼ばれています。

今回Stacktraceをcaptureする処理として重要なのは
`CaptureException`内でEventのStacktraceを取得している[`ExtractStacktrace`](https://github.com/getsentry/sentry-go/blob/v0.7.0/stacktrace.go#L50)です。

- [ExtractStacktrace](https://github.com/getsentry/sentry-go/blob/v0.7.0/stacktrace.go#L50)

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

# custom errorをSentryに対応させる

元々作成していたcustom errorは[pkg/errors](https://github.com/pkg/errors)をベースに拡張されたものでしたので[pkg/errors](https://github.com/pkg/errors)のStacktraceのInterfaceを実装していきます。

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

Sentryがreflectionで呼んでいるcustom errorに実装すべきmethodはこちら
- [StackTrace](https://github.com/pkg/errors/blob/master/stack.go#L155)

```go
// 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を取得する実装は以下の通り。

```go
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を持たせたサンプルは以下の通り。

```go: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](https://github.com/pkg/errors)であるとしましょう。
[pkg/errors](https://github.com/pkg/errors)のStacktraceを取得する方法は[pkg/errors](https://github.com/pkg/errors)のソースコードを眺めているとコメントに細かく記載されています。
https://github.com/pkg/errors/blob/v0.9.1/errors.go#L66

```go
// 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](https://github.com/pkg/errors)のStacktraceを取得してstackに詰めなおすには以下のように実装します。

```go: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