バイセル Tech Blog

バイセル Tech Blogは株式会社BuySell Technologiesのエンジニア達が知見・発見を共有する技術ブログです。

バイセル Tech Blog

Go言語とGraphQLアプリのNew Relic導入ガイド

本記事は New Relic Advent Calendar 2023 の 22日目の記事です。

はじめに

こんにちは、テクノロジー戦略本部 SRE 部長の大谷 @smpeotn です。SREの推進や組織内への浸透を担当しています。

バイセルでは以前から New Relic を活用し、アプリケーションのパフォーマンス改善などに取り組んできました。以下は過去の記事の一部です。

tech.buysell-technologies.com tech.buysell-technologies.com tech.buysell-technologies.com

これらの記事では、Ruby on Rails を使用した Web アプリケーションに焦点を当て、New Relic の利用方法やパフォーマンスチューニングについて紹介しています。

現在、私たちはリユースプラットフォーム「Cosmos」を開発しています。そこで、SRE の視点を取り入れたマイクロサービスにおけるオブザーバビリティの向上を目指し、New Relic の活用を推進しています。APM のみならず Distributed Tracing や Error Inbox など、New Relic の機能を活用して品質の高いプロダクト開発と運用を実現するために日々努力しています。

今回は、Cosmos 開発言語である Go 言語の Web アプリケーションに New Relic をインストールしました。 本記事では、その過程で発生したさまざまな課題や工夫について紹介します。

なぜNew Relicなのか

まず最初に、なぜ New Relic を推進しているのかについて説明します。バイセルの既存のプロダクトでは New Relic だけでなく、Sentry や Mackerel などのアプリケーション監視、インフラ監視のシステムも導入されています。しかし、オブザーバビリティの向上に取り組む際に、以下の理由からNew Relic が「リユースプラットフォーム Cosmos」には 最適と判断し、導入を推進することを決定しました。

  • Webブラウザからインフラまでトランザクションレベルで追跡できる
  • DistributedTracingを実装し、サービス間のボトルネックを特定できる
  • ServiceMapの適切な実装により、サービス間の関係値を可視化できる
  • サービス間で統一した言語によるコミュニケーションを実現できる
  • NRQLを用いて自由にダッシュボードを作成できる
  • 各機能が単体で動作するだけでなく、包括的に必要な機能を利用できる

GoアプリケーションにおけるNew Relicのインストール

それでは、Go 言語の Web アプリケーションにおける New Relic のインストールについて見ていきましょう。基本的なインストール方法は公式ドキュメントに記載されています。 docs.newrelic.com

今回説明するのは、純粋な Go の Web アプリケーションではなく、Hasura の Remote Schema に対する APM の導入です。

Contextを引き回す

Go 言語の特性上、New Relic の SDK を扱う際にも Context の引き回しは必要不可欠です。Context を引数として引き回す以外の方法が見当たらなかったため、初期段階で Context を引数として引き回すような実装をおすすめします。New Relic SDK も例外なく Context に対して SDK 情報を蓄積しています。

HTTP Request Wrapがフレームワークによって変わる

バイセルのアプリケーションは echo、gin、go-chi などの Web フレームワークが採用されています。APMを導入するためにはHTTP の Handler を Wrap する必要があります。しかし、フレームワークによっては Router などの挙動が不定であるため、Middleware を実装し、確実に Wrap するための一括実装を選択しました。

func main() {
  // go-chi framework
   r := chi.NewRouter()
   r.Use(New RelicHttpMiddleware(app, []string{"/ignore", “/ignore2”}))
   r.Get("/ignote", api.api.api)
   r.Group(func(r chi.Router) {
       // リスナーがいっぱい
       r.Get("/ignore2", api.api.api)
       r.Get("/path1", api.api.api)
       r.Get("/path2", api.api.api)
       r.Get("/path3", api.api.api)
       r.Get("/path4", api.api.api)
       r.Get("/path5", api.api.api)
   }
}


func New RelicHttpMiddleware(app *New Relic.Application, ignorePaths []string) func(http.Handler) http.Handler {
   return func(next http.Handler) http.Handler {
      return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
         for _, ignorePath := range ignorePaths {
            if r.URL.Path == ignorePath {
               next.ServeHTTP(w, r)
               return
            }
         }
         _, handler := New Relic.WrapHandle(app, r.URL.Path, next)
         handler.ServeHTTP(w, r)
      })
   }
}

