以前、sentry-goの実装の記事を書いたが、そこからより実践に向けのエラーをハンドリング & 情報追加をしてみました。
Pull Request
実装
導入に関しては以前の記事で紹介しているので割愛 以下は、新たに実装したものについて記載
GraphQLのエラーハンドリングを追加 & GraphQL情報を追加
SetErrorPresenterを使用することでgqlgenのエラーハンドリングが行える(以下参照)
■ app.go
srv.SetErrorPresenter(func(ctx context.Context, e error) *gqlerror.Error { err := graphql.DefaultErrorPresenter(ctx, e) goc := graphql.GetOperationContext(ctx) sentry.WithScope(func(scope *sentry.Scope) { scope.SetTag("kind", "GraphQL") scope.SetTag("operationName", goc.OperationName) scope.SetExtra("query", goc.RawQuery) scope.SetExtra("variables", goc.Variables) if err.Path.String() != "" { sentry.AddBreadcrumb(&sentry.Breadcrumb{ Category: "GraphQL", Message: "Error Path:" + err.Path.String(), Level: sentry.LevelInfo, }) } sentry.CaptureException(e) }) return err })
Sentryに送信する際に、GraphQLの情報を追加
以下の2つでタグを追加
scope.SetTag("kind", "GraphQL") scope.SetTag("operationName", goc.OperationName)
QueryとVariablesは以下のように情報を追加
scope.SetExtra("query", goc.RawQuery) scope.SetExtra("variables", goc.Variables)
また、gqlerror.errorのPathをAddBreadcrumbすることで実際にエラーになっているQueryのPathを出力する
if err.Path.String() != "" { sentry.AddBreadcrumb(&sentry.Breadcrumb{ Category: "GraphQL", Message: "Error Path:" + err.Path.String(), Level: sentry.LevelInfo, }) }
と、ここまでで、GraphQLのエラーハンドリングの追加は完了。 ただ、これだけだと問題があり、Sentryでコードトレースされる箇所がSentryを送信した箇所になってしまうため画像の通り、実際のエラー箇所まで通知してくれない状態になってしまいます。
そこで、以下にSentryにスタックトレースの情報を送るようにする方法を記載します。
スタックトレースを追加
現状、標準errors
にはスタックトレースの機能が備わっていないので、pkg/errorsパッケージを使用して実装しました。
スタックトレースを追加する方法は簡単で以下のように既存のerrorをerrors.WithStackでWrapすればOKです。
if err != nil { return nil, errors.WithStack(err) }
このerrorをSentryに送信すると以下の通りスタックトレースの情報が付与さます。
こんな感じでスタックトレースを付与できました。
ただ、pkg/errorsパッケージには気をつけなくていない箇所があり以下の用に特定のerrorに対してハンドリングを行っているコードでは、errors.Cause
でWrapしないと正しく判定が行えないので注意
例)
// ↓エラーコードがNotFoundの場合 _, err := client.Collection("cities").Doc(c.id).Doc("SF").Get(ctx) log.Pringtln("status:" status.Code(err)) // → NotFound(正しく判定できている) err1 := errors.WithStack(err) log.Pringtln("status:" status.Code(err1)) // → Unknown(うまく判定できていない) log.Pringtln("status:" status.Code(errors.Cause(err1))) // → NotFound(正しく判定できている)
ユーザーIDをSentryに追加
以下の記事を参考にSentryからエラーになったユーザーを特定できるようにする
Identify Users for Go | Sentry Documentation
以下のコードを追加
func NewGraphWithSetUserID(app *Application, f *firestore.Client, uid string) *Graph { sentry.ConfigureScope(func(scope *sentry.Scope) { // 自身のユーザーIDを設定 scope.SetUser(sentry.User{ID: uid}) }) client := &Client{ UUID: &uuidgen.UUID{}, Time: &timegen.Time{}, AuthToken: &authToken.AuthToken{}, Task: task.NewNotificationTask(), } return &Graph{ UserID: uid, FirestoreClient: f, App: app, Client: client, } }
こちらを追加することで以下のようにSentryのタグが追加されます
Breadcrumbsでログ情報を追加
SentryのBreadcrumbsを使用することで、エラー発生までのログの情報を追加する事ができます。
memoirでは未認証状態でのAPIアクセスと認証済み状態でのAPIアクセスの2パターンが存在しているので、そちらの情報を記載するように修正すると以下のようになります。
if user.FirebaseUID == "" { // Firebase認証無し u, err := app.UserRepository.FindDatabaseDataByUID(ctx, f, user.ID) if err != nil { return nil, fmt.Errorf("User Invalid") } if u.FirebaseUID != "" { return nil, fmt.Errorf("need to firebase auth") } sentry.AddBreadcrumb(&sentry.Breadcrumb{ Category: "Auth", Message: "Not logged in, UserID: " + u.ID, Level: sentry.LevelInfo, }) } else { // Firebase認証有り u, err := app.UserRepository.FindByFirebaseUID(ctx, f, user.FirebaseUID) if err != nil { return nil, fmt.Errorf("firebase auth invalid") } sentry.AddBreadcrumb(&sentry.Breadcrumb{ Category: "Auth", Message: "Logged in, UserID: " + u.ID, Level: sentry.LevelInfo, }) user.ID = u.ID }
上記を設定することでSentryには以下のように情報が付与される
リリース毎のエラーデータ確認する
実際に運用してみると、どのリリースでエラーは増えたのか、新しく発生したのか知りたくなると思います。 そういう時は、Sentryの Releasesの機能を使用します。
実装的には以下のコードの通り、sentry.InitにReleaseにオプションを設定するだけでOKです
■ app.go
sco := sentry.ClientOptions{ Dsn: os.Getenv("SENTRY_DSN"), } if os.Getenv("APP_ENV") != "local" { sco.Release = os.Getenv("RELEASE_INSTANCE_VERSION") } err := sentry.Init(sco) if err != nil { log.Fatalf("sentry.Init: %s", err) }
memoirはGAEで運用しているのGitHub Actionsでリリースする際にGAEのサービスのバージョンと同様の値を環境変数に設定デプロイするようにしています。(今回実装したのがレビュー環境なので日付データを元に作成していますが本番環境ならばGitHubのtagの情報等を設定するのが良いと思います)
■ .github/workflows/deploy.yml
- name: Get current date id: date run: echo "::set-output name=date::$(date +'%Y%m%d%H%M%S')" - name: set env run: echo "release_version=release${{ steps.date.outputs.date }}" >> $GITHUB_ENV - name: Use gcloud CLI run: | echo -e "env_variables:\n RELEASE_INSTANCE_VERSION: ${{ env.release_version }}" >> release.yaml cat release.yaml gcloud app deploy --quiet app.yaml --version=${{ env.release_version }}
Releaseを設定した情報でエラーが発生すると以下の通りバージョン毎に集計してくれるようになります。
エラー数が多い場合は制限をかける
もし有料プランで導入する場合は、エラーが送信されすぎて値段が気になるパターンがあると思いますが、その場合はRate Limitsを設定すればOKです。
https://note.com/tabelog_frontend/n/n02888dbf7c20
設定することで、時間当たりに受け取るエラー件数を制限できます。 初期導入時は設定して運用開始して、エラー内容が落ち着いてきたら解除していくのが良いかと思います。