😚

GoのAPIにSentryを導入してトレースする

に公開

概要

GoのAPIにSentryを導入して、トレースするわかりやすい解説記事がなかったので、執筆します。
また、公式のサンプルも大変わかりやすいですがあくまでもデモのコードでの解説になっていて、実際のAPIでどうやって導入するのだ?
となった箇所が個人的に多かったので将来の自分のためにも残しておきます。

対象読者

  • GoでAPIを作ったのでSentryでのトレースを試したい方
  • トレース初心者
  • Sentryを使ってみたい方

前提

  • APIは実装済み
  • Sentryは初期設定済み(ダッシュボード)
  • Sentryに送信する初期化処理は実装済み

ゴール

  • Sentryのダッシュボードからtraceが見れること
    • リクエスト情報
    • SQLクエリ

手順

  1. 親トランザクションを作成する
    • APIなので、各リクエストごとに共通処理としてトレースしたいので、ミドルウェアに処理を実装する
  2. 親トランザクションをcontextに詰める
    • 1のミドルウェアでtransaction.context()を リクエストのcontextに詰める
middleware/sentry.go
func SentryTracingMiddleware(next gin.HandlerFunc) gin.HandlerFunc {
 	return func(c *gin.Context) {
 		ctx := c.Request.Context()
 		hub := sentry.GetHubFromContext(ctx)
 		if hub == nil {
 			hub = sentry.CurrentHub().Clone()
 			ctx = sentry.SetHubOnContext(ctx, hub)
 		}
 		options := []sentry.SpanOption{
 			sentry.WithOpName("http.server"),
 			sentry.ContinueFromRequest(c.Request),
 			sentry.WithTransactionSource(sentry.SourceURL),
 		}
 		transaction := sentry.StartTransaction(ctx,
 			fmt.Sprintf("%s %s", c.Request.Method, c.Request.URL.Path),
 			options...,
 		)
 		defer transaction.Finish()
 		c.Request = c.Request.WithContext(transaction.Context())
 
 		// 次のハンドラを実行
 		next(c)
 	}
}
sever.go
func NewServer() (*gin.Engine, error) {
 	r := gin.New()
 	cfg, _ := pkg.New()
 	if cfg.Env == "PROD" {
 		gin.SetMode(gin.ReleaseMode)
 	}
 
    // ... code
 
 	// Sentry設定
+ 	sentryMiddleware := middleware.SentryTracingMiddleware(gin.Logger())
 
 	// ミドルウェア設定
 	r.Use(withReqId)
 	r.Use(withCtx)
 	r.Use(cors)
 	r.Use(httpLogger)
 	r.Use(sentrygin.New(sentrygin.Options{}))
+ 	r.Use(sentryMiddleware)
 
 	// ヘルスチェック
 	v1 := r.Group("/v1")
    // ... code
}
  1. 各処理でspanを呼び出して、処理をトレースする
    • 各処理のでspanを呼び出してトレースする
func (m *MonsterHandler) GetAll(c *gin.Context) {
 	ctx := c.Request.Context()
+  	span := sentry.StartSpan(ctx, "handler.GetAll")
+  	span.Description = "GetAll"
+ 	defer span.Finish()
    // ... code
}

ここまでで、一応、トレースは出力できた

ただ、SQLクエリが出ていない・・・

まだ、gormのログ出力設定が足りていないので、設定を追加していきます。

database/sentry.go
package mysql
 
 import (
 	"context"
 	"fmt"
 	"log"
 	"time"
 
 	"github.com/getsentry/sentry-go"
 	"gorm.io/gorm/logger"
 )
 
 type SentryLogger struct {
 	slowThreshold time.Duration
 	logLevel      logger.LogLevel
 }
 
 func NewSentryLogger() *SentryLogger {
 	return &SentryLogger{
 		slowThreshold: 200 * time.Millisecond, // スロークエリの閾値
 		logLevel:      logger.Info,            // ログレベル
 	}
 }
 
 func (l *SentryLogger) LogMode(level logger.LogLevel) logger.Interface {
 	newlogger := *l
 	newlogger.logLevel = level
 	return &newlogger
 }
 
 func (l *SentryLogger) Info(ctx context.Context, msg string, data ...interface{}) {
 	if l.logLevel >= logger.Info {
 		log.Printf("[GORM INFO] %s\n", fmt.Sprintf(msg, data...))
 	}
 }
 
 func (l *SentryLogger) Warn(ctx context.Context, msg string, data ...interface{}) {
 	if l.logLevel >= logger.Warn {
 		log.Printf("[GORM WARN] %s\n", fmt.Sprintf(msg, data...))
 	}
 }
 
 func (l *SentryLogger) Error(ctx context.Context, msg string, data ...interface{}) {
 	if l.logLevel >= logger.Error {
 		log.Printf("[GORM ERROR] %s\n", fmt.Sprintf(msg, data...))
 	}
 }
 
 func (l *SentryLogger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
 	if l.logLevel <= 0 {
 		return
 	}
 
 	elapsed := time.Since(begin)
 	sql, rows := fc()
 
 	span := sentry.StartSpan(ctx, "gorm.query")
 	span.SetData("sql", sql)
 	span.SetData("rows", rows)
 	span.SetData("elapsed", elapsed.String())
 	if err != nil {
 		span.Description = err.Error()
 	} else if l.slowThreshold != 0 && elapsed > l.slowThreshold {
 		span.Description = "slow query"
 	}
 	span.Finish()
 
 	if l.logLevel >= logger.Info {
 		log.Printf("[GORM TRACE] [%.3fms] [rows:%v] %s\n", float64(elapsed.Nanoseconds())/1e6, rows, sql)
 	}
 }

gormの初期化時にロガー設定をする必要があります。

database/db.go
if db, err = gorm.Open(dialector, &gorm.Config{
+ 		Logger: NewSentryLogger(),
 		NamingStrategy: schema.NamingStrategy{
 			SingularTable: true,
 		},
    })

SQLの実行時に("gorm.DB).WithContext(ctx)を使用してcontextを渡すようにする必要もあります。
また、(l *SentryLogger) Traceにて、spanを設定しているので、各DB操作処理にてトレースのための処理を実装する必要はありません。

まとめ

本当に最低限の設定ですが、記事も多くなく一発で解決できなかったので記事にしてみました。
また、公式のドキュメントもかなり参考にはなりますが、APIではないデモコードの記載でどうやるんだろうと悩んだ箇所も多かったり、検索してページを何回も飛ばないといけなかったりで少し大変ではありました。
この記事が誰かや今後の自分の役に立てば幸いです。

Discussion