Bugsnagを利用したエラートラッキング

Adventarを支える技術 Advent Calendar 2019 の20日目です。

今日はエラートラッキングについて書きます。

Bugsnag

エラートラッキングのサービスは色々あって、有名なのは SentryAirbrake あたりでしょうか。今回は Bugsnag というサービスを利用しました。これは Rails 時代から使っていて、無料で使える範囲が一番大きそう、という基準で選びました。

Bugsnag だと 250 events/day は無料枠で使えるので、Adventar ぐらいの規模であればスパイクしなければ余裕です。Sentry も今見たら無料で 5000 events/month なのでこっちでもいけそうな気がします(昔からこうだっけな)。

今回は Go の gRPC サーバー、Nuxt.js で SSR しているところに Bugsnag を使っています。フロントエンドの JS でも動くのですが、経験上フロントエンドでのエラートラッキングはノイズが多く、無料枠を食いつぶしてしまう可能性がありそうだったので今回は入れていません。もしかしたらそんなことはなくて、意外とさくっといける可能性はあります。

Rails では何もはまらずに使えていたのですが、Go と Node.js に有効にするのにけっこう苦労したので、それについて書いておきます。

Go/gRPCでの利用

Bugsnag の Go SDK はいくつかのフレームークに対応してますが、gRPC はありませんでした。

https://docs.bugsnag.com/platforms/go/

なのでOther Go appsを見て自力でどうにかする必要がありそうです。

Go のサーバーでエラーをトラッキングしたいのは主に

  • 予期せぬエラーになった場合
  • panic で死んだ場合

の2つで、この場合に

bugsnag.Notify(err, ctx)

を呼べばよさそうです。最終的には以下のようなコードを Interceptor に刺しこみました。

func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (_ interface{}, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = grpc.Errorf(codes.Internal, "Internal Server Error")
            fmt.Printf("%s\n", r)
            if bugsnagAPIKey != "" {
                bugsnag.Notify(fmt.Errorf("%s", r), ctx)
            }
        }
    }()
    resp, err := handler(ctx, req)
    s, _ := status.FromError(err)
    if s.Code() == codes.Unknown {
        stacktrace := fmt.Sprintf("%+v\n", err)
        fmt.Print(stacktrace)
        if bugsnagAPIKey != "" {
            bugsnag.Notify(err, ctx, bugsnag.MetaData{"info": {"stacktrace": stacktrace}})
        }
        err = grpc.Errorf(codes.Internal, "Internal Server Error")
    }
    return resp, err
},

https://github.com/adventar/adventar/blob/c175ac9bd7fd9c12a74bd86202129394ba13e41f/api-server/grpc-server/service/service.go#L75-L96

今回エラーハンドリングには xerrors を使っているのですが、xerrors で wrap したエラーを投げると、bugsnag 上でのエラーが全部 *xerrers.Wrap になるの困っています。どうにかしたいのですが、エラーの量もそこまで多くなくて困らないので放置しています。

f:id:hokaccha:20191218223657p:plain

また、スタックトレースの情報がわかりづらく、例えば以下のエラーは

f:id:hokaccha:20191218223713p:plain

以下の箇所で発生していますが、entry.go:118スタックトレースに表示されません。

https://github.com/adventar/adventar/blob/c175ac9bd7fd9c12a74bd86202129394ba13e41f/api-server/grpc-server/service/entry.go#L118

この問題はメタデータとして、fmt.Sprintf("%+v\n", err)を送信することで一時しのぎしています。これは以下の表示されます。

f:id:hokaccha:20191218223734p:plain

わかりやすい...。もう少しちゃんとしたやり方があると思うので直したいところです。

Node.js/AWS Lambdaでの利用

SSR している Lambda では express を利用しているので、ドキュメントに沿って導入してみたが動きませんでした。どうやら bugsnag へのエラー送信が終わる前に Lambda が終了してしまうことが原因みたいだったようです。Issueもありました。

https://github.com/bugsnag/bugsnag-js/issues/495

エラーを送信する処理(bugsnagClient.notify)を自分で実行して、その終了を待てばよさそうなのですが、必要な情報を自分で詰めないといけないのでややめんどうでした。例えば express plugin を使えばこのあたりでやってくれます。

https://github.com/bugsnag/bugsnag-js/blob/96238c360d1f021af9d006fead5d10f827cf0079/packages/plugin-express/src/express.js#L64-L76

とりあえず最低限の情報だけ詰めて対応しました。

app.use((err, req, _, next) => {
  const opt = {
    request: {
      headers: req.headers,
      httpMethod: req.method,
      url: req.url
    }
  };

 // このコールバックでエラー送信の終了を待つ
  bugsnagClient.notify(err, opt, () => {
    next(err);
  });
});

https://github.com/adventar/adventar/blob/c175ac9bd7fd9c12a74bd86202129394ba13e41f/frontend/server.ts#L60-L69

また、もう一つの問題は source maps です。

https://docs.bugsnag.com/platforms/javascript/source-maps/

ブラウザの JS であれば、sourceMappingURL に書いてある URL に取りに行ってくれるので便利ですが、Node.js の場合は source maps のファイルを別途 Bugsnag にアップロードする必要があります。がんばればできそうですが、今回はめんどうなので諦めました。いつかやるかもしれません。

まとめ

今回は Bugsnag を導入するのにはまったことなどを書きました。実際 Bugsnag はかなり役に立っていて、Bugsnag のおかげでいくつかのエラーを特定して潰すことができました。いくつか中途半端になっているところがあるので今後改善していきたいと思います。

明日は UI のスマホ対応について書きます。