# メトリクスの補足ガイドライン

> Source: https://www.ymotongpoo.com/works/otel-specs-ja/spec/metrics/supplementary-guidelines/


注: この文書は仕様ではなく、メトリクスの[API](../api/)と[SDK](../sdk/)の仕様を補足するために提供されています。既存の仕様に追加の要件を課すものではありません。

## Guidelines for instrumentation library authors

### Instrument selection

[Instrument](../api/#instrument)は[メトリクスAPI](../api/)の一部です。[Measurement](../api/#measurement)を[同期的に](../api/#同期instrument-api)、または[非同期に](../api/#非同期instrument-api)記録できるようにします。

正しいInstrumentを選ぶことは重要です。その理由は次のとおりです。

* ライブラリがより高い効率を達成しやすくなります。例えば、室温を[Prometheus](https://prometheus.io)へ報告したい場合、センサーを定期的にポーリングするのではなく[非同期Gauge](../api/#非同期gauge)の使用を検討したくなります。そうすれば、スクレイピングが発生したときにのみセンサーへアクセスすることになります。
* ライブラリの利用者にとって消費しやすくなります。例えば、HTTPサーバーのリクエストレイテンシーを報告したい場合、[Histogram](../api/#histogram)の使用を検討したくなります。そうすれば、ほとんどの利用者は、追加の設定を行わずにメトリクスストリームを有効にするだけで、妥当な体験（デフォルトのバケット、最小値・最大値など）を得られます。
* メトリクスストリームの意味が明確になり、利用者が結果をよりよく理解できるようになります。例えば、プロセスのヒープサイズを報告したい場合、[非同期Gauge](../api/#非同期gauge)ではなく[非同期UpDownCounter](../api/#非同期updowncounter)を使うことで、利用者がすべてのプロセスの数値を合計して「合計ヒープサイズ」を得られることを明示できます。

正しいInstrumentを選ぶ方法の1つを次に示します。

* 何かを（デルタ値を記録することによって）**カウント**したい場合。
  * 値が単調増加する（デルタ値が常に非負である）場合は、[Counter](../api/#counter)を使います。
  * 値が単調増加しない（デルタ値が正・負・ゼロのいずれにもなり得る）場合は、[UpDownCounter](../api/#updowncounter)を使います。
* 何かを**記録**または**計測**したく、その**統計量**が意味を持つ可能性が高い場合は、[Histogram](../api/#histogram)を使います。
* 何かを（絶対値を報告することによって）**測定**したい場合。
  * 測定値が[非加算的](#additive-property)である場合は、[非同期Gauge](../api/#非同期gauge)を使います。
  * 測定値が[加算的](#additive-property)である場合。
    * 値が単調増加する場合は、[非同期Counter](../api/#非同期counter)を使います。
    * 値が単調増加しない場合は、[非同期UpDownCounter](../api/#非同期updowncounter)を使います。

### Additive property

OpenTelemetryでは、[Measurement](../api/#measurement)は値と[`Attributes`](../../common/#attribute)の集合をカプセル化します。測定値の性質によって、それらは加算的である場合、非加算的である場合、その中間である場合があります。いくつか例を示します。

* サーバーの温度は非加算的です。下の表の温度を合計すると`226.2`になりますが、この値には実用的な意味がありません。

  | ホスト名 | 温度（°F） |
  | -------- | --------------- |
  | MachineA | 58.8            |
  | MachineB | 86.1            |
  | MachineC | 81.3            |

* 惑星の質量は加算的で、値`1.18e25`（`3.30e23 + 6.42e23 + 4.87e24 + 5.97e24`）は太陽系の地球型惑星の合計質量を意味します。

  | 惑星名 | 質量（kg）       |
  | ----------- | --------------- |
  | 水星     | 3.30e23         |
  | 火星        | 6.42e23         |
  | 金星       | 4.87e24         |
  | 地球       | 5.97e24         |

* 電池セルの電圧は、電池が直列に接続されている場合は合計できます。しかし、並列に接続されている場合、もはや電圧の値を合計することに意味はありません。

OpenTelemetryでは、各[Instrument](../api/#instrument)が加算的かどうかを暗示します。

| Instrument                                                        | 加算性          |
| ----------------------------------------------------------------- | ----------------- |
| [Counter](../api/#counter)                                       | 加算的          |
| [UpDownCounter](../api/#updowncounter)                           | 加算的          |
| [Histogram](../api/#histogram)                                   | 混在<sup>1</sup> |
| [非同期Gauge](../api/#非同期gauge)                 | 非加算的      |
| [非同期Counter](../api/#非同期counter)             | 加算的          |
| [非同期UpDownCounter](../api/#非同期updowncounter) | 加算的          |

1: Histogramのバケットカウントは、バケットが同じであれば加算的です。合計（sum）は加算的ですが、最小値と最大値は非加算的です。

#### Numeric type selection

インクリメントやデクリメントを入力として受け取るInstrument（[Counter](../api/#counter)や[UpDownCounter](../api/#updowncounter)など）では、基盤となる数値型（符号付き整数、符号なし整数、doubleなど）が、ダイナミックレンジ、精度、データの解釈方法に直接影響します。一般に、整数は精度が高い一方でダイナミックレンジが限られ、オーバーフロー・アンダーフローが発生する可能性があります。[IEEE-754倍精度浮動小数点フォーマット](https://en.wikipedia.org/wiki/Double-precision_floating-point_format)は、精度を犠牲にする代わりに広いダイナミックレンジの数値を扱えます。

##### Integer

例を見てみましょう。データベースでコミットされたトランザクションをカウントするために16ビットの符号付き整数を使い、15秒ごとに累積和として報告するとします。

* (T<sub>0</sub>, T<sub>1</sub>]の間、`70`を報告しました。
* (T<sub>0</sub>, T<sub>2</sub>]の間、`115`を報告しました。
* (T<sub>0</sub>, T<sub>3</sub>]の間、`116`を報告しました。
* (T<sub>0</sub>, T<sub>4</sub>]の間、`128`を報告しました。
* (T<sub>0</sub>, T<sub>5</sub>]の間、`128`を報告しました。
* (T<sub>0</sub>, T<sub>6</sub>]の間、`173`を報告しました。
* ...
* (T<sub>0</sub>, T<sub>n+1</sub>]の間、`1,872`を報告しました。
* (T<sub>n+2</sub>, T<sub>n+3</sub>]の間、`35`を報告しました。
* (T<sub>n+2</sub>, T<sub>n+4</sub>]の間、`76`を報告しました。

上記の場合、バックエンドシステムは、(T<sub>n+1</sub>, T<sub>n+2</sub>]の間に開始時刻がT<sub>0</sub>からT<sub>n+2</sub>へ変化したことから、おそらくシステムの再起動があったと判断できます。そのため、データを次のように調整する機会があります。

* (T<sub>0</sub>, T<sub>n+3</sub>]: `1,907`（1,872 + 35）。
* (T<sub>0</sub>, T<sub>n+4</sub>]: `1,948`（1,872 + 76）。

このデータベースを動かし続けたとしましょう。

* (T<sub>0</sub>, T<sub>m+1</sub>]の間、`32,758`を報告しました。
* (T<sub>0</sub>, T<sub>m+2</sub>]の間、`32,762`を報告しました。
* (T<sub>0</sub>, T<sub>m+3</sub>]の間、`-32,738`を報告しました。
* (T<sub>0</sub>, T<sub>m+4</sub>]の間、`-32,712`を報告しました。

上記の場合、バックエンドシステムは、(T<sub>m+2</sub>, T<sub>m+3</sub>]の間に整数オーバーフローがあったと判断できます（開始時刻が変わらず、値が負になっているため）。そのため、データを次のように調整する機会があります。

* (T<sub>0</sub>, T<sub>m+3</sub>]: `32,798`（32,762 + 36）。
* (T<sub>0</sub>, T<sub>m+4</sub>]: `32,824`（32,762 + 62）。

この例で分かるように、16ビット整数という制約があっても、整数オーバーフローによる情報損失を心配することなく、高い忠実度でデータベーストランザクションをカウントできます。

カウンターのリセットや整数のオーバーフロー・アンダーフローを扱えるのは、適切なダイナミックレンジと報告頻度を選んでいるという前提があるからだという点を理解することが重要です。同じ16ビットの符号付き整数を使って、データセンター内（1秒間に数千、あるいは数百万件のトランザクションが発生する可能性があります）のトランザクションをカウントするとしたら、15秒ごとにデータを報告する場合、`-32,738`が`32,762 + 36`の結果なのか、`32,762 + 65,572`なのか、あるいは`32,762 + 131,108`なのかを判別できません。この状況では、より大きな数値型（32ビット整数など）を使うか、報告頻度を上げる（コストが許容できるならマイクロ秒単位にするなど）ことが役立ちます。

##### Float

別の例を見てみましょう。アルファ磁気分光器で検出された陽電子の数をカウントするために[IEEE-754倍精度浮動小数点数](https://en.wikipedia.org/wiki/Double-precision_floating-point_format)を使うとします。陽電子が検出されるたびに、分光器は`counter.Add(1)`を呼び出し、その結果は1秒ごとに累積和として報告されます。

* (T<sub>0</sub>, T<sub>1</sub>]の間、`131,108`を報告しました。
* (T<sub>0</sub>, T<sub>2</sub>]の間、`375,463`を報告しました。
* (T<sub>0</sub>, T<sub>3</sub>]の間、`832,019`を報告しました。
* (T<sub>0</sub>, T<sub>4</sub>]の間、`1,257,308`を報告しました。
* (T<sub>0</sub>, T<sub>5</sub>]の間、`1,860,103`を報告しました。
* ...
* (T<sub>0</sub>, T<sub>n+1</sub>]の間、`9,007,199,254,325,789`を報告しました。
* (T<sub>0</sub>, T<sub>n+2</sub>]の間、`9,007,199,254,740,992`を報告しました。
* (T<sub>0</sub>, T<sub>n+3</sub>]の間、`9,007,199,254,740,992`を報告しました。

上記の場合、T<sub>n+1</sub>とT<sub>n+2</sub>の間のある時点でカウンターの増加が止まっています。これは、IEEE-754のdoubleカウンターが「飽和」したためです。`9,007,199,254,740,992 + 1`は`9,007,199,254,740,992`になるため、値の増加が止まります。

注: ECMAScript 6では、数値`9,007,199,254,740,991`（`2 ^ 53 - 1`）は`Number.MAX_SAFE_INTEGER`として知られており、これはIEEE-754倍精度数として正確に表現できる最大の整数であり、そのIEEE-754表現は他の整数を丸めた結果とはなり得ません。

「飽和」の問題に加えて、IEEE-754のdoubleが[非正規化数](https://en.wikipedia.org/wiki/Subnormal_number)をサポートしていることも理解しておく必要があります。例えば、`1.0E308 + 1.0E308`は`+Inf`（正の無限大）になります。一部のメトリクスバックエンドは非正規化数の扱いに問題を抱えることがあります。

### Monotonicity property

OpenTelemetryのメトリクス[Data Model](../data-model/)と[API](../api/)の仕様では、「単調（monotonic）」という言葉が頻繁に使われています。

異なる[Instrument](#instrument-selection)は単調性の扱いが異なるという点を理解することが重要です。

ネットワークドライバーが受信した総バイト数を記録するために[Counter](../api/#counter)を使う例を見てみましょう。

* 時間範囲(T<sub>0</sub>, T<sub>1</sub>]の間。
  * ネットワークパケットは受信されませんでした。
* 時間範囲(T<sub>1</sub>, T<sub>2</sub>]の間。
  * `30`バイトのパケットを受信しました - `Counter.Add(30)`
  * `200`バイトのパケットを受信しました - `Counter.Add(200)`
  * `50`バイトのパケットを受信しました - `Counter.Add(50)`
* 時間範囲(T<sub>2</sub>, T<sub>3</sub>]の間。
  * `100`バイトのパケットを受信しました - `Counter.Add(100)`

(T<sub>0</sub>, T<sub>1</sub>]の間の合計増分は`0`、(T<sub>1</sub>, T<sub>2</sub>]の間の合計増分は`280`（`30 + 200 + 50`）、(T<sub>2</sub>, T<sub>3</sub>]の間の合計増分は`100`、(T<sub>0</sub>, T<sub>3</sub>]の間の合計増分は`380`（`0 + 280 + 100`）であることが分かります。すべての増分が非負であり、言い換えると**合計は単調増加します**。

「T<sub>3</sub>までに受信した総バイト数は`380`である」と言うのは不正確である点に注意してください。というのも、観測を開始する前（最後のオペレーティングシステムの再起動前など）にドライバーが受信していたネットワークパケットがあるかもしれないからです。正確な言い方は「(T<sub>0</sub>, T<sub>3</sub>]の間に受信した総バイト数は`380`である」です。要するに、このカウントは時間範囲に関連付けられた**レート**を表します。

この単調性という性質は、下流のシステムがデータをよりよく扱えるように追加のヒントを与えるため重要です。受信した総バイト数を累積和のデータストリームで報告する場合を想像してください。

* T<sub>n</sub>で、`3,896,473,820`を報告しました。
* T<sub>n+1</sub>で、`4,294,967,293`を報告しました。
* T<sub>n+2</sub>で、`1,800,372`を報告しました。

バックエンドシステムは、(T<sub>n+1</sub>, T<sub>n+2</sub>]の間に整数オーバーフローかシステムの再起動があったと判断でき、データを「修正」する機会があります。整数オーバーフローに関する詳細については[Additive property](#additive-property)を参照してください。

プロセスの総ページフォールト数を報告するために[非同期Counter](../api/#非同期counter)を使う別の例を見てみましょう。

ページフォールトはオペレーティングシステムによって管理されており、プロセスはいくつかのシステムAPIを通じてページフォールトの数を取得できます。

* T<sub>0</sub>で。
  * プロセスが開始しました。
  * プロセスはオペレーティングシステムにページフォールトの報告を求めませんでした。
* T<sub>1</sub>で。
  * オペレーティングシステムがそのプロセスについて`1000`件のページフォールトを報告しました。
* T<sub>2</sub>で。
  * プロセスはオペレーティングシステムにページフォールトの報告を求めませんでした。
* T<sub>3</sub>で。
  * オペレーティングシステムが`1050`件のページフォールトを報告しました。
* T<sub>4</sub>で。
  * オペレーティングシステムが`1200`件のページフォールトを報告しました。

報告されている値がインクリメントではなく絶対値であり、その値が単調増加していることが分かります。

「(T<sub>3</sub>, T<sub>4</sub>]の間に何件のページフォールトが発生したか」を計算する必要がある場合、`1200 - 1050 = 150`という減算を適用する必要があります。

### Semantic convention

[使用するInstrument](#instrument-selection)を決めたら、次にInstrumentと属性の名前を決める必要があります。

独自のセマンティクスを発明するのではなく、`OpenTelemetryセマンティック規約`に沿わせることを強く推奨します。

## Guidelines for SDK authors

### Aggregation temporality

#### Synchronous example

OpenTelemetryのメトリクス[Data Model](../data-model/)と[SDK](../sdk/)は、CumulativeとDeltaの両方の[Temporality](../data-model/#temporality)をサポートするように設計されています。temporalityがSDKによるメモリ使用量の管理方法に影響することを理解することが重要です。次のHTTPリクエストの例を見てみましょう。

* 時間範囲(T<sub>0</sub>, T<sub>1</sub>]の間。
  * verb = `GET`、status = `200`、duration = `50 (ms)`
  * verb = `GET`、status = `200`、duration = `100 (ms)`
  * verb = `GET`、status = `500`、duration = `1 (ms)`
* 時間範囲(T<sub>1</sub>, T<sub>2</sub>]の間。
  * HTTPリクエストは受信されませんでした。
* 時間範囲(T<sub>2</sub>, T<sub>3</sub>]の間。
  * verb = `GET`、status = `500`、duration = `5 (ms)`
  * verb = `GET`、status = `500`、duration = `2 (ms)`
* 時間範囲(T<sub>3</sub>, T<sub>4</sub>]の間。
  * verb = `GET`、status = `200`、duration = `100 (ms)`
* 時間範囲(T<sub>4</sub>, T<sub>5</sub>]の間。
  * verb = `GET`、status = `200`、duration = `100 (ms)`
  * verb = `GET`、status = `200`、duration = `30 (ms)`
  * verb = `GET`、status = `200`、duration = `50 (ms)`

以下の例では、同期CounterとUpDownCounterの測定値はDelta集約temporalityを指定してAPIに入力されるため、Cumulative集約temporalityより先にDelta集約temporalityを説明します。

##### Synchronous example: Delta aggregation temporality

メトリクスを[Histogram](../data-model/#histogram)としてエクスポートし、話を単純にするため、Histogramのバケットは`(-Inf, +Inf)`の1つだけとします。

**Deltaのtemporality**を使ってメトリクスをエクスポートする場合。

* (T<sub>0</sub>, T<sub>1</sub>]
  * attributes: {verb = `GET`, status = `200`}, count: `2`, min: `50 (ms)`, max: `100 (ms)`
  * attributes: {verb = `GET`, status = `500`}, count: `1`, min: `1 (ms)`, max: `1 (ms)`
* (T<sub>1</sub>, T<sub>2</sub>]
  * Measurementを受け取らなかったため何もありません。
* (T<sub>2</sub>, T<sub>3</sub>]
  * attributes: {verb = `GET`, status = `500`}, count: `2`, min: `2 (ms)`, max: `5 (ms)`
* (T<sub>3</sub>, T<sub>4</sub>]
  * attributes: {verb = `GET`, status = `200`}, count: `1`, min: `100 (ms)`, max: `100 (ms)`
* (T<sub>4</sub>, T<sub>5</sub>]
  * attributes: {verb = `GET`, status = `200`}, count: `3`, min: `30 (ms)`, max: `100 (ms)`

SDKは**直前の収集・エクスポートサイクル以降に発生したことだけを追跡する必要がある**ことが分かります。例えば、SDKが(T<sub>1</sub>, T<sub>2</sub>]でMeasurementの処理を開始したとき、(T<sub>0</sub>, T<sub>1</sub>]の間に発生したことは完全に忘れてかまいません。

##### Synchronous example: Cumulative aggregation temporality

**Cumulativeのtemporality**を使ってメトリクスをエクスポートする場合。

* (T<sub>0</sub>, T<sub>1</sub>]
  * attributes: {verb = `GET`, status = `200`}, count: `2`, min: `50 (ms)`, max: `100 (ms)`
  * attributes: {verb = `GET`, status = `500`}, count: `1`, min: `1 (ms)`, max: `1 (ms)`
* (T<sub>0</sub>, T<sub>2</sub>]
  * attributes: {verb = `GET`, status = `200`}, count: `2`, min: `50 (ms)`, max: `100 (ms)`
  * attributes: {verb = `GET`, status = `500`}, count: `1`, min: `1 (ms)`, max: `1 (ms)`
* (T<sub>0</sub>, T<sub>3</sub>]
  * attributes: {verb = `GET`, status = `200`}, count: `2`, min: `50 (ms)`, max: `100 (ms)`
  * attributes: {verb = `GET`, status = `500`}, count: `3`, min: `1 (ms)`, max: `5 (ms)`
* (T<sub>0</sub>, T<sub>4</sub>]
  * attributes: {verb = `GET`, status = `200`}, count: `3`, min: `50 (ms)`, max: `100 (ms)`
  * attributes: {verb = `GET`, status = `500`}, count: `3`, min: `1 (ms)`, max: `5 (ms)`
* (T<sub>0</sub>, T<sub>5</sub>]
  * attributes: {verb = `GET`, status = `200`}, count: `6`, min: `30 (ms)`, max: `100 (ms)`
  * attributes: {verb = `GET`, status = `500`}, count: `3`, min: `1 (ms)`, max: `5 (ms)`

Delta→Cumulativeの変換を行っており、SDKは**直前の収集・エクスポートサイクル以前に発生したことも追跡しなければならず**、最悪の場合、SDKは**プロセスの開始以来発生したすべてのことを記憶しなければならない**ことが分かります。

長時間稼働するサービスがあり、7個の属性でメトリクスを収集し、各属性が30種類の異なる値を持てるとしましょう。最終的には、`21,870,000,000`通りの組み合わせすべてを記憶することになりかねません。この**カーディナリティ爆発**は、メトリクスの分野でよく知られた課題です。

さらに厄介なのは、直近の更新がなくても組み合わせをエクスポートしてしまうと、エクスポートするバッチが巨大になり、非常にコストが高くなることです。例えば、上記の場合に(T<sub>0</sub>, T<sub>2</sub>]について本当に同じものをエクスポートする必要・意味があるでしょうか。

そこで、SDKの実装者に検討を勧めるいくつかの提案を示します。

* 使用している集約temporalityにかかわらず、メモリ使用量を無制限に増加させるのではなく制御したいはずです。
* **不要になったものを忘れられる**ようにすることで、メモリ効率を改善したいはずです。
* 更新がない場合に同じものを繰り返しエクスポートしたくはないはずです。[Resets and Gaps](../data-model/#resets-and-gaps)の検討をお勧めします。例えば、Cumulativeのメトリクスストリームが長期間更新を受け取っていない場合、開始時刻をリセットしてもよいでしょうか。

#### Asynchronous example

上記の例では、[Histogram Instrument](../api/#histogram)によって報告されたMeasurementを扱っていました。では、[非同期Counter](../api/#非同期counter)から測定値を収集する場合はどうなるでしょうか。

次の例は、各プロセスが開始してからの[ページフォールト](https://en.wikipedia.org/wiki/Page_fault)の数を示しています。

* 時間範囲(T<sub>0</sub>, T<sub>1</sub>]の間。
  * pid = `1001`, #PF = `50`
  * pid = `1002`, #PF = `30`
* 時間範囲(T<sub>1</sub>, T<sub>2</sub>]の間。
  * pid = `1001`, #PF = `53`
  * pid = `1002`, #PF = `38`
* 時間範囲(T<sub>2</sub>, T<sub>3</sub>]の間。
  * pid = `1001`, #PF = `56`
  * pid = `1002`, #PF = `42`
* 時間範囲(T<sub>3</sub>, T<sub>4</sub>]の間。
  * pid = `1001`, #PF = `60`
  * pid = `1002`, #PF = `47`
* 時間範囲(T<sub>4</sub>, T<sub>5</sub>]の間。
  * プロセス1001が終了し、プロセス1003が開始しました。
  * pid = `1002`, #PF = `53`
  * pid = `1003`, #PF = `5`
* 時間範囲(T<sub>5</sub>, T<sub>6</sub>]の間。
  * 新しいプロセス1001が開始しました。
  * pid = `1001`, #PF = `10`
  * pid = `1002`, #PF = `57`
  * pid = `1003`, #PF = `8`

以下の例では、非同期CounterとUpDownCounterの測定値はCumulative集約temporalityを指定してAPIに入力されるため、Delta集約temporalityより先にCumulative集約temporalityを説明します。

##### Asynchronous example: Cumulative temporality

**Cumulativeのtemporality**を使ってメトリクスをエクスポートする場合。

* (T<sub>0</sub>, T<sub>1</sub>]
  * attributes: {pid = `1001`}, sum: `50`
  * attributes: {pid = `1002`}, sum: `30`
* (T<sub>0</sub>, T<sub>2</sub>]
  * attributes: {pid = `1001`}, sum: `53`
  * attributes: {pid = `1002`}, sum: `38`
* (T<sub>0</sub>, T<sub>3</sub>]
  * attributes: {pid = `1001`}, sum: `56`
  * attributes: {pid = `1002`}, sum: `42`
* (T<sub>0</sub>, T<sub>4</sub>]
  * attributes: {pid = `1001`}, sum: `60`
  * attributes: {pid = `1002`}, sum: `47`
* (T<sub>0</sub>, T<sub>5</sub>]
  * attributes: {pid = `1002`}, sum: `53`
* (T<sub>4</sub>, T<sub>5</sub>]
  * attributes: {pid = `1003`}, sum: `5`
* (T<sub>5</sub>, T<sub>6</sub>]
  * attributes: {pid = `1001`}, sum: `10`
* (T<sub>0</sub>, T<sub>6</sub>]
  * attributes: {pid = `1002`}, sum: `57`
* (T<sub>4</sub>, T<sub>6</sub>]
  * attributes: {pid = `1003`}, sum: `8`

最初の4つの期間の振る舞いは単純です。非同期Instrumentから報告されたデータをそのまま送っています。

このデータモデルは、あるストリームが終了し別のストリームが始まるこのケースにおいて、T<sub>5</sub>とT<sub>6</sub>でいくつかの有効な振る舞いを規定しています。[Resets and Gaps](../data-model/#resets-and-gaps)の節では、開始タイムスタンプとstalenessマーカーを使って、受信者がこうした事象をより理解できるようにする方法を説明しています。

SDKが個々のストリームごとに個別のタイムスタンプを保持するか、プロセスごとに1つだけ保持するかを検討してください。この例では、プロセスが終了して再起動すると、ページフォールトのカウントはゼロから開始します。この場合、T<sub>5</sub>とT<sub>6</sub>で考えられる有効な振る舞いは次のとおりです。

1. プロセス内のすべてのストリームが開始時刻を共有しており、SDKが過去のすべてのストリームを記憶する必要がない場合、そのスレッドは合計をゼロにしてプロセスの開始時刻で再起動します。リセット検出機能を持つ受信者は（収集間隔に対して頻繁な再起動を除いて）正しいレートを計算できますが、リセットの正確な時刻は分からなくなります。
2. SDKがストリームごとの開始時刻を保持する場合、直前のコールバック時刻を開始時刻として提供します。この時刻は、以降のコールバックで測定される任意の事象より前だからです。これにより、ストリームの最初の観測が診断にとってより有用になります。下流の消費者はオーバーラップの検出や重複の排除を行えるため、この場合にはリセット検出を必要としません。
3. 上記のいずれの扱いとも独立に、SDKは、あるスレッドが終了した際にストリーム内のギャップの開始を示すstalenessマーカーを追加できます。これは、以前は報告していたが現在は報告していないストリームを記憶しておくことで実現します。ストリームごとの開始タイムスタンプが使われている場合、stalenessマーカーは、ストリーム内のギャップの開始を正確に示し、報告が止まったストリームを忘れることを許すために発行できます。

ストリームごとの開始タイムスタンプとstalenessマーカーを使う選択肢を無視してもかまいません。上記の最初の対応は、追加のメモリやコードを必要とせず、データモデルの観点で正しい対応です。

##### Asynchronous example: Delta temporality

**Deltaのtemporality**を使ってメトリクスをエクスポートする場合。

* (T<sub>0</sub>, T<sub>1</sub>]
  * attributes: {pid = `1001`}, delta: `50`
  * attributes: {pid = `1002`}, delta: `30`
* (T<sub>1</sub>, T<sub>2</sub>]
  * attributes: {pid = `1001`}, delta: `3`
  * attributes: {pid = `1002`}, delta: `8`
* (T<sub>2</sub>, T<sub>3</sub>]
  * attributes: {pid = `1001`}, delta: `3`
  * attributes: {pid = `1002`}, delta: `4`
* (T<sub>3</sub>, T<sub>4</sub>]
  * attributes: {pid = `1001`}, delta: `4`
  * attributes: {pid = `1002`}, delta: `5`
* (T<sub>4</sub>, T<sub>5</sub>]
  * attributes: {pid = `1002`}, delta: `6`
  * attributes: {pid = `1003`}, delta: `5`
* (T<sub>5</sub>, T<sub>6</sub>]
  * attributes: {pid = `1001`}, delta: `10`
  * attributes: {pid = `1002`}, delta: `4`
  * attributes: {pid = `1003`}, delta: `3`

Cumulative→Deltaの変換を行っており、これには**これまでに出会ったすべての組み合わせ**の最後の値を記憶しておく必要があります。そうしなければ、`現在の値 - 直前の値`によってデルタ値を計算できないからです。お分かりの通り、これは非常にコストが高くなります。

さらに興味深いことに、最小値・最大値を持つ場合、CumulativeのtemporalityからDeltaのtemporalityを確実に導き出すことは**数学的に不可能**です。例えば次のとおりです。

* (T<sub>0</sub>, T<sub>2</sub>]における最大値が10であり、(T<sub>0</sub>, T<sub>3</sub>]における最大値が20である場合、(T<sub>2</sub>, T<sub>3</sub>]における最大値は20でなければならないと分かります。
* (T<sub>0</sub>, T<sub>2</sub>]における最大値が20であり、(T<sub>0</sub>, T<sub>3</sub>]における最大値も20である場合、値が存在しない（count = 0）と分かっていない限り、(T<sub>2</sub>, T<sub>3</sub>]における最大値が何であるかは分かりません。

そこで、SDKの実装者に検討を勧めるいくつかの提案を示します。

* Cumulative→Deltaの変換を行う必要があり、最小値・最大値に出会った場合、そのデータを単純に捨てるのではなく、[Gauge](../data-model/#gauge)など何か有用なものへ変換することを検討したくなるでしょう。

##### Asynchronous example: attribute removal in a view

上記の非同期の例のメトリクスが、`pid`属性を除去するように設定されたビュー経由でエクスポートされ、ページフォールトのカウントだけが残るとします。各メトリクスストリームについて、同じ時間区間をカバーする2つの測定値が生成され、SDKは出力を生成する前にそれらを集約することが期待されます。

このデータモデルは「natural merge」関数を使うことを規定しており、この場合は現在のポイントの値を足し合わせることを意味します。これらは`Sum`データポイントだからです。期待される出力は、依然として**Cumulativeのtemporality**で次のようになります。

* (T<sub>0</sub>, T<sub>1</sub>]
  * dimensions: {}, sum: `80`
* (T<sub>0</sub>, T<sub>2</sub>]
  * dimensions: {}, sum: `91`
* (T<sub>0</sub>, T<sub>3</sub>]
  * dimensions: {}, sum: `98`
* (T<sub>0</sub>, T<sub>4</sub>]
  * dimensions: {}, sum: `107`
* (T<sub>0</sub>, T<sub>5</sub>]
  * dimensions: {}, sum: `58`
* (T<sub>0</sub>, T<sub>6</sub>]
  * dimensions: {}, sum: `75`

上記の非同期Cumulative temporalityの例で説明した通り、リセットを検出するためのさまざまな対応があります。何もしないという最初の対応を取ったとしても、データモデルの[unknown start time](../data-model/#cumulative-streams-handling-unknown-start-time)と[inserting true start times](../data-model/#cumulative-streams-inserting-true-reset-points)のルールに従う受信者は、この場合正しいレートを計算します。T<sub>5</sub>で受け取った「58」はストリームをリセットします。「107」から「58」への変化はギャップとして記録され、T<sub>6</sub>でレートの計算は正しく再開します。リセット処理のルールは、T<sub>4</sub>の「107」に反映されていた「58」の未知の部分が、T<sub>5</sub>のリセットで二重にカウントされないようにするために設けられています。

上記でストリームごとの開始タイムスタンプを使う選択肢を取った場合、受信者の負担が軽くなり、ギャップを正確に監視し、重複するストリームを検出できるようになります。ストリームごとの状態が利用可能な場合、SDKには、報告が止まってしばらく経ってからリセットされる属性が存在する状況でビューを計算するためのいくつかの手法があります。

1. プロセスの生存期間全体にわたってすべてのストリームの累積値を記憶しておくことで、`attributes`が現れたり消えたりしても累積和は正しくなります。この場合、SDKはストリームごとのリセットを自分で検出しなければならず、そうしなければビューは誤って計算されます。
2. すべてのストリームの`attributes`を記憶するコストが高すぎる場合、ビューとそのすべての状態をリセットし、新しい開始タイムスタンプを与え、呼び出し元にストリーム内のギャップを見せます。

この点を検討する際は、メトリクスAPIが各非同期Instrumentについて推奨事項を持っていることにも注意してください。[利用者のコードは、単一のコールバック内で同じ`attributes`を持つ`Measurement`を複数提供しないことが推奨されます。](../api/#instrument)この点についての利用者の誤りが、ビューの正しさにどう影響するかを検討してください。ビューの正しさのためにストリームごとの状態を維持する場合、SDKの実装者は、利用者が重複した測定値を作っていないかを検出することを検討したくなるでしょう。重複した測定値をチェックしない場合、ビューは誤って計算される可能性があります。

### Memory management

メモリ管理は幅広いトピックですが、ここではOpenTelemetry SDKにとって最も重要な事項のいくつかだけを取り上げます。

**SDKが記憶しておくべき事柄を減らせるように、よりよい設計を選びます**。どうしても必要でない限り、メモリに事柄を保持し続けることを避けます。良い例が[aggregation temporality](#aggregation-temporality)です。

**よりよいメモリレイアウトを設計します**。そうすることで、ストレージが効率的になり、ストレージへのアクセスを高速化できます。これは通常、対象のプログラミング言語やプラットフォームに固有のものです。例えば、メモリをCPUキャッシュラインに合わせて配置する、ホットなメモリを互いに近くに配置する、メモリをハードウェアに近づける（ノンページドプール、[NUMA](https://en.wikipedia.org/wiki/Non-uniform_memory_access)など）といったことです。

**メモリを事前に割り当ててプールします**。そうすることで、SDKはその場でメモリを割り当てる必要がなくなります。これは、ガベージコレクターを持つ言語ランタイムにとって特に有用です。コード内のホットパスがガベージコレクションを引き起こさないことを保証できるからです。

**メモリ使用量を制限し、クリティカルなメモリ状況を処理します。** 一般的な期待として、テレメトリーSDKはアプリケーションを失敗させるべきではありません。これは、何らかのカーディナリティ上限アルゴリズムによって実現できます。例えば、SDKがメモリの上限に達したときに一部のデータポイントを結合・破棄し始め、データ損失を報告する仕組みを提供するといった方法です。

**アプリケーションの所有者に設定を提供します。** 「効率的なメモリ使用とは何か」という問いへの答えは、最終的にはアプリケーションの所有者の目的に依存します。例えば、アプリケーションの所有者は、メトリクス属性の組み合わせをより多く保持するためにメモリをより多く使いたいと考えることもあれば、重要な一部の属性に対してはメモリを積極的に使い、重要度の低い属性に対しては控えめな上限を保ちたいと考えることもあります。

