backendのパフォーマンス測定を実装してみた
PR
OpenTelemetryとは
- OpenTelemetryは、オープンソースのオブザーバビリティフレームワーク
- 運用しているアプリのパフォーマンスや健全性が正常な状態か判断するために使用する情報をテレメトリーデータと呼ばれている
- テレメトリーデータは主に3つのカテゴリに分類される
- ログ
- メトリクス
- トレース
- OpenTelemetryは、それらのテレメトリデータを収集するためのベンダーに依存しないAPI、ソフトウェア開発キット(SDK)、その他のツールを提供している
使用技術
実装
opentelemetry-goとCloud Traceのコードは以下を参考に実装
■ main.go
var tr trace.Tracer = sdktrace.NewTracerProvider().Tracer("memoir-backend") projectID := os.Getenv("GCP_PROJECT_ID") exporter, err := texporter.New(texporter.WithProjectID(projectID)) if err != nil { log.Fatalf("texporter.NewExporter: %v", err) } tp := sdktrace.NewTracerProvider( sdktrace.WithSampler(sdktrace.TraceIDRatioBased(0.01)), // sdktrace.WithSampler(sdktrace.AlwaysSample()), テスト時は毎回トレース情報が欲しいので、こちらを使用 sdktrace.WithBatcher(exporter), ) defer func() { if err := tp.ForceFlush(ctx); err != nil { log.Fatal(err) } }() otel.SetTracerProvider(tp) tr = otel.GetTracerProvider().Tracer("memoir-backend") (略) // 上記のトレースのプロバイダをMiddlewareの引数に渡す srv.Use(app_trace.NewGraphQLTracer(tr))
gqlgenにはMiddlewareを実装することでGraphQLの各QueryとField実行時のハンドリングが行えるので、そのハンドリングとopentelemetry-goを組み合わせると以下のようなコードになった。
■ usecase/app_trace/middleware.go
package app_trace import ( "context" "fmt" "strings" "github.com/99designs/gqlgen/graphql" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" ) type graphqlTracer struct { tracer trace.Tracer } var _ interface { graphql.HandlerExtension graphql.ResponseInterceptor graphql.FieldInterceptor } = &graphqlTracer{} func NewGraphQLTracer(tracer trace.Tracer) graphql.HandlerExtension { return &graphqlTracer{ tracer: tracer, } } func (t graphqlTracer) ExtensionName() string { return "GraphQLTracer" } func (t graphqlTracer) Validate(_ graphql.ExecutableSchema) error { return nil } func (t graphqlTracer) InterceptResponse( ctx context.Context, next graphql.ResponseHandler, ) *graphql.Response { oc := graphql.GetOperationContext(ctx) if oc.Operation.Name == "IntrospectionQuery" { return next(ctx) } q := strings.Split(oc.RawQuery, " ")[0] ctx, span := t.tracer.Start(ctx, q+":"+oc.OperationName, trace.WithSpanKind(trace.SpanKindServer)) defer span.End() if !span.IsRecording() { return next(ctx) } span.SetAttributes( attribute.Key("request.query").String(oc.RawQuery), ) res := next(ctx) if res == nil { return res } if len(res.Errors) > 0 { span.SetStatus(codes.Error, res.Errors.Error()) span.RecordError(fmt.Errorf(res.Errors.Error())) err := res.Errors[0] span.SetAttributes(attribute.Key("error.message").String(err.Message)) } return res } func (t graphqlTracer) 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) ctx, span := t.tracer.Start(ctx, fc.Field.ObjectDefinition.Name+"/"+fc.Field.Name, trace.WithSpanKind(trace.SpanKindServer), ) defer span.End() if !span.IsRecording() { return next(ctx) } span.SetAttributes( attribute.Key("resolver.path").String(fc.Path().String()), attribute.Key("resolver.object").String(fc.Field.ObjectDefinition.Name), attribute.Key("resolver.field").String(fc.Field.Name), attribute.Key("resolver.alias").String(fc.Field.Alias), ) argKV := []attribute.KeyValue{} for _, arg := range fc.Field.Arguments { if arg.Value != nil { argKV = append(argKV, attribute.Key(fmt.Sprintf("resolver.args.%s", arg.Name)).String(arg.Value.String())) } } if len(argKV) > 0 { span.SetAttributes(argKV...) } res, err := next(ctx) if err != nil { span.SetAttributes(attribute.Key("error.message").String(err.Error())) } errList := graphql.GetFieldErrors(ctx, fc) if len(errList) != 0 { span.SetStatus(codes.Error, errList.Error()) span.RecordError(fmt.Errorf(errList.Error())) err := errList[0] span.SetAttributes(attribute.Key("error.message").String(err.Message)) } return res, err }
実装は、これで完了。 後は実際にGraphQLを実行すると以下のように、Cloud Traceで表示される。
複数Queryを実行した際にボトルネックになっている箇所などがわかりやすくなり、運用しやすくなった。