wheatandcatの開発ブログ

React Nativeで開発しているペペロミア & memoirの技術系記事を投稿してます

sentry-goを使用して、正しくエラーをハンドリングする

以前、sentry-goの実装の記事を書いたが、そこからより実践に向けのエラーをハンドリング & 情報追加をしてみました。

www.wheatandcat.me

Pull Request

github.com

github.com

実装

導入に関しては以前の記事で紹介しているので割愛 以下は、新たに実装したものについて記載

GraphQLのエラーハンドリングを追加 & GraphQL情報を追加

SetErrorPresenterを使用することでgqlgenのエラーハンドリングが行える(以下参照)

gqlgen.com

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)

f:id:wheatandcat:20210920121305p:plain

QueryVariablesは以下のように情報を追加

  scope.SetExtra("query", goc.RawQuery)
  scope.SetExtra("variables", goc.Variables)

f:id:wheatandcat:20210920121436p:plain

また、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,
    })
}

f:id:wheatandcat:20210920142625p:plain

と、ここまでで、GraphQLのエラーハンドリングの追加は完了。 ただ、これだけだと問題があり、Sentryでコードトレースされる箇所がSentryを送信した箇所になってしまうため画像の通り、実際のエラー箇所まで通知してくれない状態になってしまいます。

f:id:wheatandcat:20210920142742p:plain

そこで、以下にSentryにスタックトレースの情報を送るようにする方法を記載します。

スタックトレースを追加

現状、標準errorsにはスタックトレースの機能が備わっていないので、pkg/errorsパッケージを使用して実装しました。

github.com

スタックトレースを追加する方法は簡単で以下のように既存のerrorをerrors.WithStackでWrapすればOKです。

repository/item.go

 if err != nil {
        return nil, errors.WithStack(err) 
    }

このerrorをSentryに送信すると以下の通りスタックトレースの情報が付与さます。

f:id:wheatandcat:20210920145409p:plain

こんな感じでスタックトレースを付与できました。 ただ、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

以下のコードを追加

graph/index.go

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のタグが追加されます

f:id:wheatandcat:20210920152207p:plain

Breadcrumbsでログ情報を追加

SentryのBreadcrumbsを使用することで、エラー発生までのログの情報を追加する事ができます。

docs.sentry.io

memoirでは未認証状態でのAPIアクセスと認証済み状態でのAPIアクセスの2パターンが存在しているので、そちらの情報を記載するように修正すると以下のようになります。

graph/index.go

 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には以下のように情報が付与される

f:id:wheatandcat:20210920153031p:plain

リリース毎のエラーデータ確認する

実際に運用してみると、どのリリースでエラーは増えたのか、新しく発生したのか知りたくなると思います。 そういう時は、Sentryの Releasesの機能を使用します。

docs.sentry.io

実装的には以下のコードの通り、sentry.InitReleaseにオプションを設定するだけで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を設定した情報でエラーが発生すると以下の通りバージョン毎に集計してくれるようになります。

f:id:wheatandcat:20210920154157p:plain

f:id:wheatandcat:20210920154208p:plain

エラー数が多い場合は制限をかける

もし有料プランで導入する場合は、エラーが送信されすぎて値段が気になるパターンがあると思いますが、その場合はRate Limitsを設定すればOKです。

https://note.com/tabelog_frontend/n/n02888dbf7c20

スクリーンショット 2021-09-18 18 40 51

設定することで、時間当たりに受け取るエラー件数を制限できます。 初期導入時は設定して運用開始して、エラー内容が落ち着いてきたら解除していくのが良いかと思います。