Hasuraの/graphqlに対するリクエストトランザクションの区別がつかない

Hasura の Endpoint は /graphql しかなく、 POST Request で body に含まれる Query によって内部処理が変化します。Remote Schema を構成する GraphQL サーバーとして gqlgen を採用しているため、そのリクエストに対する APM を実装します。

上記で述べた APM(HTTP Wrap)を実装しても、New Relic 上ではすべてのリクエストが 同じトランザクション名"WebTransaction/Go/POST /graphql" として記録されてしまい、どのリクエストに問題があるのかを判断することができませんでした。そのため、以下の2つの方法を試しました。
1. CustomSegmentとしてQueryNameを記録する
2. Middleware上でTransaction名を変更する

1. CustomSegmentとしてQueryNameを記録する

Go では Segment の記録をアプリケーションコード内で個別に実装する必要があります。gqlgen の Middleware を使用して QueryName を トランザクション の CustomSegment に記録するように実装しました。

srv := handler.NewDefaultServer(generated.NewExecutableSchema(
   generated.Config{Resolvers: &graph.Resolver{
      Config:    cfg,
      Publisher: publisher,
   }}),
)
srv.Use(gqlgenMiddleware())

// gelgen.graphql.HandlerExtension interface を実装
func (g *graphTracer) InterceptField(ctx context.Context, next graphql.Resolver) (interface{}, error) {
   oc := graphql.GetOperationContext(ctx)
   if oc.Operation.Name == "IntrospectionQuery" {
      return next(ctx)
   }
   fc := graphql.GetFieldContext(ctx)


   txn := New Relic.FromContext(ctx)
   seg := txn.StartSegment(fc.Field.ObjectDefinition.Name + "@" + fc.Field.Name)
   defer seg.End()


   // 後続処理
}

これにより、「WebTransaction/Go/POST /graphql」というトランザクションに、区別をつけるための識別子を追加できました。ただし、これではトランザクション名が同じであり、データ表示にばらつきが生じる可能性があるため、次の方法を採用しました。

2. Middleware上でトランザクション名を変更する

gqlgenのMiddleware内でトランザクション名を上書きすることを検討しました。

func (g *graphTracer) InterceptResponse(ctx context.Context, next graphql.ResponseHandler) *graphql.Response {
   var opName string
   oc := graphql.GetOperationContext(ctx)
   if opName = oc.OperationName; opName == "" {
      opName = "graphql"
   }
   txn := New Relic.FromContext(ctx)
   seg := txn.StartSegment(opName)
   defer seg.End()


   // transaction Name 変更
   txn.SetName(txn.Name() + " @ " + opName)
   // 後続処理
}

gqlgen.graphql.HandlerExtension インターフェースを実装し、InterceptResponse 内で txn.SetName を使用して Query に関する情報をトランザクション名に追加することができました。これにより、各 Query ごとにトランザクションが分離され、New Relic 上でデータのばらつきが少なくなり、どのリクエストがボトルネックなのかを正確に判断できるようになりました。

まとめ

Go 言語や採用しているフレームワークによって、New Relic SDK の導入は多少のハードルがあるかもしれませんが、自由度の高い言語のおかげで実現できたことがあります。まだまだ導入初期段階であり、これからの無限の可能性に胸を躍らせています。バイセルとして、また個人としてもさらなる活用を追求していきます。

最後までお読みいただき、ありがとうございました。

BuySell Technologiesでは一緒に働いてくれる仲間を絶賛募集中です。少しでも興味のある方はぜひご確認ください!カジュアルなお話でもOKです! herp.careers herp.careers