OTEP-0072: メトリクスオブザーバー仕様(改訂)

メトリクスオブザーバーゲージはOTEP 0008で説明されていましたが、以前のOTEPがオブザーバーゲージメトリクス計装器の有効な呼び出し規約を明確にしていなかったため、現在のメトリクス仕様からは除外されていました。 この提案はOTEP 0008を完全に置き換えるものです。

動機

メトリクス仕様の初期のバージョンでは、メトリクスコールバックはメトリクスイベントを生成するための代替手段として説明されており、アプリケーションは収集間隔が望む頻度でのみメトリクスイベントを生成できるようになっていました。 このサポートはすべての種類の計装器に対して規定されていました。

この提案は、ゲージ計装器と同じセマンティクスを持つ専用の Observer 種別の計装器に限定して、コールバックを使用できる機能を復活させます。 ゲージ計装器と同様に、オブザーバー計装器は変数の現在の値を報告するために使用されます。

なぜオブザーバー計装器がAPIの第一級の要素であるべきなのか、単にメトリクス収集間隔でユーザーレベルのコードを呼び出すための計装器非依存のコールバックを登録するだけではいけないのか、と問うこともできるでしょう。 そうすれば、通常のゲージ計装器を、ここで提案するオブザーバー計装器の代わりとして使用することが可能になります。 ここで提案するアプローチはより柔軟です。 なぜなら、Meterの実装が計装器ごとに収集間隔を制御したり、計装器を無効化したりできるようになるからです。

解説

ゲージメトリクス計装器は通常、測定間隔が任意である、システムによって事前計算されるか瞬時に読み取られるプロパティを反映するために使用されます。 カウンターや測定(measure)の種類のメトリクス計装器とは異なり、ゲージを選択する場合、現在の値の計算や読み取りにかなりの計算コストがかかることがあります。 このような場合、最適化として、値をオンデマンドで提供することに関心を持つのは理解できます。

オブザーバー計装器の目的にとって、この最適化という側面は極めて重要です。 もし上で示したより単純な代替案——計装器非依存のコールバックを登録するという方法——が代わりに実装された場合、呼び出し側は、計装器が「記録中(recording)」かどうかを尋ねる方法を要求するでしょう。 これはSpan.IsRecording APIに似ています。

オブザーバー計装器はゲージ計装器と意味的に等価ですが、Set() 操作の代わりにコールバックをサポートする点が異なります。 オブザーバーコールバックは、Observe() をサポートします。 なぜCounterやMeasureのセマンティクスに対しては同じことをせず、Gaugeのセマンティクスに対してのみコールバックをサポートするのでしょうか。

なぜMeasureコールバックはないのか

Measure計装器は、その定義上、個々の測定値についての情報を保持しているため、評価をコールバックへ遅延させることに利点はありません。 オブザーバーコールバックは測定の回数を減らすために設計されており、これはMeasure計装器のセマンティクスと相容れません。

なぜCounterコールバックはないのか

Counter計装器は、事前計算のコストが高い場合や、瞬時に読み取られる場合には、オブザーバー計装器として表現できます。 これには、オブザーバー計装器のセマンティクスを用いて扱う方法が2通りあります。

オブザーバー計装器は、ゲージ計装器と同様に、デフォルトで「最後の値(last value)」による集約を使用します。 このデフォルトの解釈を踏まえると、単調増加のCounterは、Observe() から現在の合計値を報告するだけで、単調増加のオブザーバー計装器として表現できます。 その場合、「最後の値」を直接合計値として解釈できます。 現在の合計値に対するレート計算をサポートするシステム(Prometheusなど)は、これらのメトリクスをそのまま使用できます。 非単調のCounterはその現在の値として表現できますが、この方法では意味のある集約はできません。

オブザーバー計装器のコールバックからCounterのようなデータを Observe() する望ましい方法は、コールバック内で差分(delta)を報告し、エクスポーター側でSum集約を設定することです。 この方法で報告されたデータは、真のCounterと同様にレート計算をサポートします。

GaugeとObserverの違い

明示的に Set() されるゲージと、オブザーバーコールバックとを比較したときの重要な違いの1つは、Set() がコンテキスト(すなわち分散コンテキスト)の内部で発生するのに対し、オブザーバーコールバックはいかなる分散コンテキストの下でも実行されないという点です。

