GraphQLにおけるQuery, Mutation単位のエラーレートの取得(99designs/gqlgen利用)
概要
弊社ではクラウドインフラとしてAWSを使い、BackendとFrontend間の通信にはもっぱらGraphQLを利用しています。
先ごろ、SLI/SLO策定のための取り組みとして、このGraphQLのQuery, Mutation単位でのエラーレートを取得したくなりました。
こういうときの一般的なソリューションとしては、Apiのエンドポイント単位でリクエストとレスポンスをカウントしていくというものがあるかと思います。ただ、GraphQLはエンドポイントが共通(/graphql
)であり、エラーの場合も(一般的には)HTTPStatusコード200でレスポンスを作成するため、この方法を利用することはできない、という問題がありました。
そこで99designs/gqlgenのMiddlewareでCloudWatchMetricsへのカスタムメトリクスのPutを行うことで、Query, Mutation単位でのエラーレート計算を可能にすることができました。
実装
早速ですが、以下に実装例を記載します。
package main
import (
"context"
"fmt"
"net/http"
"time"
"github.com/99designs/gqlgen/graphql"
gqlHandler "github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/rs/cors"
)
type httpServer struct {
mux *chi.Mux
cwClient *cloudwatch.CloudWatch
}
const (
healthCheckPath = "/"
customMetricNamespace = "graphql"
dimensionName = "Operations"
unknownOperationName = "unknown"
)
var ignorePaths = []string{healthCheckPath}
func newHTTPServer(cwClient *cloudwatch.CloudWatch) *httpServer {
r := chi.NewRouter()
r.Use(cors.New(cors.Options{
AllowedHeaders: []string{"*"},
}).Handler)
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Recoverer)
setUpGraphQL(r, cwClient)
r.Get(healthCheckPath, func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
})
return &httpServer{
mux: r,
cwClient: cwClient,
}
}
func setUpGraphQL(r *chi.Mux, cwClient *cloudwatch.CloudWatch) {
srv := gqlHandler.NewDefaultServer(
graphql.MustParseSchema(schema, &Resolver{}),
)
srv.AroundOperations(func(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler {
operationContext := graphql.GetOperationContext(ctx)
if operationContext != nil {
operationName := operationContext.OperationName
if operationName == "" {
operationName = unknownOperationName
}
putMetricData(cwClient, operationName+"-count")
}
res := next(ctx)
return res
})
srv.AroundRootFields(func(ctx context.Context, next graphql.RootResolver) graphql.Marshaler {
operationContext := graphql.GetOperationContext(ctx)
func() {
if len(graphql.GetErrors(ctx)) == 0 && operationContext != nil {
operationName := operationContext.OperationName
if operationName == "" {
operationName = unknownOperationName
}
putMetricData(cwClient, operationName+"-success-count")
}
}()
res := next(ctx)
return res
})
r.Handle("/playground", playground.Handler("GraphQL playground", "/graphql"))
r.Handle("/graphql", srv)
}
func (s *httpServer) Start() {
svr := http.Server{
Handler: s.mux,
Addr: ":8080",
}
if err := svr.ListenAndServe(); err != nil {
fmt.Println(err)
}
}
func putMetricData(cwClient *cloudwatch.CloudWatch, metricName string) {
_, err := cwClient.PutMetricData(&cloudwatch.PutMetricDataInput{
Namespace: aws.String(customMetricNamespace),
MetricData: []*cloudwatch.MetricDatum{
{
MetricName: aws.String(metricName),
Timestamp: aws.Time(time.Now()),
Value: aws.Float64(1),
Unit: aws.String(cloudwatch.StandardUnitCount),
Dimensions: []*cloudwatch.Dimension{
{
Name: aws.String(dimensionName),
Value: aws.String(metricName),
},
},
},
},
})
if err != nil {
fmt.Println(err)
}
}
解説
以下、一応の解説です。
はじめに、AroundOperations
でQuery, Mutationの呼び出しを記録します。
次にAroundRootFields
でエラー非発生時のみ、当該オペレーションについての成功を記録します。あとはCloudWatchMetricsの方でこねこねすれば、エラーレートが取得できるという寸法です。
欠点としては、ここで記録されるのはGraphQLのリクエストに内包されているOperationName
に依存した値となるため、フロントエンド側での呼び出し単位に大きく左右されるという点があります。弊社の場合ではそれでも大きな困りごとはなさそうなのですが、利用状況によっては、これが難点になってしまうかもしれません。
また、PutMetricの回数がむやみやたらに多いよりは少ない方がいいかなという思いから、当初は成功時ではなくてエラー発生時に記録しようと思っていたのですが、どうも処理中でpanicが発生した場合AroundRootFields
を経由しないようなので、現在のかたちに落ち着きました。ただこのあたりは、まだ十分に理解できているとは言い難く、何かより上手い方法とかあるかもしれないなと思っております。有識者の方からのご指摘をいただけますと泣いて喜びます。
なお、上記の実装にあたってはhsaki氏の「GraphQL特有のミドルウェア」記事を参考にさせていただきました。
Discussion