YAMAGUCHI::weblog

噛み付き地蔵に憧れて、この神の世界にやってきました。マドンナみたいな男の子、コッペです。

OpenTelemetryでgRPCのヘルスチェックのトレースを無視する

はじめに

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つは私が起票したものですが...)

これは当然で、単純に無駄なトレースが大量に表示されてダッシュボードが見づらくなるだけでなく、従量課金になっているようなサービスではコストの無駄になります。そこでワークアラウンドが欲しくなります。

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を出した。

github.com

追記(2022.08.17 16:30)

Pull Requestが無事にマージされたのでこれからは go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc/filters を使えば簡単にフィルターできます。

github.com