sano11o1

echoサーバーからSentryにエラー通知する時はsentry-go/echoが便利

公開日:

はじめに

Sentryではどのユーザーでエラーが発生したのかを送信することができる。
echoを使ってWebサーバーを立ち上げる構成の場合、Sentryが提供しているsentry-go/echo を使った方が、安全かつ手軽にユーザー情報をSentryに送信できることがわかったので、忘れないようにメモしておく

Hub, Scopeについて 

普段Sentryに通知するメソッドを呼び出すだけでは意識することはないが、SentryにはHubとScopeという概念がある。

  • Hub: SentryとSentrySDKを繋ぐモノ。Sentry.Initすると作られる
  • Scope: Hubを介して送られる情報を入れておくモノ


https://docs.sentry.io/platforms/go/enriching-events/scopes に詳細な説明がある

ユーザー情報をSentryに送信する際の注意点

ConfigureScope, SetUserメソッド を使うとユーザー情報を送信できる。

sentry.ConfigureScope(func(scope *sentry.Scope) {
	scope.SetUser(sentry.User{Email: "sano11o1@example.com"})
})


concurrency に記載があるが、複数の非同期処理で1つのHubに対してScopeを設定する場合、競合が発生する可能性がある。
競合が発生すると、本来エラーが起きたはずのユーザーとは異なるユーザー情報が送信されるので、非常にまずいことになる。
echoは非同期でリクエストを捌くので、例えば以下のようなコードではユーザー情報の競合が発生しうる。

import (
  "fmt"
  "net/http"
  "github.com/getsentry/sentry-go"
  "github.com/labstack/echo/v4"
  "github.com/labstack/echo/v4/middleware"
)

if err := sentry.Init(sentry.ClientOptions{
  Dsn: "",
  TracesSampleRate: 1.0,
}); err != nil {
  fmt.Printf("Sentry initialization failed: %v\n", err)
}


app := echo.New()

app.Use(middleware.Logger()) // JWTからユーザーを特定し、contextにuser_idを格納するMiddlewareと仮定

app.GET("/", func(ctx echo.Context) error {
    // ハンドラの様々な処理 
   // エラーが発生したらユーザー情報と共にSentryに送信
  if err != nil {
    sentry.ConfigureScope(func(scope *sentry.Scope) {
      scope.SetUser(
        sentry.User{
          ID: c.Value(UserIDKey).(*model.UserID),
        }
      )
    })
    sentry.CaptureException(err)
    return err
  }
  return ctx.String(http.StatusOK, "Hello, World!")
})

app.Logger.Fatal(app.Start(":3000"))


ユーザー情報の競合を防ぐ

concurrency にもあるように、リクエスト毎にHubを複製するのが良い。
sentry-go/echo が提供するMiddlewareは内部的にHubの複製をやってくれる。

具体的には以下のコード でHubを複製していることがわかる。

func (h *handler) handle(next echo.HandlerFunc) echo.HandlerFunc {
  return func(ctx echo.Context) error {
     hub := sentry.GetHubFromContext(ctx.Request().Context())
	  if hub == nil {
	     hub = sentry.CurrentHub().Clone()
	   }
     // 後続の処理
}


Middlewareはリクエスト毎に呼ばれるので、複数のリクエストが共通のHubを変更することはない。

Hubはecho.Contextに格納されるので、エラーを通知する処理ではecho.ContextからHubを取り出すことになる。
最終的には以下のようなコードになる。

sentryHub := sentryecho.GetHubFromContext(c)
sentryHub.ConfigureScope(func(scope *sentry.Scope) {
  scope.SetUser(sentry.User{
    ID: c.Value(UserIDKey).(*model.UserID),
  })
})
sentryHub.CaptureException(err)