OpenTelemetryのトレースシグナル取得をする上で、ユーザーの属性に応じてサンプリングを調整したいことがある。たとえば以下のような具合だ。
- userType=freeの場合は1%サンプリング(1%拾い、99%は廃棄)
- standardなどほかのuserTypeの場合は全サンプリング(全部拾う)
テールサンプリングかな?と思うところだが、これは少々問題がある。
tail_sampling:
decision_wait: 10s
policies:
- name: free_sampling
type: and
and:
and_sub_policy:
- name: match_free
type: ottl_condition
ottl_condition:
span:
- attributes["userType"] == "free"
- name: probabilistic_policy
type: probabilistic
probabilistic:
sampling_percentage: 1
- name: all_sampling
type: always_sample
これはうまくいかない。ポリシーで設定したfree_samplingとall_samplingは並列で評価が実行され、残ったものが拾われる挙動になっている。そのため、free_samplingで落としても、all_samplingで拾われてしまう。
回避策のひとつは、all_samplingで明示的にuserTypeがfreeのものを除外することである。
- name: free_sampling
type: and
and:
and_sub_policy:
- name: match_free
type: ottl_condition
ottl_condition:
span:
- attributes["userType"] == "free"
- name: probabilistic_policy
type: probabilistic
probabilistic:
sampling_percentage: 1
- name: all_sampling
type: and
and:
and_sub_policy:
- name: not_match_free
type: ottl_condition
ottl_condition:
span:
- attributes["userType"] != "free"
- name: always_sample_policy
type: always_sample
ただ、このような書き方の場合、将来的にuserTypeが増えてくると、そのたびに「拾う条件」と「その否定条件」の2つをメンテナンスすることになってしまう。
何か手立てがないかOpenTelemetry Collector Contribを眺めていたところ、Routing Connectorを使うと、この問題を解決できるのではないかと考えた。
Routing Connectorは、テーブルに設定した条件(属性など)に応じてパイプラインを切り替えるコネクタで、トレース-トレース、メトリック-メトリック、ログ-ログ、と同じシグナル形式の間をつなぐ。「デフォルト」のパイプラインも指定できる。
今回の例で言えば、次のような記述となる。
connectors:
routing:
default_pipelines: [traces/all_sampling]
table:
- condition: attributes["userType"] == "free"
pipelines: [traces/free_sampling]
pipelines:
traces:
receivers: [otlp]
...
exporters: [routing]
traces/all_sampling:
receivers: [routing]
...
traces/free_sampling:
receivers: [routing]
...
tracesパイプラインはreceiversのotlpでトレースシグナルを受け、routingコネクタにエクスポートするroutingコネクタではデフォルトで渡すパイプラインとしてtraces/all_samplingを設定し、属性userTypeがfreeであればtraces/free_samplingのパイプラインに渡すtraces/all_samplingパイプラインはroutingコネクタからのトレースシグナルを受けるtraces/free_samplingパイプラインもroutingコネクタからのトレースシグナルを受ける(属性userTypeがfreeのときにここに来る)
これならば、区別したい属性が増えても拾う条件とパイプラインを追加するだけなので、見通しがよくなる。
ただ、ここまで読んで「よし、やってみよう」と飛びつく前に、Routing Connectorを使う上での注意がある。それは、「判定に使われる属性はリソース属性」ということだ。リソース属性はアプリケーション・サービスやトレース全体にかかる属性で、たとえばサービスバージョンやデプロイ環境名など、アプリケーション起動時に通常決定されるものがそれに当たる。一方のスパン属性は、スパンに関する情報で、リクエストや処理単位ごとに変わる。
構成にもよると思うがuserTypeは一般にスパン属性だろうから、これをなんとかする必要がある。アプリケーション側でやるよりはOpenTelemetry Collector側で属性を処理できると楽だろう。
属性を変更するプロセッサとしては、transformプロセッサを使う(attributesプロセッサはリソース属性の操作はできない)。transformプロセッサの中で、スパン属性userTypeの内容をリソース属性userTypeにコピーする。
transform:
error_mode: ignore
trace_statements:
- set(resource.attributes["userType"], span.attributes["userType"])
...
pipelines:
traces:
receivers: [otlp]
processors: [..., transform]
exporters: [routing]
ところで、属性でパイプラインを分けたことで、userTypeがfreeの1%サンプリングは、テールサンプリングではなく、シンプルなヘッドベースの確率サンプリングで済むことにお気付きだろうか。
probabilistic_sampler:
sampling_percentage: 1
...
traces/free_sampling:
receivers: [routing]
processors: [probabilistic_sampler, batch]
全体としては次のようになった。
receivers:
otlp:
protocols:
http:
endpoint: "0.0.0.0:4318"
processors:
batch:
timeout: 5s
send_batch_size: 5000
send_batch_max_size: 5000
resource/namespace:
attributes:
- key: service.namespace
value: samplingtest
action: upsert
transform:
error_mode: ignore
trace_statements:
- set(resource.attributes["userType"], span.attributes["userType"])
probabilistic_sampler:
sampling_percentage: 1
connectors:
routing:
default_pipelines: [traces/all_sampling]
table:
- condition: attributes["userType"] == "free"
pipelines: [traces/free_sampling]
exporters:
otlphttp/mackerel:
endpoint: https://otlp-vaxila.mackerelio.com
compression: gzip
headers:
Mackerel-Api-Key: ${env:MACKEREL_APIKEY}
otlphttp/oteltui:
endpoint: http://localhost:4319
debug:
service:
telemetry:
metrics:
level: none
pipelines:
traces:
receivers: [otlp]
processors: [resource/namespace, transform]
exporters: [routing]
traces/all_sampling:
receivers: [routing]
processors: [batch]
exporters: [otlphttp/oteltui, otlphttp/mackerel]
traces/free_sampling:
receivers: [routing]
processors: [probabilistic_sampler, batch]
exporters: [debug, otlphttp/oteltui, otlphttp/mackerel]

freeなものを200回、standardなものを10回投稿し、Mackerelや、4319ポートで上げているotel-tui(otel-tui --http 4319)で様子を見る。freeのものについてはdebugにもエクスポートすることで、Routing Connectorで確かに分岐して少しだけしか届かないことを確認した。


全体のレイテンシーに基づく分岐などはテールサンプリングでしかできないが、属性値で分岐するようなシンプルなものであれば、Routing Connectorが効果的な解決方法になりそうだ。