OpenTelemetry公式のデモとして「OpenTelemetry Demo」が用意されている。その中にOpenTelemetry Collectorのベストプラクティス的な設定が含まれていないかと思い、otelcol-config.ymlを読んでいた。
コメント部
# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0
権利表記。ライセンスは自由度の高いApacheライセンスバージョン2。SPDX-License-IdentififerのSPDXはSoftware Package Data Exchangeの略。ファイル単位でこの形でメタ情報が付けられている。
レシーバー
receivers:
otlp:
protocols:
grpc:
endpoint: ${env:OTEL_COLLECTOR_HOST}:${env:OTEL_COLLECTOR_PORT_GRPC}
http:
endpoint: ${env:OTEL_COLLECTOR_HOST}:${env:OTEL_COLLECTOR_PORT_HTTP}
cors:
allowed_origins:
- "http://*"
- "https://*"
まずはOTLP Receiverで、gRPC(grpc)とHTTP(http)の両方を使って待ち受けている。endpointを指定しないとデフォルトがlocalhostのバインドなので、Dockerでコンテナとして分離しているときに通信できなくなるため、代わりにコンテナホスト名が.env経由で渡される。gRPCは4317、HTTPは4318を使うのがデフォルトで、.envでも同じように設定されている。
httpにはCORSを設定するcorsパラメータがある。allowed_originsの設定で、別ドメインのhttpもhttpsでもどんとこい設定になっている。デフォルトでCORSでひっかかったときに、このオプションを思い出すときがくるかもしれない。
httpcheck/frontend-proxy:
targets:
- endpoint: http://frontend-proxy:${env:ENVOY_PORT}
ヘルスチェックのHTTP Check Receiver。/frontend-proxyはそういう特別なものがあるわけではなくて、httpcheckレシーバーの設定に固有の名前を付けて複数指定可能にするオプショナルなもの。
ここでは具体的にenvoyプロキシのURLをtargetsのendpointで指して、ヘルスチェックさせている。デフォルト設定では1分ごとにGETリクエストをかけて、ステータスコードを取得し、httpcheck.statusメトリックを作る。エンドポイントごとにメソッドや認証ヘッダなどは任意に設定できる。
READMEにメトリックの例があった。
httpcheck.status{http.status_class:1xx, http.status_code:200,...} = 0
httpcheck.status{http.status_class:2xx, http.status_code:200,...} = 1
httpcheck.status{http.status_class:3xx, http.status_code:200,...} = 0
httpcheck.status{http.status_class:4xx, http.status_code:200,...} = 0
httpcheck.status{http.status_class:5xx, http.status_code:200,...} = 0
5本のメトリックが常に生成されることになるので、通常のヘルスチェックのつもりだと、やや高くつきそう。
docker_stats:
endpoint: unix:///var/run/docker.sock
Docker Stats Receiverは、Dockerの状態を拾ってメトリック化する。ここではUnixソケットで拾っているが、DockerシステムがHTTPで提供しているのであれば、それをエンドポイントに指定することもできる。Unixソケットではマウントなど諸々面倒ではあるので、HTTPで出ているものならそれを使うほうがよい気がする。
READMEを見ていると細々調整はできそうだ。
redis:
endpoint: "valkey-cart:6379"
username: "valkey"
collection_interval: 10s
Redis Receiverは、RedisにINFOコマンドを発行して情報を取得し、メトリック化する。パスワード指定も必要ならばpasswordパラメータを使う。
# Host metrics
hostmetrics:
root_path: /hostfs
scrapers:
cpu:
metrics:
system.cpu.utilization:
enabled: true
disk:
load:
filesystem:
exclude_mount_points:
mount_points:
- /dev/*
- /proc/*
- /sys/*
- /run/k3s/containerd/*
- /var/lib/docker/*
- /var/lib/kubelet/*
- /snap/*
match_type: regexp
exclude_fs_types:
fs_types:
- autofs
- binfmt_misc
- bpf
- cgroup2
- configfs
- debugfs
- devpts
- devtmpfs
- fusectl
- hugetlbfs
- iso9660
- mqueue
- nsfs
- overlay
- proc
- procfs
- pstore
- rpc_pipefs
- securityfs
- selinuxfs
- squashfs
- sysfs
- tracefs
match_type: strict
memory:
metrics:
system.memory.utilization:
enabled: true
network:
paging:
processes:
process:
mute_process_exe_error: true
mute_process_io_error: true
mute_process_user_error: true
レシーバー最後のでっかいやつがHost Metrics Receiver。ホストのリソースであるCPU利用・ロードアベレージ・メモリ利用・ページング・ファイルシステム利用・ディスクI/O・ネットワークI/O・プロセス数・プロセスごとのCPU/メモリ/ディスクI/O、起動時間をメトリック化する。
とはいえ、コンテナ化しているときにOpenTelemetry Collectorのコンテナ内のリソースを見ても、正直意味がほとんどない。そのために、root_path: /hostfsという設定でDocker母艦のファイルシステムをバインドマウントしているわけだが…それには/を/hostfsにマウントしないといけない(/procも/hostfs/procでマウントする)ので、「うーん、わかるんだが…うーん…」という気持ちになる。Host Metrics receiverの信用性というわけではなく、個人的にはコンテナは親環境とは最低限の状態に閉じていてほしいので、もし運用するならばコンテナのOpenTelemetry Collector http/4318を親に対しても開けるようにして、親環境の最小のOpenTelemetry Collector+Host Metrics receiverで取得したシグナルをそれに送るという構成がよいのではないだろうか(OpenTelemetry Demoだとそうはいかないので、これは仕方がないが)。
取りたいものはscrapersに明示的に指定していく必要がある。
- cpu:CPUの利用状況。
system.cpu.timeはデフォルトで取得、さらにここでは追加でsystem.cpu.utilizationを有効にしている。 - disk:ディスクI/Oにかかるものを全部取っている。
- load:ロードアベレージ。デフォルトの1、5、15分のCPUロードアベレージを取得する。
- filesystem:ファイルシステム利用状況。
exclude_mount_pointsやexclude_fs_typesなど説明があまりない感じだが、まぁマウントポイントやファイルシステム形式の除外であることは見ればわかる(逆にinclude_で指定する方法もある)。match_typeはstrict(完全一致)またはregexp(正規表現一致)を指定する。ほかにinclude_virtual_filesystemsというbool指定もあるようだ。 - memory:メモリ利用状況。デフォルトで
system.memory.usageがあって、ここではさらに追加でsystem.memory.utilizationも有効にしている。Linux向けにはほかにもsystem.linux.memory.available(より正確)やsystem.linux.memory.dirty(ダーティメモリ量)もあるので、可視化されて嬉しいことがあるかもしれない。 - network:ネットワーク利用状況。デフォルトで接続数・ドロップ・エラー・I/O・パケットが取れるのでほぼ困らなそうだ。conntrack(
system.network.conntrack.count、system.network.conntrack.max)を知りたいときはオプションで追加する必要がある。 - paging:ページング状況。デフォルト取得の
system.paging.usageはsumだけれども、オプション取得のsystem.paging.utilizationはgaugeだった。 - processes:プロセス情報。スクレイプ時のプロセス数合計と、作成されていたプロセス数だけ。
- process:個々のプロセスのメトリック。使うときにはよく考えないと、いかにもコストに響きそうだし、カーディナリティも爆発しそうだ。でも、たとえばめちゃくちゃメモリ使ってるプロセス(
process.memory.utilization)を調べたい、というときにはうまくフィルタリング・サンプリングして拾えると便利かもしれない。mute_はプロセスの情報を権限などの理由でうまく拾えなかったときにエラーをミュートする設定。
systemはuptimeしかないので使っていないようだ。単調増加する「せやなぁ」以上の情報ではないとは思う。Gaugeなので差分が負の値になったらアラートするとか…?(あえてメトリックでやることでもなさそう)
エクスポーター
exporters: debug:
Debug Exporterはデバッグ出力用…のわりにはけっこう高級でいろいろできてお世話になっていることが多い。ここではverbosityの指定がない(デフォルトのbasic)なので、「スパンやメトリックを今回いくつ送った」くらいの情報粒度になっている。verbosityを上げるともっとたくさん出るが、OpenTelemetry Demoでやるとたぶん後悔するだろう。
otlp:
endpoint: "jaeger:4317"
tls:
insecure: true
otlphttp/prometheus:
endpoint: "http://prometheus:9090/api/v1/otlp"
tls:
insecure: true
OTLP gRPC Exporter(otlp)では、エンドポイントとしてトレーサーのJaegerに送るようになっている。証明書はないのでtlsのinsecure: trueで非暗号通信。
同様にOTLP/HTTP Exporter(otlphttp)では、Prometheus用の設定を書いている(先ほどのとおり/prometheusは単に固有設定を作るためで、別に送り先はPrometheusでなくてもよい)。
opensearch:
logs_index: otel
http:
endpoint: "http://opensearch:9200"
tls:
insecure: true
これはログの送出用。OpenSearch Exporterを使い、OpenSearchのエンドポイントに送り付ける。logs_indexでotelインデックスに送信される。
プロセッサ
processors: batch:
プロセッサは、レシーバー〜エクスポーターのパイプラインの途中で何か処理をさせたいときに設定する。
Batch Processorは、レシーバーで取得したシグナルを即エクスポーターに送るのではなく、いったんOpenTelemetry Collectorで溜めて、サイズが一杯になるか、時間間隔でエクスポーターに送る仕組み。特にエクスポーターが外部だったときに、圧縮や接続回数軽減などで通信のコストを減らし、効率化できる。
デフォルト設定になっているので、send_batch_sizeは8192(シグナルの数)、timeoutは200ms、send_batch_max_sizeは0(バッチサイズ上限。設定するとバッチが分割される。send_batch_size以上であること)。シグナルの数で設定するもので、消費バイト数を設定するものではない。
memory_limiter:
check_interval: 5s
limit_percentage: 80
spike_limit_percentage: 25
Memory Limit Processorは、メモリ飽和を防ぐためのもので、メモリ状況を見ながらGCを強制したり受け取りを拒否したりする。
各パラメータはデフォルトから「must be changed」らしい。
check_interval:メモリ利用の測定間隔。デフォルト0s、推奨値1s。スパイクが激しいときには減らすかspike_limit_mibを増やすかするのがよいようだ。limit_percentage:総メモリ量に対してプロセスヒープに割り当て可能な率。デフォルト0。limit_mibの算出に使われる。spike_limit_percentage:スパイクぶんの率。デフォルトはlimit_percentageの20%。spike_limit_mibの算出に使われる。
ここでは出てこなかったけれども、次の2つもREADMEにはある。
limit_mib:プロセスヒープに割り当て可能なメモリをMiB単位で指定する。デフォルト0。典型的には50MB以上にはなるらしい。これがハードリミットとなる。spike_limit_mib:スパイク幅をMiB単位で指定する。デフォルトはlimit_mibの20%。ソフトリミットを決めるのに使われ、limit_mibを4000(MiB)、spike_limit_mibを800(MiB)にしたときは4000 - 800の3200MiBがソフトリミットとなる。
ここでは率で指定しているので、もし1000MiBの環境で実行すると、limit_percentage: 80からハードリミットは1000×0.80 = 800MiB。spike_limit_percentage: 25からスパイク幅は1000×0.25 = 250MiBで、ソフトリミットは800 - 250 = 550MiBとなる。
ソフトリミットになると、シグナルの受け取りを拒否するようになる。
transform:
error_mode: ignore
trace_statements:
- context: span
statements:
# could be removed when https://github.com/vercel/next.js/pull/64852 is fixed upstream
- replace_pattern(name, "\\?.*", "")
- replace_match(name, "GET /api/products/*", "GET /api/products/{productId}")
Transform Processorは、OTTL(OpenTelemetry Transformation Language)でデータにさまざまな加工ができるすごいやつ。XMLで加工するやつほどではないが、YAMLで長い加工を書くのもなかなかしんどさがある。
でも、メトリックの加工などで便利なものはけっこうありそうだ(「semanticsを壊すこともあるからuse at your own riskでね」みたいな注釈が付いているものもある)。
error_modeはstatementsの加工中にエラーが発生したときの挙動で、ignoreにすると無視して次のステートメントを実行する(推奨設定)。多段にしたときにはsilentやpropagateも活用することになる。とはいえ、多段は人間が追える限界がある気がするので、AIに書いてもらうのがよさそうだ。
trace_statementsでトレースシグナルに関する加工を定義する。また、contextで対象を推論せずにspanと明示指定している。
ここでやりたいことは、コメントにあるとおりNext.jsのスパン名のカーディナリティ問題へのハックで、replace_pattern(name, "\\?.*", "")ではスパン名から「?〜」なパラメータを正規表現で削り、replace_match(name, "GET /api/products/*", "GET /api/products/{productId}")ではglobマッチングしたものを{productId}に置き換えている。関数いっぱいあるね。
コネクター
connectors: spanmetrics:
コネクターは、あるシグナルを受けて別のシグナルを作り出す役目がある。ここではSpan Metrics Connectorを使う宣言をしている。
Span Metrics Connectorは、スパンからREDメソッド…Request, Error, Durationを算出してメトリックを出すもの。トレースからメトリック化した指標もできますよ、という話題で登場する。
サービス
service: pipelines:
あとはこれらを動かすサービスとして、パイプラインを並べていく。pipelinesが複数形になっているとおり、同時に複数走らせることができる。traces、metrics、logsが基本だけれども、traces/〜のようにオプショナルに作って増やすことも可能。たとえばトレースの処理パイプラインを2つ実行し、片方はエラー抽出のテイルベースサンプリングしてトレーサーへ、もう片方はspanmetricsで精緻めなエラー/リクエストのメトリック数値化、といったことができるのではないか(当然OpenTelemetry Collectorの負担は大きくなるが)。
traces:
receivers: [otlp]
processors: [memory_limiter, transform, batch]
exporters: [otlp, debug, spanmetrics]
トレースの設定。receivers、processors、exportersにここまで宣言していたものを並べていく(たとえOpenTelemetry Collectorにビルトインされていた機能であっても、宣言しないと使えない)。
プロセッサ(processors)については並列実行ではなくこの順に実行されるので、処理対象の最適化のために、サンプリングや加工などは最初に行い、バッチは最後に置くことになる。メモリ制限→サンプリング/加工→バッチ という感じだが、加工しないとサンプリングできないパターンもあるかもしれない。
metrics:
receivers: [hostmetrics, docker_stats, httpcheck/frontend-proxy, otlp, redis, spanmetrics]
processors: [memory_limiter, batch]
exporters: [otlphttp/prometheus, debug]
logs:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [opensearch, debug]
メトリックとログも同様。
telemetry:
metrics:
level: detailed
readers:
- periodic:
interval: 10000
timeout: 5000
exporter:
otlp:
protocol: http/protobuf
endpoint: ${env:OTEL_COLLECTOR_HOST}:${env:OTEL_COLLECTOR_PORT_HTTP}
serviceの下にはもう1つ、OpenTelemetry Collector自体のオブザーバビリティとしてシグナルを提供するInternal telemetry設定がある。ちょうど先日調べていた。
メトリックをdetailedレベルで、10000ミリ秒=10秒ごとに送っている。タイムアウトは5秒。送り先は自分自身で、昔はgRPCで送っていたのだけれども、TLS強制になってしまう問題があって最近http/protobufに変えたようだ(gRPCでの対策は進められている模様)。
つぶやき
さほど新たな知見が得られたというわけではないが、初見だった時代には「なんのこっちゃさっぱりわからん…」だったのが今は「読める読めるぞ…!」になっているのは感慨深いものがあるね。