ゲージの値は Set() が呼び出された時点でコンテキストを持ちますが、オブザーバーコールバックはコンテキストを持ちません。 オブザーバー計装器は、リクエストに固有ではない値を報告するのに適しています。

詳細

オブザーバー計装器はゲージ計装器と意味的に等価ですが、異なる呼び出し規約を使用します。 オブザーバー計装器には、言語固有のコンストラクタ(例: metric.NewFloat64Observer())を使用してください。 オブザーバー計装器は、ゲージ計装器と同様に MonotonicNonMonotonic のオプションをサポートします。

コールバックはブロッキングを避けるべきです(SHOULD)。 コールバックが長時間ブロックする場合、実装は計算をキャンセルすることを要求される場合があります。

コールバックは、いかなるOpenTelemetry APIを介しても、アプリケーションコードと同期的に呼び出されてはなりません(MUST NOT)。 これにより、アプリケーションが自身のスレッドから同期的に呼び出されることによって、潜在的に自らをデッドロックさせてしまう事態を防ぎます。 この保証を提供できない実装は、オブザーバー計装器を実装しないことが望ましいです(SHOULD)。

コールバックは、上記の要件に反しない限り、エクスポーターに代わってSDK内で同期的に呼び出されてもかまいません(MAY)。

コールバックは、Observe() に提供されるインターフェース以外のOpenTelemetry APIの呼び出しを避けるべきです(SHOULD)。 これにより、SDKが自身のスレッドから同期的に呼び出されることによって、潜在的に自らをデッドロックさせてしまう事態を防ぎます。 これを強制することは不可能であるか、コストが高い場合があると私たちは認識しています。 SDKは、このような再入の試みに対してどのように応答するかを文書化するべきです(SHOULD)。

Observerの呼び出し規約

オブザーバーコールバックは、以下のように、コールバック内で直接イベントを捕捉することをサポートするインターフェースである ObserverResult とともに呼び出されます。

特定の LabelSet を用いた観測値を捕捉するには、ObserverResult.Observe(value, LabelSet) を使って ObserverResult を直接呼び出します。

Counter、Gauge、Measureの各計装器に存在するような「束縛された(bound)」オブザーバー計装器に相当するものはありません。 オブザーバー計装器では束縛の呼び出し規約は必要ありません。 なぜなら、オブザーバー計装器は収集時に呼び出されるため、収集と並行して「アクティブな」レコードを維持する必要がないからであり、そうすることによる性能上の利点はほとんど、あるいはまったくないためです。

1回のコールバック呼び出しの中で複数の観測を行うことが許されています。

コールバックに渡された ObserverResult は、渡された呼び出しの外で使用するべきではありません(SHOULD NOT)。

計装器ごとに1つのコールバック

このAPIは、登録された(「束縛された」)ラベルセットに紐づく独立したコールバックを登録する方式を採ることも_できました_が、代わりに計装器ごとに1つのコールバックをサポートする方式を採用しています。 検討すべきケースは2つあります。 (a) 計装器の値のソースが一度に1つの値しか提供しない場合、(b) 計装器の値のソースが一度に複数の値を提供する場合です。

計装器ごとに1つのコールバックをサポートするという決定は、次の理由により正当化されます。 上記の(a)のケースでは、複数の値のためにソースを複数回呼び出すことが比較的容易であるのに対し、(b)のケースでは、ソースを1回呼び出して複数のコールバックから値を報告することが比較的困難であるためです。

疑似コード

例を示します。

class YourClass {
  private static final Meter meter = ...;
  private static final ObserverDouble cpuLoad = ...;

  void init() {
    LabelSet labelSet = meter.createLabelSet("low_power", isLowPowerMode());
    cpuLoad.setCallback(
        new ObserverDouble.Callback<ObserverDouble.Result>() {
          @Override
          public void update(Result result) {
              result.Observe(getCPULoad(), labelSet);
        });
  }
}

トレードオフと緩和策

コールバックは比較的危険なプログラミングパターンであり、アプリケーションとAPIまたはSDKとの間のデッドロックを避けるために注意が必要になる場合があります。 実装は、安全かつ経済的な手段によってデッドロックを防ぐことを検討するべきです(SHOULD)。