# メトリクスのデータモデル

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


**ステータス**: [Mixed](../../document-status/)

## Overview

**ステータス**: [Stable](../../document-status/)

メトリクスに関するOpenTelemetryのデータモデルは、事前に集約されたメトリクスのタイムシリーズデータを配送するためのプロトコル仕様とセマンティック規約から構成されます。このデータモデルは、既存のシステムからデータを取り込み、既存のシステムへデータを書き出すために設計されているのに加え、SpanやLogのストリームからメトリクスを生成するというOpenTelemetry内部のユースケースをサポートするためにも設計されています。

既存の人気のメトリクスデータフォーマットは、意味や忠実度を損なうことなく、メトリクスに関するOpenTelemetryのデータモデルへ一意に変換できます。PrometheusとStatsdの公開フォーマットからの変換は明示的に規定されています。

このデータモデルは、収集経路上で使う、意味を保存する多数のデータ変換を規定しており、柔軟なシステム構成をサポートします。このモデルは、CumulativeとDeltaのいずれの転送を選ぶかによって、信頼性とステートレス性の制御をサポートします。このモデルは、空間的・時間的な再集約によってコストの制御をサポートします。

OpenTelemetry Collectorは、多数のフォーマットでメトリクスデータを受け付け、OpenTelemetryのデータモデルを使ってデータを転送し、既存のシステムへエクスポートするように設計されています。このデータモデルは、属性の自動削除やヒストグラムの解像度の低減を含む、明確に定義されたデータの変換を通じて、機能や意味を損なうことなくPrometheus Remote Writeプロトコルへ一意に変換できます。

## Events => Data Stream => Timeseries

**ステータス**: [Stable](../../document-status/)

OTLPメトリクスプロトコルは、メトリクスデータを転送するための標準として設計されています。このデータの意図された使い方と関連する意味を記述するため、OpenTelemetryのメトリクスデータストリーム型は、メトリクスAPIと離散的な入力値に関する上位レベルのモデル、およびTimeseriesと離散的な出力値を定義する下位レベルのモデルを含む、より大きな枠組みに結び付けられます。モデル間の関係は下の図に示されています。

