タイトルは What I Wish I Knew When Learning Haskell リスペクトです.
SDK と API
OpenTelemetry でアプリケーションの計装をする際に使うパッケージ (モジュール) は, 計測・集約・エクスポートなどの実装の本体である SDK と, 計測のためのインターフェースである API に分離されています.
たとえば Node.js の場合, アプリケーションのエントリポイント付近で @opentelemetry/sdk-metrics パッケージを使って SDK を初期化し, グローバルな MeterProvider を設定します.
import { MeterProvider } from "@opentelemetry/sdk-metrics"; import { metrics } from "@opentelemetry/api"; const meterProvider = new MeterProvider({ // 各種設定 }); metrics.setGlobalMeterProvider(meterProvider);
あるいは @opentelemetry/sdk-node パッケージを使うとメトリクスだけでなくトレースとログの SDK もまとめて初期化できますが, この場合も裏側で同様の処理が行われています.
import { NodeSDK } from "@opentelemetry/sdk-node"; const sdk = new NodeSDK({ // 各種設定 }); sdk.start();
アプリケーションの各コンポーネントでは @opentelemetry/api を使って, 抽象的なインターフェース経由で計測を行います.
インターフェースの裏側にいるのは上記で設定したグローバルな MeterProvider です.
import { metrics } from "@opentelemetry/api"; const meter = metrics.getMeter("example"); const foo = meter.createGauge("foo"); foo.record(42);
このように SDK と API が分離されていることによって, アプリケーションの各コンポーネントはどう計測するかにのみ関心を持てばよく, 計測の結果がどのように使われるのかについては関心を持たなくて済むようになっています. 例えば SDK の設定で計測結果の使い道 (集約方法やエクスポート先など) が変わっても, 計測を行っている各コンポーネントのコードは全く変更の必要がありません. サードパーティのライブラリがあらかじめ計測コードを記述しておくことも可能です.
(それはそれとして使うべきパッケージが多すぎて, 何を使ったら良いのかわかりづらかったり, ぱっと見でとっつきづらく見えるのは困る...)
Instrument や Metric Point の種類と意味
計測の道具である Instrument には主に以下の 4 つがあります.
- Counter
counter.add(1)のように増加を計測
- UpDownCounter
upDownCounter.add(-1)のように増減を計測
- Gauge
gauge.record(42)のように現在値を計測
- Histogram
histogram.record(3.14)のようにサンプル値を計測
- (この他に Counter, UpDownCounter, Guage の asynchronous (observable) 版を加えて, 全部で 7 つあります)
どの Instrument を使っても, それぞれ一度に一つの数値を計測するという点では同じですね.
Instrument で計測した数値はメモリ上に蓄積され, エクスポートする際に集約 (Aggregation, 後述) されて Metric Point に変換されます. この Metric Point には主に以下の 3 つがあります.
- Sum
- 足し算に意味のある数値
- 短調増加 (monotonic) かどうかの区別がある
- Gauge
- 足し算に意味のない数値
- Histogram
- 数値の分布
- (この他に Histogram の亜種である Exponential Histogram とレガシーな Summary を加えて, 全部で 5 つあります)
この Metric Point は SDK 内部で使われるだけでなく, OpenTelemetry Protocol (OTLP) を通してそのままの形でエクスポートすることもできます. そして最終的には終着点となるエクスポート先のデータモデルに合わせて再び変換・保存されることになるでしょう.
Metric Point の種類ごとの違いは, Sum/Gauge と Histogram のようにデータの種類が異なる場合は明らかですが, Sum と Gauge のようにデータの意味が異なるという場合は少しわかりづらいです. 例えばメトリクス同士の演算をしたり, エクスポート先のデータモデルにも同様の意味的な区別がある場合は, これらの意味的な違いが重要になってきます.
ここで Instrument に話を戻すと, Instrument の種類ごとの違いは全て意味的なものです. 集約の際には, それぞれの意味に応じたデフォルトの集約方法が自動的に使われるようになっています.
| Instrument | Aggregation | Metric Point |
|---|---|---|
| Counter | Sum | Sum (monotonic) |
| UpDownCounter | Sum | Sum (non-monotonic) |
| Gauge | Last Value | Gauge |
| Histogram | Explicit Bucket Histogram | Histogram |
(種類の異なるものに対する用語の重複が多くて厄介ですね. 混乱しないよう注意しましょう.)
そしてこれらはあくまでデフォルトであり, View (後述) を使うと集約方法を変更することもできます (意味的な妥当性がない集約方法も選べてしまうことに注意).
以上のことを踏まえると, 計測時にどの Instrument を使うのかは, その計測結果をデフォルトではどのように扱ってほしいかを考えて決めるのが良いでしょう. 基本的には同じようなことが書いてありますが, ライブラリ作成者向けのガイドラインも参考になります.
MeterProvider/Meter/Instrument と Resouce/Scope/Metric
OTLP のメトリクスは以下のような階層構造からなっています.
- Resouce: メトリクス発生元のリソース (マシン, OS, ランタイムなど) の情報
- Scope: メトリクス発生元のアプリケーションコンポーネントやライブラリの情報
- Metric
- Scope: メトリクス発生元のアプリケーションコンポーネントやライブラリの情報
API の MeterProvider, Meter, Instrument の階層構造はこれに対応したものとなっています.
- MeterProvider: 作成時に Resouce の情報を設定する
- Meter: 作成時に Scope の情報を設定する
- Instrument
- Meter: 作成時に Scope の情報を設定する
また Meter の情報は View (後述) で Instrument を選択する際にも使われます.
View
通常は Instrument と同じ名前で Metric が生成されますが, これはあくまでデフォルトの設定です. View を使うと, Instrument から生成する Metric の名前や, 上で説明したように集約の方法などをデフォルトのものから変更できます.
各 View のオプションは, マッチする Instrument の条件と, マッチした Instrument から生成する Metric の設定の二つの部分からなります.
以下の例では Meter example の下の Instrument foo の計測結果に対して, 生成する Metric の名前を bar に変更しています.
(マッチの条件と Metric の設定がフラットに並ぶ構造になっていて, 正直読みづらいと思う...)
const sdk = new NodeSDK({ // ... views: [ { // マッチする Instrument の条件 // Meter の名前やバージョン, Instrument の名前や種類が使える meterName: "example", instrumentName: "foo", // マッチした Instrument から生成する Metric の設定 // 名前, 属性 (Attributes), 集約の方法などをデフォルトのものから変更できる name: "bar", }, // ... ], // ... });
View のユースケースとしては上記の例のように Metric の名前を変える他にも,
- 集約の方法を Drop に変更することで, Metric を生成しないようにする
- 一つの Instrument に対して複数の View を設定し, 名前や集約方法を変えた複数の Metric を生成する
などがあります.
AggregationTemporality
MetricReader または MetricExporter の設定として AggregationTemporality というものがあり, メトリクスをエクスポートする際にどの期間で集約した値を出力するかを変更できます.
const sdk = new NodeSDK({ // ... metricReaders: [ new PeriodicExportingMetricReader({ exporter: new OTLPMetricExporter({ temporalityPreference: AggregationTemporality.DELTA, }), }), ], // ... });
例として, 時刻 t0 から計測を始めて t1, t2, t3, ... の各時刻にメトリクスをエクスポートした場合,
- Cumulative:
- 出力する値は (t0, t1], (t0, t2], (t0, t3], ... の各期間で集約されたもの
- メモリ使用量や送信するデータ量は増えるが, どこかの点が抜け落ちても時間的な解像度が下がるだけで済む
- Delta:
- 出力する値は (t0, t1], (t1, t2], (t2, t3], ... の各期間で集約されたもの
- メモリ使用量や送信するデータ量は少ないが, どこかの点が抜け落ちたらその時刻の情報は完全に失われる
のような違いがあります. どちらの AggregationTemporality でも, 出力される情報量は基本的には同じ (Histogram の min, max は除く) で, 相互変換が可能 (ただしステートフルなコンポーネントが必要) です.
エクスポート先のデータの保存方法や機能などによって,
- Cumulative / Delta いずれかのエクスポートにしか対応していない
- Cumulative / Delta いずれのエクスポートにも対応しているが, どちらかに変換した上で保存される
- Cumulative / Delta いずれのエクスポートにも対応しているが, 変換せずに保存され, 扱いはユーザーに委ねられる
のように各 AggregationTemporality へのサポート状況が異なるため, どちらを使うのが適切かはエクスポート先のドキュメントでの案内を確認しましょう.
注意したいのは, 設定箇所からもわかるように, AggregationTemporality はエクスポートの設定であって, メトリクスの意味を変えるための設定ではありません. 例えば Delta で Sum をエクスポートすると見かけ上レートのようにはなるのですが, その値をそのままレートとして解釈するのは誤りです. レートを見たい場合はエクスポート先で計算するか, Collector の Delta to Rate Processor を使って単位時間あたりのレートに変換しましょう.
付録: バージョン情報
これまでにもバージョン間で使い方が変わったことがまあまああるっぽいので, 参考情報として参照した仕様とパッケージのバージョンを記しておきます.
- OTel Spec: 1.49.0
@opentelemetry/api: 1.9.0@opentelemetry/exporter-metrics-otlp-grpc: 0.205.0@opentelemetry/sdk-metrics: 2.1.0@opentelemetry/sdk-node: 0.205.0
参考文献
公式ドキュメントを読めばだいたいわかるだろうと思って読んだけどまあまあ難解で困った.