gRPC server (Ruby) でinterceptorを用いたSentryインテグレーション実装

サービスの運用・保守にあたり、 Run-timeのエラーログ収集はとても重要なタスクです。
ログ全体からエラー発生部分を抽出し監視することもできますが、エラーログの収集に特化したサービスとしてSentryが便利です。
Web画面からスタックトレースを確認し、ユーザID等の補足情報をもとにバグ修正を行うことが可能です。

最近、golangでgRPCサーバーを実装する機会があり、エラートラッキングにSentryを採用しました。 十分に情報がまとまっておらず(Sentry x gRPCは事例がまだ少ない?)、実装にすこし手こずったのでその経験を記します。

またgRPCサーバーで簡単にSentryインテグレーションを行うためのライブラリsentry-raven-grpcを公開しましたのでその紹介をします。

Sentry 公式ドキュメント

golangのSDKに関してはまだ開発段階とのことですが、公式ドキュメントに簡単な説明が記されています。

とくにgolang標準のnet/httpライブラリを用いたWebアプリケーション実装に対しては、ミドルウェアも提供されています。 ただしpanicの利用が前提となっているようで、golangのスタンダードから外れている感じを否めません。

当然ながら、Sentryのgolang SDKのリポジトリを覗いてみても、gRPCサーバーのインテグレーションに関する説明は見当たりません。

愚直な方法

golangでgRPCサーバーを実装する場合、各サービスメソッドはserver structをレシーバとする関数(以降handlerと呼びます)として記述します。
📝 https://github.com/grpc/grpc-go/blob/v1.21.x/examples/helloworld/greeter_server/main.go#L40-L44

// server is used to implement helloworld.GreeterServer.
type server struct{}

// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    log.Printf("Received: %v", in.Name)
    return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}

愚直にSentryインテグレーションを行おうとすると、すべてのhandlerに対してエラー補足処理を記述する必要があります。 少し工夫してエラー補足処理をヘルパー関数として括りだすこともできますが、handlerごとにヘルパー関数の呼び出しが必要となりDRYでありません。

sentry-raven-grpc

本記事のメインです。

上記のとおり愚直な方法でgRPCサーバー向けのインテグレーションすると、handlerごとに処理が重複しDRYでないという問題がありました。
そこで、gRPCで利用可能な機能 "Interceptor" 用いることで上記問題を解決しSentryインテグレーションを実装しました。

golangのgRPCサーバー実装ではInterceptorが利用できます (オフトピックですがrubyでもInterceptor APIが提供されています)
Interceptorはhandlerをラップし、その実行前後に処理を挟むレイヤとして機能します。

Interceptorに関してはすでにいくつか記事が存在するため詳しい説明は割愛します。
ちなみにInterceptorの利用例としては下記のリポジトリにまとめられています。
https://github.com/grpc-ecosystem/go-grpc-middleware
handlerの実行前後に処理を挟み込めるという性質を活かし、認証・認可機能やモニタリングの用途として活用されているようです。

拙作 sentry-raven-grpcでもInterceptorを用いてSentryインテグレーションを行います。
難しいことは一切なく、たったひとつの図で説明が終わります。
handlerはpanicを利用する必要はなく、golangのお作法どおりに呼び出し元にerrorを返すように記述できます。 interceptor

メインとなるコードは高々10行程度しかありません。 引数として渡されるhandlerを実行し、返されるerrorを捕捉してsentryに報告します。
📝 https://github.com/sat0yu/sentry-raven-grpc/blob/1e8cb0c/interceptor.go#L13-L22

ポイントとして、sentry-raven-grpc では新たに ErrorCapturer インタフェースを定義し、SDKが提供する捕捉メソッド CaptureErrorを抽象化しています。 これによりSDK部分をmockしてテストが書きやすくしています。

セットアップは非常に簡単で、Sentry DSNを渡してServerOptionを生成しgRPCサーバー生成時に設定するだけで済みます。 具体的にはこちらのcommitが参考になるかと思います。

StackTraceが途中で切れる

Sentryインテグレーション自体はできたものの、いざ収集されたエラーを確認してみるとStackTraceが不十分という問題がありました。
Sentry上ではsentry-raven-grpcで補足した時点からのStackTraceしか確認できず、バグ修正には全く意味のない情報です。
この問題はgolang標準の"errors"ではなくgithub.com/pkg/errorsを利用することで回避できます。

StackTraceが消えてしまう(集められない)理由に関する詳しい調査はできていませんが、判明し次第追記するつもりです。