![Events → Data Stream → Timeseries Diagram](https://raw.githubusercontent.com/open-telemetry/opentelemetry-specification/v1.60.0/specification/metrics/img/model-layers.png)

このプロトコルは、OpenCensus Metricsシステムの要件、特にMetrics Viewsという概念を満たすように設計されました。OpenTelemetryのメトリクスデータモデルでは、収集経路上でのデータ変換をサポートすることでViewを実現しています。

OpenTelemetryは、コスト・信頼性・リソース配分を制御する方法として、メトリクス収集システムを構築する際に有用な、意味を保存する3種類のメトリクスデータ変換を特定しています。OpenTelemetryのメトリクスデータモデルは、これらの変換を、データが発生するSDK内部でも、OpenTelemetry Collector内部での再処理ステージとしても、サポートするように設計されています。これらの変換は次のとおりです。

1. **時間的な再集約**: 高頻度で収集されるメトリクスは、より長い間隔へ再集約でき、低解像度のタイムシリーズを事前計算したり、元のメトリクスデータの代わりに使ったりできます。
2. **空間的な再集約**: 不要な属性を伴って生成されたメトリクスは、より少ない属性を持つメトリクスへ再集約できます。
3. **Delta-to-Cumulative**: Delta temporalityで入出力されるメトリクスは、クライアントが高カーディナリティな状態を保持する負担から解放されます。deltaを使うことで、下流のサービスがCumulativeのタイムシリーズへの変換コストを負担するか、そのコストを負担せずに直接レートを計算するかを選べます。

OpenTelemetryのメトリクスデータストリームは、以下に示す条件のもとで、これらの変換を同じ型のストリームへ自動的に適用できるように設計されています。すべてのOTLPデータストリームは、[分解可能な集約関数](https://en.wikipedia.org/wiki/Aggregate_function#Decomposable_aggregate_functions)を本質的に持っており、時間的・空間的な属性の両方をまたいでデータポイントをマージすることが意味的に明確に定義されています。また、すべてのOTLPデータポイントは意味を持つ2つのタイムスタンプを持ち、これは本質的な集約と組み合わさることで、モデルの基本的な各ポイント種別について標準的なメトリクスデータ変換を実行しつつ、その結果が意図した意味を保つことを可能にします。

OpenCensus Metricsと同様に、メトリクスデータは、集約間隔と望ましい属性を選ぶだけで、1つ以上のViewへ変換できます。1本のOTLPデータストリームは、異なるViewを設定することで複数のタイムシリーズ出力へ変換でき、必要なViewの処理はSDK内部でも外部のCollectorによっても適用できます。

### Example Use-cases

このメトリクスデータモデルは、一連の「中核となる」ユースケースを中心に設計されています。この一覧は網羅的ではありませんが、OTelメトリクスの使われ方の範囲と広さを代表するものであることを意図しています。

1. OTel SDKが、状態を持つクライアント・状態を持たないサーバーのためにCumulative temporalityを使って、10秒の解像度で単一のOTel Collectorへエクスポートします。
    - Collectorは元のデータをそのままOTLPの宛先へパススルーします
    - Collectorは属性を変えずに、より長い間隔へ再集約します
    - Collectorは、利用可能な属性の部分集合をそれぞれ持つ複数の異なるViewへ再集約し、同じ宛先へ出力します
2. OTel SDKが、状態を持たないクライアント・状態を持つサーバーのためにDelta temporalityを使って、10秒の解像度で単一のOTel Collectorへエクスポートします。
    - Collectorは60秒の解像度へ再集約します
    - CollectorはDeltaをCumulative temporalityへ変換します
3. OTel SDKが、10秒の解像度（CPU、リクエストレイテンシーなど）と15分の解像度（室温など）の両方を単一のOTel Collectorへエクスポートします。Collectorは、集約の有無にかかわらず、ストリームを上流にエクスポートします。
4. ローカルで実行されている複数のOTel SDKが、それぞれ10秒の解像度でエクスポートし、それぞれが単一の（ローカルの）OTel Collectorへ報告します。
    - Collectorは60秒の解像度へ再集約します
    - Collectorは、個々のSDKのアイデンティティ（異なる`service.instance.id`の値など）を取り除くように再集約します
    - CollectorはOTLPの宛先へ出力します
5. OTel Collectorのプールが、OTLPを受け取り、Prometheus Remote Writeへエクスポートします。
    - Collectorはサービスディスカバリーとメトリクスリソースを結合します
    - Collectorは「up」やstalenessマーカーを計算します
    - Collectorは特定の外部ラベルを適用します
6. OTel Collectorが、Statsdを受け取り、OTLPへエクスポートします。
    - Delta temporalityの場合: 状態を持たないCollector
    - Cumulative temporalityの場合: 状態を持つCollector
7. OTel SDKが、サードパーティのバックエンドへ直接エクスポートします。

これらは、メトリクスデータモデル内でのトレードオフと設計上の決定を分析するために使われる、「中核」のユースケースとみなされています。

### Out of Scope Use-cases

このメトリクスデータモデルは、メトリクスの完璧なロゼッタストーンとなるように設計されているわけでは**ありません**。以下は、完全にサポート対象外というわけではないものの、主要な設計上の決定においては対象範囲に含まれないユースケースの一覧です。

- 互換性のない2つのフォーマット間の中間フォーマットとしてOTLPを使うこと
  - [statsd](https://github.com/statsd/statsd)からPrometheus PRWへの取り込み
  - [collectd](https://github.com/collectd/collectd/wiki/Binary-protocol)からPrometheus PRWへの取り込み
  - Prometheusエンドポイントのスクレイプから[statsd push | collectd | opencensus]への取り込み
  - OpenCensusの「oca」から、OCでもOTelでもない任意のフォーマットへの取り込み
- TODO: 他のケースを定義する。

## Model Details

**ステータス**: [Stable](../../document-status/)

OpenTelemetryは、メトリクスを相互作用する3つのモデルに分解します。

<!-- cSpell:ignore emetry -->
- **Event model**: 計装がどのようにメトリクスデータを報告するかを表すモデルです。
- **Timeseries model**: バックエンドがどのようにメトリクスデータを保存するかを表すモデルです。
- **Metric Stream model**: *O*pen*T*e*L*emetry *P*rotocol（OTLP）を定義するモデルであり、メトリクスデータストリームがEvent modelとTimeseriesストレージの間でどのように操作・送信されるかを表します。これが本文書で規定するモデルです。

### Event Model

Event modelは、データの記録が行われる場所です。その基盤は[Instrument](../api/#instrument)からできており、Instrumentはイベントを通じて観測データを記録するために使われます。これらの生のイベントは、何らかの形で変換されてから、別のシステムへ送信されます。OpenTelemetryのメトリクスは、同じInstrumentとイベントを異なる方法で使ってメトリクスストリームを生成できるように設計されています。

![Events → Streams](https://raw.githubusercontent.com/open-telemetry/opentelemetry-specification/v1.60.0/specification/metrics/img/model-event-layer.png)

観測イベントを直接バックエンドへ報告することも可能ですが、オブザーバビリティシステムで使われるデータの量が膨大であることと、テレメトリー収集の目的で利用できるネットワーク・CPUリソースが限られていることから、実際には非現実的です。この最良の例がHistogramメトリクスであり、生のイベントは個々のタイムシリーズとしてではなく圧縮されたフォーマットで記録されます。

> [!NOTE]
> 上の図は、1つのInstrumentがイベントを複数の種類のメトリクスストリームへ変換できることを示しています。これをいつ、どのように行うかについては、注意点や細かい違いがあります。Instrumentとメトリクスの設定については、[メトリクスAPI仕様書](../api/)に概説されています。

OpenTelemetryは、Instrumentをどのようにメトリクスストリームへ変換するかについて柔軟性を提供していますが、Instrumentは、妥当なデフォルトのマッピングを提供できるように定義されています。正確な[OpenTelemetryのInstrument](../api/#instrument)については、API仕様書で詳しく説明されています。

Event modelにおいて、主要なデータは（Instrument、数値）のポイントであり、（それぞれ同期・非同期のケースについて）リアルタイムに、あるいは必要に応じて観測されます。

### Timeseries Model

この低レベルのメトリクスデータモデルでは、Timeseriesは、いくつかのメタデータプロパティから構成されるエンティティによって定義されます。

- メトリクス名
- 属性（次元）
- ポイントの値の型（整数、浮動小数点数など）
- 計測単位

各タイムシリーズの主要なデータは、順序付けられた（タイムスタンプ、値）のポイントであり、次のいずれかの値の型を持ちます。

1. Counter（単調、Cumulative）
2. Gauge
3. Histogram
4. Exponential Histogram

このモデルは、[Prometheus Remote Write](https://docs.google.com/document/d/1LPhVRSFkGNSuU1fBd81ulhsCPR4hkSZyyBj1SZ8fWOM/edit#heading=h.3p42p5s8n0ui)を理想化したものと見なせます。そのプロトコルと同様に、ポイントの値が暗黙的または明示的に欠落している場合と比べて、いつ値が定義されているかを知ることにも関心があります。deltaデータポイントのメトリクスストリームは、時点の値ではなく時間区間の値を定義します。データの存在・欠落を正確に定義するには、これらのモデル間の対応関係についてさらなる検討が必要です。

注: Prometheusは、OpenTelemetryがマッピングできる唯一のタイムシリーズモデルではありませんが、本文書全体を通して参照として使われています。

### OpenTelemetry Protocol data model

OpenTelemetry Protocol（OTLP）のデータモデルは、Metricデータストリームから構成されます。これらのストリームは、さらにメトリクスデータポイントから構成されます。Metricデータストリームは、Timeseriesへ直接変換できます。

メトリクスストリームは、次の要素によって識別される個々の`Metric`オブジェクトへグループ化されます。

- 発生元の`Resource`属性
- 計装の`Scope`（計装ライブラリ名、バージョンなど）
- メトリクスストリームの`name`

`name`を含め、`Metric`オブジェクトは次のプロパティによって定義されます。

- データポイントの型（`Sum`、`Gauge`、`Histogram`、`ExponentialHistogram`、`Summary`など）
- メトリクスストリームの`unit`
- メトリクスストリームの`description`
- 該当する場合の本質的なデータポイントのプロパティ: `AggregationTemporality`、`Monotonic`

データポイントの型、`unit`、本質的なプロパティは識別的なものとみなされますが、`description`フィールドは明示的に識別的な性質を持たないものとされます。

特定のポイントの外在的なプロパティは識別的なものとはみなされません。これには次のものが含まれますが、これらに限りません。

- `Histogram`データポイントのバケット境界
- `ExponentialHistogram`データポイントのスケールやバケット数

`Metric`オブジェクトは、`Attributes`の集合によって識別される個々のストリームを含みます。個々のストリームの中で、ポイントは1つまたは2つのタイムスタンプによって識別され、詳細はデータポイントの型によって異なります。

一部のデータポイントの型（`Sum`や`Gauge`など）では、数値のポイントの値にばらつきが許容されています。この場合、関連するばらつき（浮動小数点数か整数かなど）は識別的なものとはみなされません。

#### OpenTelemetry Protocol data model: Producer recommendations

Producerは、同じ`Resource`と`Scope`属性を持つ、ある`name`について複数の`Metric`アイデンティティが存在することを防ぐべきです（SHOULD）。Producerは、同一の`Metric`オブジェクトについてデータを集約することが基本的な機能として期待されているため、複数の`Metric`が出現すること（「セマンティックエラー」とみなされます）は、通常、どこかで競合する重複したInstrumentの登録が発生したことを意味します。

Producerは、それがSDKであるか下流のプロセッサーであるかによって、この問題を修復できることがあります（MAY）。

1. 潜在的な競合が識別的でないプロパティ（すなわち`description`）に関わる場合、Producerはより長い文字列を選ぶべきです（SHOULD）。
2. 潜在的な競合が、類似しているが一致しない単位（「ms」と「s」など）に関わる場合、実装は単位を変換してセマンティックエラーを避けてもよい（MAY）です。そうでない場合、実装は利用者にセマンティックエラーを通知し、競合するデータをそのまま通過させるべきです（SHOULD）。
3. 潜在的な競合が`AggregationTemporality`プロパティに関わる場合、実装はCumulative-to-DeltaまたはDelta-to-Cumulativeの変換を使ってtemporalityを変換してもよい（MAY）です。そうでない場合、実装は利用者にセマンティックエラーを通知し、競合するデータをそのまま通過させるべきです（SHOULD）。
4. 一般に、識別的なプロパティ（`description`を除くすべてのプロパティ）に関わる潜在的な競合について、Producerは利用者にセマンティックエラーを通知し、競合するデータをそのまま通過させるべきです（SHOULD）。

こうしたセマンティックエラーがOpenTelemetry APIの実装内部で発生する場合、`Resource`の値は固定されているという前提があります。したがって、OpenTelemetry APIを実装するSDKは、重複したInstrument登録の競合の発生元について完全な情報を持っており、利用者がセマンティックエラーを避けられるよう手助けできることがあります。詳細はSDK仕様書を参照してください。

#### OpenTelemetry Protocol data model: Consumer recommendations

Consumerは、セマンティックエラーを含むOpenTelemetryのメトリクスデータ（すなわち、ある`name`、`Resource`、`Scope`に対して複数の`Metric`アイデンティティが存在するデータ）を拒否してもよい（MAY）です。

OpenTelemetryは、そうした結果をエンドユーザーへ伝える手段を規定していませんが、この点は今後の検討課題です。

#### Point kinds

メトリクスストリームは、次の基本的なポイント種別のいずれかを使えます。いずれも上記の要件を満たし、つまり同じ種別のポイントに対する分解可能な集約関数（「natural merge」関数とも呼ばれます）を定義します。<sup>[1](#footnotes)</sup>

基本的なポイント種別は次のとおりです。

1. [Sum](https://github.com/open-telemetry/opentelemetry-proto/blob/c5c8b28012583fda55b0cb16f73a820722171d49/opentelemetry/proto/metrics/v1/metrics.proto#L247)
2. [Gauge](https://github.com/open-telemetry/opentelemetry-proto/blob/c5c8b28012583fda55b0cb16f73a820722171d49/opentelemetry/proto/metrics/v1/metrics.proto#L241)
3. [Histogram](https://github.com/open-telemetry/opentelemetry-proto/blob/c5c8b28012583fda55b0cb16f73a820722171d49/opentelemetry/proto/metrics/v1/metrics.proto#L260)
4. [Exponential Histogram](https://github.com/open-telemetry/opentelemetry-proto/blob/c5c8b28012583fda55b0cb16f73a820722171d49/opentelemetry/proto/metrics/v1/metrics.proto#L270)

OTLPのMetric Data StreamとTimeseriesのデータモデルを比較すると、OTLPはそのポイント型からTimeseriesのポイントへ1対1でマッピングされるわけではありません。OTLPでは、Sumポイントは単調なカウントも非単調なカウントも表せます。つまり、OTLPのSumは、そのsumが単調である場合はTimeseriesのCounterに変換され、単調でない場合はGaugeに変換されます。

![Stream → Timeseries](https://raw.githubusercontent.com/open-telemetry/opentelemetry-specification/v1.60.0/specification/metrics/img/model-layers-stream.png)

具体的には、OpenTelemetryにおいてSumは常に加算によって結合できる集約関数を持ちます。したがって、OpenTelemetryにおける非単調なsumについては、（自然に）加算によって集約できます。Timeseriesモデルでは、任意のGaugeがsumであると仮定することはできないため、デフォルトの集約は加算にはなりません。

OTLPで使われる中核のポイント種別に加えて、既存のメトリクスフォーマットとの互換性のために設計されたデータ型もあります。

- [Summary](#summary-legacy)

## Metric Points

**ステータス**: [Stable](../../document-status/)

メトリクスポイントは、メトリクスの基本的な構成要素です。[ポイント種別](#point-kinds)によって、メトリクスポイントが持つフィールドは異なります。以下の節では、各ポイント種別のフィールドと、これらのポイントがどのようにメトリクスを形成するかを説明します。

### Sums

OTLPにおける[Sum](https://github.com/open-telemetry/opentelemetry-proto/blob/v0.9.0/opentelemetry/proto/metrics/v1/metrics.proto#L230)は、次のもので構成されます。

- deltaまたはcumulativeの*Aggregation Temporality*。
- Sumが[単調](https://en.wikipedia.org/wiki/Monotonic_function)かどうかを示すフラグ。メトリクスの文脈では、これはsumが名目上増加することを意味し、一般性を失わずにこの前提を置きます。
  - deltaかつ単調なsumの場合、読み手は非負の値を期待すべきです（SHOULD）。
  - cumulativeかつ単調なsumの場合、読み手は直前の値より小さくない値を期待すべきです（SHOULD）。
- データポイントの集合。それぞれ次のものを含みます。
  - 独立した属性名・値の組の集合。
  - 集約temporalityに応じて、deltaまたはcumulativeのsumを表す数値。
  - Sumが計算された時間ウィンドウ（`(start, end]`）。
    - この時間区間は終了時刻を含みます。
    - 時刻は、`1970年1月1日00:00:00 UTC`からのUNIXエポック時刻（ナノ秒）で指定されます。
    - `start`タイムスタンプは、このタイムシリーズについて測定が記録され得た最初の時点を最もよく表します。
  - （オプション）エグゼンプラーの集合（[Exemplars](#exemplars)を参照）。
  - （オプション）データポイントフラグ（[Data point flags](#data-point-flags)を参照）。

集約temporalityは、そのsumが計算された文脈を理解するために使われます。集約temporalityが「delta」の場合、メトリクスストリームの時間ウィンドウに重複がないことが期待されます。例えば次のとおりです。

![Delta Sum](https://raw.githubusercontent.com/open-telemetry/opentelemetry-specification/v1.60.0/specification/metrics/img/model-delta-sum.png)

これをcumulative集約temporalityと対比すると、cumulativeでは「start」（多くの場合プロセス・アプリケーションの開始に近い時点）以降の合計全体を報告することが期待されます。

![Cumulative Sum](https://raw.githubusercontent.com/open-telemetry/opentelemetry-specification/v1.60.0/specification/metrics/img/model-cumulative-sum.png)

DeltaとCumulativeのどちらの集約を使うかには、さまざまなユースケースにおいてさまざまなトレードオフがあります。例えば次のとおりです。

- プロセスの再起動の検出
- レートの計算
- PushベースかPullベースかのメトリクス報告

OTLPは両方のモデルをサポートし、API・SDK・利用者が自分のユースケースに最適なトレードオフを決められるようにします。

### Gauge

OTLPにおける[Gauge](https://github.com/open-telemetry/opentelemetry-proto/blob/v0.9.0/opentelemetry/proto/metrics/v1/metrics.proto#L200)は、ある時点でサンプリングされた値を表します。Gaugeストリームは次のもので構成されます。

- データポイントの集合。それぞれ次のものを含みます。
  - 独立した属性名・値の組の集合。
  - サンプリングされた値（現在のCPU温度など）
  - 値がサンプリングされた時刻のタイムスタンプ（`time_unix_nano`）
  - （オプション）このタイムシリーズについて測定が記録され得た最初の時点を最もよく表すタイムスタンプ（`start_time_unix_nano`）。
  - （オプション）エグゼンプラーの集合（[Exemplars](#exemplars)を参照）。
  - （オプション）データポイントフラグ（[Data point flags](#data-point-flags)を参照）。

OTLPでは、Gaugeストリーム内のポイントは、ある時間ウィンドウについて最後にサンプリングされたイベントを表します。

![Gauge](https://raw.githubusercontent.com/open-telemetry/opentelemetry-specification/v1.60.0/specification/metrics/img/model-gauge.png)

この例では、Gaugeでサンプリングしている元のタイムシリーズを見て取れます。Event modelでは、あるメトリクスの報告間隔について複数回サンプリング*できます*が、OTLPを通じてメトリクスストリームに報告されるのは最後の値だけです。

Gaugeは集約のセマンティクスを提供しません。代わりに、時間的なアラインメントや解像度の調整のような操作を行う際には「最後にサンプリングされた値」が使われます。

Gaugeは、ヒストグラムや他のメトリクス型へ変換することで集約できます。これらの操作はデフォルトでは行われず、利用者による直接の設定が必要です。

### Histogram

[Histogram](https://github.com/open-telemetry/opentelemetry-proto/blob/v0.9.0/opentelemetry/proto/metrics/v1/metrics.proto#L258)メトリクスデータポイントは、記録された測定値の母集団を圧縮されたフォーマットで伝えます。ヒストグラムは、一連のイベントを、全体のイベント数とすべてのイベントの合計を伴う複数の母集団へ分割してまとめます。

![Delta Histogram](https://raw.githubusercontent.com/open-telemetry/opentelemetry-specification/v1.60.0/specification/metrics/img/model-delta-histogram.png)

Histogramは次のもので構成されます。

- deltaまたはcumulativeの*Aggregation Temporality*。
- データポイントの集合。それぞれ次のものを含みます。
  - 独立した属性名・値の組の集合。
  - Histogramがまとめられた時間ウィンドウ（`(start, end]`）。
    - この時間区間は終了時刻を含みます。
    - 時刻の値は、UNIXエポック（1970年1月1日00:00:00 UTC）からのナノ秒として指定されます。
    - `start`タイムスタンプは、このタイムシリーズについて測定が記録され得た最初の時点を最もよく表します。
  - Histogram内のポイントの母集団全体のカウント（`count`）。
  - Histogram内のすべての値の合計（`sum`）。
  - （オプション）Histogram内のすべての値の最小値（`min`）。
  - （オプション）Histogram内のすべての値の最大値（`max`）。
  - （オプション）次を伴う一連のバケット。
    - 明示的な境界値。これらの値は、バケットの下限・上限と、ある観測値がこのバケットに記録されるかどうかを示します。
    - このバケットに収まった観測値の数のカウント。
  - （オプション）エグゼンプラーの集合（[Exemplars](#exemplars)を参照）。
  - （オプション）データポイントフラグ（[Data point flags](#data-point-flags)を参照）。

Sumと同様に、Histogramも集約temporalityを定義します。上の図はDelta temporalityを示しており、蓄積されたイベント数は報告後にゼロへリセットされ、新しい集約が発生します。一方Cumulativeでは、イベントを集約し続け、新しい開始時刻を使うことでリセットします。

集約temporalityは、min・maxフィールドにも影響します。Cumulativeのminとmaxが表す値はイベントが記録されるほど安定していくため、minとmaxはDelta temporalityでより有用です。加えて、minとmaxはDeltaからCumulativeへ変換することは可能ですが、CumulativeからDeltaへ変換することはできません。CumulativeからDeltaへ変換する際、minとmaxは捨てるか、gaugeのような別の表現で捉えることができます。

バケットカウントはオプションです。バケットを持たないHistogramは、sumとcountのみによって母集団を伝え、`(-Inf, +Inf)`をカバーする単一のバケットを持つヒストグラムとして解釈できます。

#### Histogram: Bucket inclusivity

バケットの上限は（上限が+Infである場合を除いて）含まれ、バケットの下限は含まれません。つまり、バケットは、その下限より大きく、その上限以下である値の数を表します。OpenTelemetryのメトリクスデータを扱うインポーターとエクスポーターは、下限を含み上限を含まないヒストグラムフォーマットとの間で変換する際、この規定を無視することを意図しています。境界の包含・非包含を変更することは、Histogramの最悪ケース誤差の一例です。利用者は、最悪ケースの誤差が自分の許容範囲に収まるようにHistogramの境界を選ぶべきです。

### ExponentialHistogram

**ステータス**: [Stable](../../document-status/)

[ExponentialHistogram](https://github.com/open-telemetry/opentelemetry-proto/blob/cfbf9357c03bf4ac150a3ab3bcbe4cc4ed087362/opentelemetry/proto/metrics/v1/metrics.proto#L222)データポイントは、[Histogram](#histogram)データポイントの代替表現であり、記録された測定値の母集団を圧縮されたフォーマットで伝えるために使われます。ExponentialHistogramは、指数関数式を使ってバケット境界を圧縮するため、同程度のサイズの他の表現と比べて、小さな相対誤差で高いダイナミックレンジのデータを伝えるのに適しています。

集約temporality、属性、タイムスタンプ、および`sum`、`count`、`min`、`max`、`exemplars`フィールドに関して`Histogram`について述べたことは、`ExponentialHistogram`にもそのまま当てはまります。これらのフィールドはいずれも`Histogram`と同一の解釈を共有し、この2つの型の違いはバケット構造だけです。

#### Exponential Scale

ExponentialHistogramの解像度は、`scale`として知られるパラメータによって特徴付けられ、`scale`の値が大きいほど精度が高くなります。ExponentialHistogramのバケット境界は、「成長因子」とも呼ばれる`base`の整数乗の位置にあります。

```
base = 2**(2**(-scale))
```

これらの式の記号`**`はべき乗を表し、`2**x`は「xの2乗」と読み、通常は`math.Pow(2.0, x)`のような式で計算されます。選択したいくつかのscaleに対して計算された`base`の値を以下に示します。

| Scale | Base    | 式  |
| --    | --      | --          |
| 10    | 1.00068 | 2**(1/1024) |
| 9     | 1.00135 | 2**(1/512)  |
| 8     | 1.00271 | 2**(1/256)  |
| 7     | 1.00543 | 2**(1/128)  |
| 6     | 1.01089 | 2**(1/64)   |
| 5     | 1.02190 | 2**(1/32)   |
| 4     | 1.04427 | 2**(1/16)   |
| 3     | 1.09051 | 2**(1/8)    |
| 2     | 1.18921 | 2**(1/4)    |
| 1     | 1.41421 | 2**(1/2)    |
| 0     | 2       | 2**1        |
| -1    | 4       | 2**2        |
| -2    | 16      | 2**4        |
| -3    | 256     | 2**8        |
| -4    | 65536   | 2**16       |

この設計の重要な性質は、「完全な部分集合性」として説明されます。あるscaleを持つ指数ヒストグラムのバケットは、より小さいscaleを持つ指数ヒストグラムのバケットへ正確に対応するため、消費者は誤差を発生させずにヒストグラムの解像度を下げる（すなわちダウンスケールする）ことができます。

#### Exponential Buckets

`index`（符号付き整数）で識別されるExponentialHistogramのバケットは、`base**index`より大きく`base**(index+1)`以下である母集団内の値を表します。

ヒストグラムの正の範囲と負の範囲は別々に表現されます。負の値は、その絶対値を、正の範囲と同じscaleを使って負の範囲へマッピングします。したがって、負の範囲では、ヒストグラムのバケットは下限を含む境界を使う点に注意してください。

ExponentialHistogramデータポイントの各範囲は、バケットの密な表現を使います。ここでは、バケットの範囲は単一の`offset`値（符号付き整数）と、カウント値の配列によって表現され、配列要素`i`はバケットインデックス`offset+i`のバケットカウントを表します。

正・負いずれの範囲についても、次のことが言えます。

- バケットインデックス`0`は、範囲`(1, base]`にある測定値をカウントします
- 正のインデックスは、`base`より大きい絶対値に対応します
- 負のインデックスは、1以下の絶対値に対応します
- 2の連続するべき乗の間には`2**scale`個のバケットがあります。

例えば、`scale=3`の場合、1と2の間には`2**3`個のバケットがあります。`scale=3`のヒストグラムにおけるバケットインデックス4の下限は、`scale=2`のヒストグラムにおけるバケットインデックス2の下限へ、そして`scale=1`のヒストグラムにおけるバケットインデックス1（すなわち`base`）の下限へ対応することに注意してください。これらは完全な部分集合性の例です。

| `scale=3`のバケットインデックス | 下限 | 式                     |
| --                     | --             | --                           |
| 0                      | 1              | 2**(0/8)                     |
| 1                      | 1.09051        | 2**(1/8)                     |
| 2                      | 1.18921        | 2**(2/8), 2**(1/4)           |
| 3                      | 1.29684        | 2**(3/8)                     |
| 4                      | 1.41421        | 2**(4/8), 2**(2/4), 2**(1/2) |
| 5                      | 1.54221        | 2**(5/8)                     |
| 6                      | 1.68179        | 2**(6/8)                     |
| 7                      | 1.83401        | 2**(7/8)                     |

#### Zero Count and Zero Threshold

ExponentialHistogramは、特別な`zero_count`バケットと、オプションの`zero_threshold`フィールドを持ちます。`zero_count`には、絶対値が`zero_threshold`以下である値のカウントが入ります。`zero_threshold`の正確な値は任意であり、scaleとは関係しません。

`zero_threshold`が未設定または`0`の場合、このバケットには、標準の指数関数式で表現できない値と、ゼロに丸められた値が格納されます。

異なる`zero_threshold`を持つヒストグラムは、関係するすべてのヒストグラムの中で最大の`zero_threshold`を採用し、より小さい`zero_threshold`を持つヒストグラムの下位のバケットを、共通のより広いゼロバケットへマージすることで、簡単にマージできます。マージ後の`zero_threshold`が値の入ったバケットの中間にある場合は、そのバケットの上限に一致するように増やす必要があります。

特別な場合には、値の入ったバケットの総数を制限するために、より広いゼロバケットを使うこともできます。

#### Producer Expectations

Producerは、一般的なケースでは、次の理由から不正確なマッピング関数を使ってもよい（MAY）です。

1. 正確なマッピング関数は、実装がかなり複雑になります。
2. 境界値は、すべてのscaleにおいて浮動小数点数として正確に表現できるわけではありません。

一般に、Producerは、すべての入力について正しい結果との期待差が最大1であるマッピング関数を使うべきです（SHOULD）。

ExponentialHistogramの設計により、64ビットの「double」浮動小数点フォーマットで表現するには大きすぎる、あるいは小さすぎる値を表現できます。`scale`の特定の値は、意味を持つとしても、必ずしも有用ではありません。

ExponentialHistogramが表現するデータの範囲によって、どのscaleを有用に適用できるかが決まります。scaleにかかわらず、Producerは、エンコードされたバケットのインデックスが符号付き32ビット整数の範囲に収まることを保証すべきです（SHOULD）。この推奨事項は、OpenTelemetry Collectorのような標準的な処理パイプラインで使われる整数の幅を制限するために適用されています。ワイヤーレベルのプロトコルは、将来のリリースで64ビットのバケットインデックスへ拡張される可能性があります。

Producerは、マッピング関数を使ってバケットインデックスを計算します。Producerは、11ビットの指数部と52ビットの仮数部を持つIEEE倍精度浮動小数点数をサポートしていると仮定されます。値を指数へマッピングする以下の疑似コードは、次の定数を参照します。

```golang
const (
    // SignificandWidth is the size of an IEEE 754 double-precision
    // floating-point significand.
    SignificandWidth = 52
    // ExponentWidth is the size of an IEEE 754 double-precision
    // floating-point exponent.
    ExponentWidth = 11

    // SignificandMask is the mask for the significand of an IEEE 754
    // double-precision floating-point value: 0xFFFFFFFFFFFFF.
    SignificandMask = 1 << SignificandWidth - 1

    // ExponentBias is the exponent bias specified for encoding
    // the IEEE 754 double-precision floating point exponent: 1023.
    ExponentBias = 1 << (ExponentWidth-1) - 1

    // ExponentMask are set to 1 for the bits of an IEEE 754
    // floating point exponent: 0x7FF0000000000000.
    ExponentMask = ((1 << ExponentWidth) - 1) << SignificandWidth
)
```

以下のマッピング関数の選択は、参照実装によって検証されています。

##### Scale Zero: Extract the Exponent

scaleがゼロの場合、値のインデックスは、その正規化された2を底とする指数に等しくなります。すなわち、`1._significand_ * 2**_exponent_`という2進の小数表現における*exponent*の値です。通常のIEEE 754倍精度浮動小数点数のインデックスは`[-1022, +1023]`の範囲にあり、非正規化数のインデックスは`[-1074, -1023]`の範囲にあります。これは次のように書けます。

```golang
// MapToIndexScale0 computes a bucket index at scale 0.
func MapToIndexScale0(value float64) int32 {
    rawBits := math.Float64bits(value)

    // rawExponent is an 11-bit biased representation of the base-2
    // exponent:
    // - value 0 indicates a subnormal representation or a zero value
    // - value 2047 indicates an Inf or NaN value
    // - value [1, 2046] are offset by ExponentBias (1023)
    rawExponent := (int64(rawBits) & ExponentMask) >> SignificandWidth

    // rawFragment represents (significand-1) for normal numbers,
    // where significand is in the range [1, 2).
    rawFragment := rawBits & SignificandMask

    // Check for subnormal values:
    if rawExponent == 0 {
        // Handle subnormal values: rawFragment cannot be zero
        // unless value is zero.  Subnormal values have up to 52 bits
        // set, so for example greatest subnormal power of two, 0x1p-1023, has
        // rawFragment = 0x8000000000000.  Expressed in 64 bits, the value
        // (rawFragment-1) = 0x0007ffffffffffff has 13 leading zeros.
        rawExponent -= int64(bits.LeadingZeros64(rawFragment - 1) - 12)

        // In the example with 0x1p-1023, the preceding expression subtracts
        // (13-12)=1, leaving the rawExponent equal to -1.  The next statement
        // below subtracts `ExponentBias` (1023), leaving `ieeeExponent` equal
        // to -1024, which is the correct upper-inclusive bucket index for
        // the value 0x1p-1023.
    }
    ieeeExponent := int32(rawExponent - ExponentBias)
    // Note that rawFragment and rawExponent cannot both be zero,
    // or else the value is exactly zero, in which case the the ZeroCount
    // bucket is used.
    if rawFragment == 0 {
        // Special case for normal power-of-two values: subtract one.
        return ieeeExponent - 1
    }
    return ieeeExponent
}
```

実装は、非正規化数を最小の正規数へ丸めることが許可されており、これにより組み込み関数を使えることがあります。

```golang
// MapToIndexScale0 computes a bucket index at scale 0.
func MapToIndexScale0(value float64) int {
    // Note: Frexp() rounds submnormal values to the smallest normal
    // value and returns an exponent corresponding to fractions in the
    // range [0.5, 1), whereas an exponent for the range [1, 2), so
    // subtract 1 from the exponent immediately.
    frac, exp := math.Frexp(value)
    exp--

    if frac == 0.5 {
        // Special case for powers of two: they fall into the bucket
        // numbered one less.
        exp--
    }
    return exp
}
```

##### Negative Scale: Extract and Shift the Exponent

負のscaleの場合、値のインデックスは、（上記の`MapToIndexScale0()`による）正規化された2を底とする指数を`-scale`分右シフトしたものに等しくなります。符号拡張のため、このシフトは負のインデックスについて正しい丸めを行うことに注意してください。これは次のように書けます。

```golang
// MapToIndexNegativeScale computes a bucket index for scales <= 0.
func MapToIndexNegativeScale(value float64) int {
    return MapToIndexScale0(value) >> -scale
}
```

逆マッピング関数は次のとおりです。

```golang
// LowerBoundaryNegativeScale computes the lower boundary for index
// with scales <= 0.
func LowerBoundaryNegativeScale(index int) {
    return math.Ldexp(1, index << -scale)
}
```

マッピング関数が非正規化数を正規数へ丸める場合でも、逆マッピング関数は非正規化数を生成することが期待される点に注意してください。最小の正規数を含むバケットの下限が非正規化数になることがあるためです。例えば、scale -4では、最小の正規数`0x1p-1022`は下限`0x1p-1024`を持つバケットに入ります。

##### All Scales: Use the Logarithm Function

上記のscaleがゼロおよび負の場合のマッピング関数・逆マッピング関数は、正確であるため推奨されます。これらのscaleでは、`math.Log()`は不正確になり得るうえ、バケットインデックスを直接計算するよりコストが高くなる可能性があります。この節の手法はすべてのscaleで使ってもよい（MAY）ですが、正のscaleで特に有用です。

組み込みの自然対数関数は、以下のように導出されるスケーリング係数を適用することで、バケットインデックスを計算するために使えます。

1. 指数の底は`base == 2**(2**(-scale))`と定義されます
2. `base**index < value <= base**(index+1)`となる`index`を求めます。
3. 底`base`の対数を適用します。すなわち`log_base(base**index) < log_base(value) <= log_base(base**(index+1))`です（`log_X(Y)`は`Y`の底`X`の対数を表します）
4. `log_X(X**Y) == Y`を使って書き換えます。
5. したがって`index < log_base(value) <= index+1`となります
6. `Ceiling()`関数を使って式を単純化します: `Ceiling(log_base(value)) == index+1`
7. 両辺から1を引きます: `index == Ceiling(log_base(value)) - 1`
8. 自然対数を使えるように`log_X(Y) == log_N(Y) / log_N(X)`を使って書き換えます
9. したがって`index == Ceiling(log(value)/log(base)) - 1`となります
10. スケーリング係数`1/log(base)`は、(1)、(4)、(8)の式を使って導出できます。

スケーリング係数は`2**scale / log(2)`に等しく、定数`math.Log2E`が`1/log(2)`と定義されているため`math.Ldexp(math.Log2E, scale)`と書けます。これらをまとめると次のようになります。

```golang
// MapToIndex for any scale.
func MapToIndex(value float64) int {
    scaleFactor := math.Ldexp(math.Log2E, scale)
    return math.Ceil(math.Log(value) * scaleFactor) - 1
}
```

`math.Log()`を使ってバケットインデックスを計算する場合、2のべき乗付近で正確に正しくなることは保証されません。境界付近の値は、不正確さのために誤ったバケットへマッピングされることがあります。正確なマッピング関数を定義することは、本文書の対象範囲外です。

しかし、入力が2の正確なべき乗である場合、正確に正しいバケットインデックスを計算できます。2の正確なべき乗であることの確認は比較的簡単なため、実装はこうした特殊なケースを適用すべきです（SHOULD）。

```golang
// MapToIndex for any scale, exact for powers of two.
func MapToIndex(value float64) int {
    // Special case for power-of-two values.
    if frac, exp := math.Frexp(value); frac == 0.5 {
        return ((exp - 1) << scale) - 1
    }
    scaleFactor := math.Ldexp(math.Log2E, scale)
    // Note: math.Floor(value) equals math.Ceil(value)-1 when value
    // is not a power of two, which is checked above.
    return math.Floor(math.Log(value) * scaleFactor)
}
```

正のscaleに対する逆マッピング関数は次のとおりです。

```golang
// LowerBoundary computes the bucket boundary for positive scales.
func LowerBoundary(index, scale int) float64 {
    inverseFactor := math.Ldexp(math.Ln2, -scale)
    return math.Exp(index * inverseFactor)
}
```

実装は、マッピング関数と逆マッピング関数が、IEEE浮動小数点数の最小値・最大値付近で正しいことを検証することが期待されます。数学的に正しい式でも、浮動小数点計算の誤差の蓄積や、中間結果のアンダーフロー・オーバーフローのために、誤った結果を生成することがあります。例えばGoの参照実装では、上記の式は最大インデックスのバケットについて`+Inf`を計算します。この場合、インデックスから`1<<scale`を引き、その結果に`2`を掛けるのが適切です。

```golang
func LowerBoundary(index, scale int) float64 {
    // Use this form in case the equation above computes +Inf
    // as the lower boundary of a valid bucket.
    inverseFactor := math.Ldexp(math.Ln2, -scale)
    return 2.0 * math.Exp((index - (1 << scale)) * inverseFactor)
}
```

例えばGoの参照実装では、上記の式は最小インデックスのバケット（非正規化数）の下限を正確には計算しません。この場合、インデックスに`1<<scale`を足し、その結果を`2`で割るのが適切です。

*可読性のため、上記のコード断片からは浮動小数点数と整数型の変換を省略しています。*

#### ExponentialHistogram: Producer Recommendations

64ビットIEEE浮動小数点数の最小・最大付近では、あるバケットの範囲が浮動小数点数フォーマットで部分的にしか表現できないことがあります。こうしたバケットに含まれる数値をマッピングする際、Producerは、そうした部分的にしか表現できないバケットのインデックスを正しく返してよい（MAY）です。これは正常な状態とみなされます。

正のscaleについては、対数を使う手法が好まれます。必要なコードが非常に少なく、検証が容易であり、ルックアップテーブルを使う手法とほぼ同等の速度・正確さを持つからです。scaleがゼロまたは負の場合は、浮動小数点表現から直接インデックスを計算する方が効率的です。

組み込みの対数関数を使うと、任意精度演算やルックアップテーブルを使って計算されるバケットインデックスとは異なる結果になることがありますが、Producerは正確な計算を行うことを要求されません。その結果、ExponentialHistogramのエグゼンプラーが、カウントがゼロのバケットへマッピングされることがあります。こうした値は隣接するバケットでカウントされていると期待されます。

#### ExponentialHistogram: Consumer Recommendations

ExponentialHistogramのバケットインデックスは、上限・下限の両方をIEEE 754倍精度浮動小数点数で表現できるバケットへマッピングされることが期待されます。Consumerは、部分的にしか表現できないバケットインデックスの、表現できない境界を、最も近い表現可能な値へ丸めてもよい（MAY）です。

Consumerは、この表現をオーバーフロー・アンダーフローするような`scale`とバケットインデックスを持つExponentialHistogramデータを拒否すべきです（SHOULD）。こうしたデータを拒否するConsumerは、範囲外のデータを受信したことをエラーログによって利用者に警告すべきです（SHOULD）。

#### ExponentialHistogram: Bucket inclusivity

[明示的境界を持つHistogramデータについて規定されているバケットの包含性](#histogram-bucket-inclusivity)は、ExponentialHistogramデータにも等しく適用されます。

### Summary (Legacy)

[Summary](https://github.com/open-telemetry/opentelemetry-proto/blob/v0.9.0/opentelemetry/proto/metrics/v1/metrics.proto#L268)メトリクスデータポイントは、分位数の要約を伝えます。例えば、HTTPサーバーの99パーセンタイルレイテンシーは何か、といったものです。OpenTelemetryの他のポイント型とは異なり、Summaryポイントは常に意味のある形でマージできるわけではありません。この型は新しいアプリケーションでは推奨されず、他のフォーマットとの互換性のために存在しています。

Summaryは次のもので構成されます。

- データポイントの集合。それぞれ次のものを含みます。
  - 独立した属性名・値の組の集合。
  - 値がサンプリングされた時刻のタイムスタンプ（`time_unix_nano`）
  - （オプション）このタイムシリーズについて測定が記録され得た最初の時点を最もよく表すタイムスタンプ（`start_time_unix_nano`）。
  - データポイントの母集団内の観測数のカウント。
  - 母集団内の値の合計。
  - （厳密な昇順の）分位数の値の集合。それぞれ次のものから構成されます。
    - 区間`[0.0, 1.0]`内の分布の分位数。例えば、値`0.9`は90パーセンタイルを表します。
    - その分位数の値。これは非負でなければなりません（MUST）。

分位数の値0.0と1.0は、それぞれ最小値と最大値に等しいと定義されます。

分位数の値は、`start_time_unix_nano`と`time_unix_nano`の間に観測された値を表す必要はなく、直近の時間ウィンドウ（通常は直近の5〜10分間）に対して計算されることが期待されます。

## Exemplars

**ステータス**: [Stable](../../document-status/)

エグゼンプラーは、OpenTelemetryのコンテキストを、Metric内のあるメトリクスイベントへ関連付ける、記録された値です。1つのユースケースは、利用者がTraceシグナルとMetricsを結び付けられるようにすることです。

エグゼンプラーは次のもので構成されます。

- （オプション）その記録に関連付けられたトレース（`trace_id`、`span_id`）
- 観測時刻（`time_unix_nano`）
- 記録された値（`value`）
- 観測が行われた時点のコンテキストについて追加の知見を提供する、フィルタリングされた属性の集合（`filtered_attributes`）

Histogramの場合、エグゼンプラーが存在するとき、その値は既にヒストグラムポイントが報告する`bucket_counts`、`count`、`sum`に反映されています。

Sumの場合、エグゼンプラーが存在するとき、その値は既に合計全体に含まれています。

Gaugeの場合、エグゼンプラーが存在するとき、その値は同じ発生元のgauge区間内のある時点で観測されたものです。

## Data point flags

**ステータス**: [Stable](../../document-status/)

特別なデータポイントを示すために、特定のフラグを適用できます。フラグは`true`または`false`のいずれかに設定できるブーリアンプロパティです。現在サポートされているフラグは以下のとおりです。

### No recorded value

このフラグがデータポイントに設定されている場合、そのデータポイントは、シリーズ内で明示的にデータが欠落していることを表します。これは、それまで存在していたタイムシリーズが削除されたことを示す指標として機能し、この指標を受け取った後は、このタイムシリーズはクエリで返されるべきではありません（SHOULD NOT）。これは[Prometheusのstalenessマーカー](https://prometheus.io/docs/prometheus/latest/querying/basics/#staleness)に相当します。

このフラグが設定されている場合、属性・タイムスタンプ・時間ウィンドウを除く他のすべてのデータポイントプロパティは無視されるべきです（SHOULD）。

このフラグの既定値は`false`です。

## Single-Writer

**ステータス**: [Stable](../../document-status/)

OTLP内のすべてのメトリクスデータストリームは、論理的な書き手を1つだけ持たなければなりません（MUST）。これは、概念的には、このプロトコルから作成される任意のTimeseriesが、信頼できる発生元を1つだけ持たなければならないことを意味します（MUST）。実際的には、これは次のことを意味します。

- OTel SDKによって生成されるすべてのメトリクスデータストリームは、任意の時点においてグローバルに一意なアイデンティティを持つべきです（SHOULD）。[`Metric`のアイデンティティは上で定義されています。](#opentelemetry-protocol-data-model-producer-recommendations)
- メトリクスストリームの集約は、任意の時点において単一の論理的な発生源からのみ書き込まれなければなりません（MUST）。
  **注: これは、集約されたメトリクスストリームが1つの宛先に到達しなければならないことを意味します**。

システムには、同じメトリクスストリームについて複数の書き手がデータを送信する可能性（重複）があります。例えば、あるSDKの実装が、あるコンポーネントを一意に識別するリソース属性を見つけられない場合、そのコンポーネントのすべてのインスタンスが、同じリソースからのものであるかのようにメトリクスを報告することになりかねません。この場合、メトリクスは一貫しない時間間隔で報告されます。累積和のようなメトリクスでは、これがペアのポイントが累積和をリセットしたように見える問題を引き起こし、利用できないメトリクスになってしまうことがあります。

あるメトリクスストリームに複数の書き手が存在することはエラー状態、あるいは誤動作しているシステムとみなされます。受信者は、単一の書き手が意図されていたと推定し、重複を排除すべきです（SHOULD）。

注: アイデンティティは、ほとんどのメトリクスシステムにおいて重要な概念です。例えば、[Prometheusは一意性について明確に言及しています](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#metric_relabel_configs)。

> Take care with `labeldrop` and `labelkeep` to ensure that metrics
> are still uniquely labeled once the labels are removed.
>
> （`labeldrop`と`labelkeep`を使う際は、ラベルが削除された後もメトリクスが一意にラベル付けされたままであることを確認するよう注意してください。）

OTLPにおいて、Single-Writerの原則は、エラーの状況について推論し、修正措置を取るための手段を提供します。加えて、正しく振る舞うシステムが、望ましくない劣化や可視性の損失なしにメトリクスストリームの操作を実行できることを保証します。

Single-Writerの原則への違反はセマンティックエラーではなく、一般に設定の誤りから生じる点に注意してください。セマンティックエラーはViewの設定によって修正できることがあるのに対し、Single-Writerの原則への違反は、使用する`Resource`を区別するか、ある`Resource`と`Attribute`の集合に対するストリームが時間的に重複しないことを保証することで修正できます。

## Temporality

**ステータス**: [Stable](../../document-status/)

temporalityという概念は、加算可能な量が時間との関係でどのように表現されるか、つまり報告される値が以前の測定値を組み込んでいるかどうかを指します。特にSum、Histogram、ExponentialHistogramのデータポイントは、集約temporalityの選択をサポートします。

すべてのOTLPメトリクスデータポイントは、2つの関連するタイムスタンプを持ちます。1つ目の必須のタイムスタンプは観測に関連付けられたもので、測定が現在のものになった、あるいは有効になった瞬間を表し、`TimeUnixNano`と呼ばれます。2つ目のオプションのタイムスタンプは、一連のポイントが途切れていないことを示し、あるタイムシリーズが測定値の蓄積を開始した時点を示すために使われます。2つ目のタイムスタンプは`StartTimeUnixNano`と呼ばれます。

2つ目のタイムスタンプは、Sum、Histogram、ExponentialHistogramのポイントについて強く推奨されます。再起動を認識した形でOTLPストリームからレートを正しく解釈するために必要だからです。途切れていない一連のポイントの開始を示すために`StartTimeUnixNano`を使うことは、ストリーム内の暗黙的なギャップを符号化するためにも使えることを意味します。

- **Cumulative temporality**は、連続するデータポイントが開始タイムスタンプを繰り返すことを意味します。例えば、開始時刻T<sub>0</sub>から、cumulativeのデータポイントは時間範囲(T<sub>0</sub>, T<sub>1</sub>]、(T<sub>0</sub>, T<sub>2</sub>]、(T<sub>0</sub>, T<sub>3</sub>]などをカバーします。
- **Delta temporality**は、連続するデータポイントが開始タイムスタンプを進めることを意味します。例えば、開始時刻T<sub>0</sub>から、deltaのデータポイントは時間範囲(T<sub>0</sub>, T<sub>1</sub>]、(T<sub>1</sub>, T<sub>2</sub>]、(T<sub>2</sub>, T<sub>3</sub>]などをカバーします。

単調なsumにcumulative temporalityを使うことは一般的であり、Prometheusがその例です。cumulativeかつ単調なsumに基づくシステムは、信頼性を追加するコストの観点で自然に単純になります。収集が断続的に失敗した場合、cumulativeの測定値からはデータのギャップが自然に平均化されます。cumulativeのデータでは、送信者がそれまでのすべての測定値を記憶しておく必要があり、カーディナリティに比例した「先払いの」メモリコストが発生します。

メトリクスのsumにdelta temporalityを使うことも一般的であり、Statsdがその例です。OpenTelemetryのトレーシングとの関連もあります。トレーシングでは、あるSpanイベントは一般に2つのメトリクスイベント（1のカウントと時間の測定値）へ変換されます。Delta temporalityはサンプリングを可能にし、カーディナリティのコストをプロセスの外側へ移すことをサポートします。

## Resets and Gaps

**ステータス**: [Development](../../document-status/)

`StartTimeUnixNano`フィールドが存在する場合、Consumerはストリーム内のギャップと重複する書き手を観測できます。正しく使えば、Consumerは、一過性・継続的なSingle-Writer原則への違反と、リセットイベントの両方を観測できます。途切れていない一連の観測において、`StartTimeUnixNano`は常に、同じシーケンス内の他のポイントの`TimeUnixNano`または`StartTimeUnixNano`のいずれかと一致します。途切れていないシーケンスの最初のポイントについては次のとおりです。

- `StartTimeUnixNano`が`TimeUnixNano`より小さい場合、既知の開始時刻での「真の」リセットとともに、新しい途切れていない観測のシーケンスが始まります。ゼロ値は暗黙的であり、開始点を記録する必要はありません。
- `StartTimeUnixNano`が`TimeUnixNano`と等しい場合、未知の開始時刻でのリセットとともに、新しい途切れていない観測のシーケンスが始まります。最初に観測された値は、途切れていない観測のシーケンスが再開したことを示すために記録されます。これらのポイントは持続時間がゼロであり、以前に報告されたポイントについて何も分かっておらず、データが失われた可能性があることを示します。

途切れていないシーケンス内の後続のポイントについては次のとおりです。

- Delta集約temporalityを持つポイントについては、各ポイントの`StartTimeUnixNano`は直前のポイントの`TimeUnixNano`と一致します
- それ以外の場合、各ポイントの`StartTimeUnixNano`は、最初の観測の`StartTimeUnixNano`と一致します。

メトリクスストリームは、`StartTimeUnixNano`と`TimeUnixNano`フィールドでその範囲をカバーするポイントが存在しない時間範囲があれば、その範囲は暗黙的に未定義であるギャップを持ちます。

### Cumulative streams: handling unknown start time

上述の通り、途切れていない観測のストリームは、持続時間ゼロで非ゼロの値を持つポイントで再開します。cumulative集約temporalityを持つポイントについて、各ポイントがタイムシリーズに与えるレートへの寄与は、ストリーム内の直前のポイントの値に依存します。

途切れていないシーケンスの最初のポイントによるレートへの寄与を正しく計算するには、それが最初のポイントであるかどうかを知る必要があります。開始時刻が未知のリセットポイントは、一連のポイントの`StartTimeUnixNano`と等しい`TimeUnixNano`を持って現れ、この場合、最初のポイントのレートへの寄与はゼロとみなされます。それより前の一連の観測は、観測のギャップより前に同じcumulativeの状態を報告していたと期待されます。

`TimeUnixNano`が`StartTimeUnixNano`と等しいポイントの有無は、シーケンス内の最初のポイントからレートへの寄与をどうカウントするかを示します。未知の開始時刻でのリセットシーケンスの最初のポイントが失われた場合、このデータのConsumerは、2番目のポイントのレートへの寄与を過大にカウントしてしまう可能性があります。それが「真の」リセットのように見えてしまうためです。

過大カウントを避けるためにさまざまな手法を取れます。例えば、あるシステムは、ストリームの以前の部分からの状態を使って、開始時刻の曖昧さを解決できます。

### Cumulative streams: inserting true reset points

cumulativeのカウンターの絶対値はしばしば意味を持つとみなされますが、cumulativeの値がレート関数の計算にのみ使われる場合、最初の未知の開始時刻でのリセットポイントを捨て、代わりに最初に観測された値を記憶しておいて、以降の観測を修正することが可能です。cumulativeのシーケンスの後の部分は、最初の値との相対値として出力されるため、未知の定数だけずれた真のリセットのように見えます。

このプロセスは、真のリセットポイントの挿入として知られており、cumulativeシリーズに対する再集約の特殊なケースです。

## Overlap

**ステータス**: [Development](../../document-status/)

Overlapは、あるメトリクスストリームについて、ある時間ウィンドウ内に複数のメトリクスデータポイントが定義されている場合に発生します。Overlapは通常設定の誤りによって引き起こされ、データの深刻な誤解釈につながる可能性があります。Consumerが重複するポイントを認識し、対応できるようにするため、`StartTimeUnixNano`が推奨されます。

Overlapを扱うための3つの原則を定義します。

- Resolution（ポイントの削除による修正）
- Observability（データがバックエンドへ流れることを許すこと）
- Interpolation（データの操作による修正）

### Overlap resolution

複数のプロセスが同じメトリクスデータストリームへ書き込む場合、OTLPデータポイントが重複して見えることがあります。この状況は通常設定の誤りから生じますが、同一のプロセスが実行されていること（[プロセス属性](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/process.md#process)の欠落のような、オペレーティングシステムやSDKのバグを示唆する状況）から生じることもあります。ポイントが重複している場合、受信者は重複がなくなるようにポイントを取り除くべきです（SHOULD）。重複している場合にどちらのデータを選ぶかは規定されていません。

### Overlap observability

OpenTelemetry Collectorは、データストリーム内で重複するポイントを観測した際にテレメトリーをエクスポートすべきです（SHOULD）。これにより、利用者が誤った設定を監視できるようになります。

### Overlap interpolation

あるプロセスが別のプロセスの終了とほぼ同時に開始する場合、重複するポイントが現れることが予期されます。この場合、OpenTelemetry Collectorは、こうした状況においてギャップの幅をゼロに減らし、重複が生じないように、Sumデータポイントに対して補間を使って切り替わり地点のポイントを修正すべきです（SHOULD）。

## Stream Manipulations

**ステータス**: [Development](../../document-status/)

導入予定。

### Sums: Delta-to-Cumulative

OpenTelemetry（および一部のメトリクスバックエンド）はDeltaとCumulativeの両方のsumの報告を許していますが、対象とするタイムシリーズモデルはdeltaのカウンターをサポートしません。このため、バックエンドがこの仕組みを使えるように、deltaからcumulativeへの変換を定義する必要があります。

> [!NOTE]
> これはDeltaからCumulativeへの唯一のアルゴリズムではありません。OTelのデータモデルに適合する、あり得る実装の1つに過ぎません。

deltaのポイントからcumulativeのポイントへ変換することは、本質的にステートフルな操作です。正しく変換するには、受信するすべてのdeltaのポイントが、現在のカウンター状態を保持し、新しいcumulativeのデータストリームを生成できる1つの宛先へ届く必要があります（[single writerの原則](#single-writer)を参照）。

このアルゴリズムは次のように予定されています。

- あるカウンターについて最初のDeltaポイントを受け取った際、次のことを設定します。
  - cumulativeのsumを保存する新しいカウンターを、最初のカウンターの値に設定します。
  - 最初のポイントの開始時刻に一致する開始時刻。
  - 最初のポイントの時刻に一致する「最後に見た」時刻。
- 以降のDeltaポイントを受け取った際、次のことを行います。
  - 次のポイントが期待される次の時間ウィンドウに一致する場合（[deltaの再起動の検出](#sums-detecting-alignment-issues)を参照）
    - 「最後に見た」時刻を、現在のポイントの時刻に一致するように更新します。
    - 現在の値をcumulativeのカウンターに加算します
    - 元の開始時刻と現在の「最後に見た」時刻・カウントを持つ新しいcumulativeのポイントを出力します。
  - 現在のポイントが開始時刻より前である場合、このポイントを破棄します。注: 遅れて到着するポイントを扱えるアルゴリズムもあります。
  - 次のポイントが期待される次の時間ウィンドウに一致しない場合、現在のポイントが最初のポイントであった場合と同じ手順に従ってカウンターをリセットします。

#### Sums: detecting alignment issues

あるメトリクスストリームについて次に報告されたdeltaのsumが期待される場所と一致しない場合、次のいずれかが発生した可能性があります。

- メトリクスを報告しているプロセスが再起動し、そのメトリクスの報告間隔が新しくなった。
- 複数のプロセスが同じメトリクスストリームを報告しているというSingle-Writer原則への違反。
- データポイントが失われた、または情報が破棄された。

いずれのシナリオでも、cumulativeなメトリクスに対して、何らかのデータが失われたことを最大限伝え、カウンターをリセットします。

アラインメントは次の2つの機構によって検出します。

- 受信したdeltaの時間区間が直前の時間区間と大きく重複している場合、single-writerの原則への違反を想定し、次のいずれかの方法で対処できます。
  - 単純に時間区間の不整合を報告します。このエラー状態は設定の誤りによって引き起こされている可能性があるためです。
  - 受信側で重複を排除・重複除去します。
  - 重複する時間から使われている特定の`Resource`と`Attribute`の集合を区別することで、不整合な時間区間を修正します。
- 受信したdeltaの時間区間が、最後に見た時刻から大きなギャップを持つ場合、何らかの再起動を想定し、cumulativeのカウンターをリセットします。

#### Sums: Missing Timestamps

delta-to-cumulativeアルゴリズムの縮退ケースの1つは、メトリクスデータポイントにタイムスタンプが欠落している場合です。OpenTelemetryが生成するメトリクスを使う場合はこうした状況にはならないはずですが、他のメトリクスフォーマットを適応させる場合（[StatsDのcounts](https://github.com/statsd/statsd/blob/master/docs/metric_types.md#counting)など）には発生することがあります。

このシナリオでは、上記のアルゴリズムは、アラインメントやポイントの重複を判別できないため、データポイントごとにcumulativeのsumをリセットしてしまいます。比較として、すべてのポイントが加算され、失われたポイントは無視される[statsdのsums](https://github.com/statsd/statsd/blob/master/stats.js#L281)で使われている単純なロジックを参照してください。

## References

- [OTEP0049 メトリック`LabelSet`仕様](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.60.0/oteps/metrics/0049-metric-label-set.md)
- [OTEP0113 メトリクスとエグゼンプラーの統合](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.60.0/oteps/metrics/0113-exemplars.md)
- [OTEP0146 メトリクスAPI/SDKプロトタイピングのシナリオ](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.60.0/oteps/metrics/0146-metrics-prototype-scenarios.md)

## Footnotes

\[1\] OTLPは、これらの条件を満たさないデータポイント種別もサポートしています。それらは明確に定義されていますが、標準的なメトリクスデータ変換をサポートしません。

