はじめに
OpenTelemetryを使ってgRPCのトレースを楽に取ろうと思うと otelgrpc
を使ってよしなにリクエストのトレースを取っていることと思います。
たとえばサーバー側であれば
interceptorOpt := otelgrpc.WithTracerProvider(otel.GetTracerProvider()) srv := grpc.NewServer( grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor(interceptorOpt)), grpc.StreamInterceptor(otelgrpc.StreamServerInterceptor(interceptorOpt)), )
クライアント側であれば
interceptorOpt := otelgrpc.WithTracerProvider(otel.GetTracerProvider()) *conn, err = grpc.DialContext(ctx, addr, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor(interceptorOpt)), grpc.WithStreamInterceptor(otelgrpc.StreamClientInterceptor(interceptorOpt)), )
といった具合にOpenTelemetry用のインターセプターを挿入することになると思います。(あえて明示的にTracerProviderを指定していますが、globalなものを使うのであればこれは必要ありません)
しかし、これをこのまま実行すると、KubernetesなどでgRPCのヘルスチェックをしているようなときに、そのトレースまで送られてしまいます。つぎのスクリーンショットで大量の grpc.health.v1.Health/Check
のトレースが見えています。(分布図の最下部あるトレースがそれ。スクリーンショットは1つだけハイライトしたもの。)
実際にこれが鬱陶しいのでフィルターをOpenTelemetry側でしてほしいという要望がいくつか出ています。(1つは私が起票したものですが...)
- Filter out health check request in grpc · Issue #373 · open-telemetry/opentelemetry-python-contrib · GitHub
- Investigate GRPC Tracer to support filtering or exclude the Health Checking Protocol. · Issue #355 · open-telemetry/opentelemetry-go-contrib · GitHub
これは当然で、単純に無駄なトレースが大量に表示されてダッシュボードが見づらくなるだけでなく、従量課金になっているようなサービスではコストの無駄になります。そこでワークアラウンドが欲しくなります。
WithSampler
でワークアラウンドする
特定のトレースを取得するかどうかをAPIで差し込める余地は実は殆ど無く、あるとしたら
- 自らexporterを書いて特定のトレースはバックエンドに送らないようにする(あるいはexporterによってはそのようなオプションがあるかも)
- Samplerを自分で書く
くらいしかありません。前者は実現したい事柄に対してのワークアラウンドが大きすぎるので非現実的です。後者は多少コードは多くなるけれど、まだ許容できる範囲なので、こちらでやっていこうと思います。
import sdktrace "go.opentelemetry.io/otel/sdk/trace" func IgnoreWithNameSampler(targets []string) sdktrace.Sampler { return ignoreWithNameSampler{ targets: targets, } } type ignoreWithNameSampler struct { targets []string } func (s ignoreWithNameSampler) Description() string { return "drop all spans with the name that contains one of targets." } func (s ignoreWithNameSampler) ShouldSample(p sdktrace.SamplingParameters) sdktrace.SamplingResult { ts := trace.SpanContextFromContext(p.ParentContext).TraceState() for _, t := range s.targets { if strings.Contains(p.Name, t) { return sdktrace.SamplingResult{ Decision: sdktrace.Drop, Tracestate: ts, } } } return sdktrace.SamplingResult{ Decision: sdktrace.RecordAndSample, Tracestate: ts, } }
これを作ったらあとは普通にTraceProviderにSamplerとして指定するだけです。
tp := sdktrace.NewTracerProvider( sdktrace.WithSampler(IgnoreWithNameSampler([]string{"grpc.health.v1.Health/Check"})), sdktrace.WithBatcher(exporter), )
紹介してきましたが、本来であれば TraceIDRatioBased
などと組み合わせて簡単にやりたいはずなので、標準で持っていてくれていいはずなのにとは思います。
追記(2022.07.12 14:30)
レポートと共にPull Requestを出した。
追記(2022.08.17 16:30)
Pull Requestが無事にマージされたのでこれからは go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc/filters
を使えば簡単にフィルターできます。