インフラ

OpenTelemetryのメトリクス実装|計器選定とカーディナリティ設計

業務システム開発

OpenTelemetryでメトリクスを出すとき、アプリに書くのは1行でも、その1行がバックエンドで何本の時系列になるかはSDK側の設定で決まります。計器の種別、CumulativeとDeltaのどちらで送るか、Viewで属性をどこまで削るか。決めないまま計装を広げると、時系列が数万本に膨らんで課金が跳ねるか、Prometheus側で名前が変わってダッシュボードが空になります。7種の計器の選び分け、カーディナリティ制限、Exemplarでの紐付け、Prometheus exporterへの受け渡しを実装の順に扱います。

まとめ|OpenTelemetryメトリクス実装で先に決める計器種別と集約設計

結論から示します。メトリクスは2026年8月時点でC++・.NET・Go・Java・JavaScript・PHP・Pythonの各SDKがStableへ到達しており、ログのように言語の成熟度で採否が割れるシグナルではありません。詰まるのは計器の選び方と、送信先が受け取れる形へ集約を合わせる作業です。

先に決める設定は3つです。第一が計器種別。単調増加ならCounter、増減するならUpDownCounter、分布ならHistogramを選び、値が既にどこかにあるなら非同期計器へ寄せます。第二がTemporalityで、既定はCumulative、Delta指定が効くのはCounter・非同期Counter・Histogramの3種です。第三が属性の設計。SDKは1計器あたり2000データポイントの制限を既定で持ち、超えた分はotel.metric.overflowの1系列へ丸め込まれます。

事故になりやすいのはPrometheusへ渡す段です。exporterは既定で単位の接尾辞と_totalを付け足すため、計器名とPromQLに書いた名前がずれます。移行の前後で同じダッシュボードを使い回すなら、変換後の名前を先に実測してください。

OpenTelemetryの7種の計器と同期・非同期の使い分け・既定集約

計器はデータ型ではなく、値の性質と取得方法の掛け算で選びます。シグナル全体の関係やOTLPの位置づけはOpenTelemetry(OTel)とは|3つのシグナル・OTLP・Collector・Prometheusとの違いで扱っています。

同期計器3種と非同期計器3種+Gaugeの線引きと選定の判断基準

仕様が定める計器は7種類です。同期はCounter・UpDownCounter・Histogram・Gaugeの4つ、非同期(Observable)はCounter・UpDownCounter・Gaugeの3つ。同期はイベントが起きた場所でAddRecordを呼び、非同期は収集のタイミングでSDKがコールバックを呼んで、その時点の値を1回だけ受け取ります。

計器 同期・非同期 単調増加 既定の集約
Counter 同期 あり Sum
UpDownCounter 同期 なし Sum
Histogram 同期 なし 明示バケット
Gauge 同期 なし LastValue
非同期Counter 非同期 あり Sum
非同期UpDownCounter 非同期 なし Sum
非同期Gauge 非同期 なし LastValue

選び方は単純です。処理の中で数え上げるならCounter、増減する値ならUpDownCounter、分布を見たいならHistogram。OSやライブラリの側にすでに値があって読むだけなら非同期計器に寄せます。取り違えると集計が壊れます。秒間数千回の書き込みを同期Gaugeで受けると、収集間隔の中で最後に書いた1点しか残りません。

計器ごとの既定集約とHistogramのAdviceでのバケット境界宣言

計器を選んだ時点で既定の集約が決まります。Counter・UpDownCounter・非同期Counter・非同期UpDownCounterはSum、Gaugeと非同期GaugeはLastValue、HistogramはExplicit Bucket Histogramです。この既定を変えたいときだけViewを書く、と考えると設定が最小限で済みます。

Histogramで実装者が最初に触るのはバケット境界です。仕様はAdvisory ParameterとしてExplicitBucketBoundariesを定めており、計器を作る側が推奨境界を宣言できます。HTTPのレイテンシ用の境界を、ジョブの実行時間を秒単位で測る計器へそのまま当てると、ほぼ全件が最上位バケットへ落ちてp95が意味を失います。境界の宣言を計器側へ寄せておけば、収集側の設定を触る必要はありません。

決めきれない場合の逃げ道が指数ヒストグラムです。OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATIONbase2_exponential_bucket_histogramを指定すると値域に応じてバケットが伸縮しますが、Delta指定と組み合わせると後述のとおり破棄されます。

CumulativeとDeltaの選び分けと送信先バックエンドからの逆算

Temporalityは後回しにされやすく、そして後から一番効いてくる設定です。

既定はCumulative・Temporality設定を環境変数で切り替える手順

集約時間性(Aggregation Temporality)は、送り出す値が累計か前回収集からの差分かを決めます。仕様の既定はCumulativeで、MetricReaderへ何も指定しなければ全計器が累計で出る挙動です。累計はプロセス開始時からの合計を毎回送るため、収集が1回落ちても次の1回で値が復旧します。

切り替えは環境変数1つで済みます。OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCEの既定値はcumulativeで、ほかにdeltalowmemoryを取ります。deltaを指定してDeltaへ切り替わるのはCounter・非同期Counter・Histogramの3種で、UpDownCounter系は累計のままです。増減する値に差分という概念を当てても意味を成さないため、ここは指定しても変わりません。

中間の選択肢がlowmemoryです。Deltaになるのは同期のCounterとHistogramだけで、属性の組み合わせごとの合計値をプロセスが持ち続ける負担を、状態を捨てられる計器に限って軽くします。外部の値を読むだけの非同期計器はもともと状態を持たないため、対象から外れています。送信間隔も併せて確認してください。OTEL_METRIC_EXPORT_INTERVALの既定は60000ミリ秒、つまり1分間隔で、Deltaではこの値がそのまま1区間の長さになります。

Deltaを選ぶと再起動で欠測が出る条件とCollectorでの変換

Deltaを選ぶ前に確認すべきは送信先の対応です。差分を前提とするバックエンドではDeltaが素直で、累計を前提とするPrometheusでは扱いが変わります。exporterは明示バケットのHistogramならDeltaを累計へ戻せますが、指数ヒストグラムのDeltaは変換できず破棄します。

欠測の性質も違います。累計は1回の送信が落ちても次回の値で埋め合わせが効く一方、Deltaは落ちた区間の増分がそのまま消えます。不安定な経路へ直接Deltaで送る構成は避けてください。両立させたいなら、アプリは累計で出し、Collectorのcumulativetodeltaプロセッサで変換します。変換をCollector側へ寄せる考え方は、OpenTelemetryのログ実装|直接送信とfilelog receiverの選び分けで扱った送出経路の判断と同じ筋です。

Viewでの集約変更とカーディナリティ制限2000・overflow属性

カーディナリティは計装を書いた後ではなく、Viewを書く段で決めます。Kubernetes上ではk8sattributes processorが付ける属性も系列数に効くため、OpenTelemetryのKubernetes構成|Operator導入とCollector二層設計もあわせて確認してください。

Viewの計器選択条件と属性のallow-listで次元を削る書き方

Viewは「どの計器に対して」「どう出力を変えるか」の組で書きます。選択条件に使えるのは計器名・計器種別・単位・Meter名・Meterのバージョン・スキーマURLの6つで、名前にはワイルドカードが使えます。

出力側で指定できるのは、メトリクス名、説明、残す属性キー、集約関数、Exemplarのリザーバ、収集サイクルあたりのデータポイント上限の6つ。実務で効くのは属性キーで、これはallow-listとして働きます。列挙したキー以外を落として同じ時系列へ畳み込む挙動です。

リクエスト時間を測る計器へ、ルート・メソッド・ステータスコードだけを残すViewを当てておけば、ユーザーIDが紛れ込んでも時系列は増えません。属性を落とす場所をコードではなくViewに置けば、計装を書き直さずに次元を絞れます。どのラベルなら安全かという設計論はメトリクス監視とは|4種のメトリック型・収集方式・アラート閾値の設計で扱っています。

既定2000のカーディナリティ制限とotel.metric.overflowの読み方

Viewを書き忘れても、SDKには最後の防波堤があります。仕様は集約のカーディナリティ制限に既定値2000を定めており、1つの計器が1回の収集サイクルで持てるデータポイントが2000点までという意味です。

上限を超えた測定値は、単純に捨てられるわけではありません。otel.metric.overflowという属性にtrueが入った1本の時系列へまとめられ、そこに合算されます。この属性が付いた系列をバックエンドで見つけたら、その計器の属性設計が破綻している合図。1本にアラートを張っておくと計装の事故に早く気づけます。

制限値そのものはViewのaggregation_cardinality_limitで計器ごとに変えられます。ただし引き上げは対症療法です。2000点を超えるのは属性に個体識別子が混ざっている場合がほとんどで、上限を1万へ上げればメモリと課金が増えます。削る順序は、個体識別子、URLのクエリ文字列のような自由入力、他の属性から復元できる冗長な属性の順。使っていない計器はViewの集約にDropを指定すれば丸ごと止められます。

Exemplarでメトリクスからトレースへ飛ぶ実装と検証の手順

Exemplarは、集計された数値から個別のトレースへ戻るための小さな橋です。

既定のtrace_basedフィルタとサンプリング率で失う紐付けの量

Exemplarは、メトリクスのデータポイントへ、その値に寄与した測定の一例としてTraceIdとSpanIdを添える仕組みです。p99が悪化したときに、その遅い1件のトレースへ直接飛べます。数値からトレースへ降りる導線を、バックエンド側の推測ではなくデータ自体が持ちます。

既定のフィルタはtrace_basedで、環境変数OTEL_METRICS_EXEMPLAR_FILTERの既定値も同じです。サンプリングされた親スパン内で記録された測定だけが候補になる、という条件が付きます。ここが見落としの起点。トレースのサンプリング率を1%に絞っていれば、Exemplarの候補も同じ割合まで減ります。率の決め方はトレースサンプリングとは?ヘッドベースとテールベースの仕組み・OpenTelemetry実装で扱っています。

Exemplarを載せる条件とenable_open_metricsの既定false

Collectorのprometheus exporterはenable_open_metricsの既定値がfalseで、Exemplarが載るのはOpenMetrics形式のときだけです。対象もヒストグラムと単調増加のSumに限られ、ネイティブヒストグラムでは出ません。エンドポイントを直接叩き、値の行の末尾にトレース識別子が並ぶかを見れば、SDKより後ろで落ちているかを切り分けられます。

Prometheus exporterへの受け渡しと命名変換で崩れる互換性

OTLPで出した名前は、Prometheusへ渡る途中で必ず書き換わります。

_totalと単位サフィックスの自動付与でダッシュボードが壊れる

計器名は仕様上、最大255文字・先頭は英字で、ドットによる名前空間の区切りが作法です。単位はUCUMの記法で書き、秒ならs、バイトならByを指定します。この2つがPrometheusへ渡る時点で書き換わる点が起点。単調増加のSumは名前が_totalで終わっていなければ既定で_totalが付き、単位の接尾辞は型固有の接尾辞の前に挿入されます。Collectorのprometheus exporterではadd_metric_suffixesの既定値がtrueで、付与が有効な状態が出発点です。

移行で壊れるのはこの一点。ドットはアンダースコアへ置換され、連続分は1つに詰められます。OTel側でhttp.server.request.durationと名付けた指標は、Prometheus側ではhttp_server_request_duration_secondsのような名前で見えます。従来の名前を参照している既存のアラートルールは、切り替えた瞬間に沈黙する可能性が高い。並走期間中に/metricsを取得し、変換後の名前を突き合わせてください。

target_infoでResource属性を渡す設計と5分で失効する既定

Resource属性はラベルとしてそのまま付きません。仕様は、リソースが空でなければtarget_infoという情報メトリクスへ変換し、リソース属性をそのラベルとして載せると定めています。サービス名で絞るならPromQLでこれを結合するか、resource_to_telemetry_conversionで全メトリクスのラベルへ展開します。後者はリソース属性の数だけラベルが増えるため、前章のカーディナリティ制限と地続きの判断です。

metric_expirationの既定が5分である点も押さえてください。この時間だけ更新がない指標は公開対象から消えるため、日次バッチのように出現間隔の長い指標はスクレイプ時点で存在せず、グラフが歯抜けになります。

既存Prometheus資産があるときのOTel Metrics採用条件と見送る場面

ここからは判断です。Metricsが多くの言語でStableでも、既存のPrometheus構成をOTelへ寄せるのが得とは限りません。

Metricsが多言語でStableでも移行を急がなくてよい理由

2026年8月15日時点の言語別ステータスでは、メトリクスは主要言語で出揃っています。

言語 Metrics Traces Logs
C++・.NET・Java・PHP Stable Stable Stable
Go Stable Stable Beta
Python Stable Stable Development
JavaScript Stable Stable Development
Rust Beta Beta Beta
Ruby Development Stable Development
Swift Development Stable Development

ログとの対比が分かりやすい。言語の成熟度が採否を決めるログと違い、メトリクスで同じ制約に当たるのはRuby・Swift・Erlang系と、全体がBetaのRustに限られます。多くの現場での判断軸は「入れられるか」ではなく「入れて何が変わるか」に移ります。

急がなくてよい理由は、既存のPrometheus計装が壊れていないことにあります。Micrometerで出す指標が動き、ダッシュボードもアラートも回っているなら、載せ替えても取れる数字は同じです。名前だけが変わります。費用は変換後の名前への追従とパイプラインの運用、効果は送信先を差し替えられる点に集約されます。ここが要らないなら投資は回収できません。

OTel Metricsへ寄せる採用条件3つと踏み切ってよい構成

言い切ります。次の3条件のうち2つ以上に当てはまるなら、OTel Metricsへ寄せる判断が立ちます。

  1. トレースを既にOTelで出しており、メトリクスだけ別系統で管理が二重になっている
  2. 監視SaaSの乗り換えか併用を具体的に検討していて、送信先の差し替えをアプリ改修なしで行いたい
  3. 言語やフレームワークが混在し、計装の作法とResource属性を1つに揃えたい

最も効くのは1つ目です。トレースとメトリクスを同じSDK・同じResource属性で出すとservice.nameが一致し、Exemplarでの相互参照が成立します。自動計装が成熟したJavaなら、追加で書くコードはほとんど要りません。効果は二重管理の突き合わせ作業が消えたかどうかで測ってください。

Prometheus receiverで受けるだけに留めるべき3つの条件

逆に、次のどれかに当てはまるならSDKの載せ替えは見送り、CollectorのPrometheus receiverで既存のエンドポイントを吸うだけに留めてください。

  1. アプリの改修に稟議が要る、あるいはソースを触れない既製品が監視対象の中心である
  2. Prometheus+Grafanaで完結しており、送信先を差し替える予定が当面ない
  3. ダッシュボードとアラートルールの本数が多く、名前変更への追従コストが移行の効果を上回る

receiverで受ける構成なら、アプリは1行も変わりません。既存のexporterのエンドポイントをCollectorがスクレイプし、OTLPで先へ流すだけです。これでも送信先差し替えの効果は半分手に入ります。手に入らないのはExemplarと、セマンティック規約に沿った属性の統一。この2つが要るかどうかが分岐点です。

移行の判断より重いのは、寄せた後の運用です。Collectorのパイプライン、View設定の保守、版上げの追随が、そのまま自社の運用項目へ移ります。見る画面が増えたのに見る人がいない状態が最も損で、体制を用意できないなら移行時期を後ろへ倒してください。外部に持たせる前提なら、システム保守運用・内製化支援のように監視設計から運用体制づくりまで引き受ける委託先を、計画の段階から入れる判断になります。

OpenTelemetryのメトリクス計装と集約に関するよくある質問

実装の判断が止まりやすい論点を、5つに絞って回答します。

OpenTelemetryのメトリクスとPrometheusのメトリクスは何が違いますか?

データモデルと既定の集約時間性が違います。OpenTelemetryは計器を7種に分け、CumulativeとDeltaの両方を仕様として持ちます。Prometheusは累計のカウンターとゲージが中心で、収集はプル型が前提です。相互運用は仕様で定義されていますが、変換時に_totalや単位のサフィックスが付き、同じ指標でも名前が変わります。ここが実務上の最大の差です。

CumulativeとDeltaはどちらを選べばよいですか?

送信先から逆算してください。累計前提のバックエンドへ送るなら既定のCumulativeのままが素直で、差分を前提とするSaaSへ送るならDeltaを指定します。メモリだけを削りたいならlowmemoryで同期のCounterとHistogramのみDeltaにできます。迷う場合は累計で出し、変換をCollectorのcumulativetodeltaへ寄せると切り戻しが容易です。

カーディナリティ制限の2000は変更すべきですか?

まず変更せず、超えた原因を先に潰す進め方を推奨します。超えた分はotel.metric.overflowtrueの1系列へ合算される仕組みで、この系列が出た時点で属性に個体識別子が混ざっている可能性が高いと判断できます。Viewのallow-listで属性を絞り、それでも足りない計器だけaggregation_cardinality_limitを個別に引き上げる順序が安全です。

GaugeとAsynchronous Gaugeはどう使い分けますか?

値を誰が持っているかで分かれます。アプリの処理の中で値が変わり、その場で記録できるなら同期のGaugeです。OSの空きメモリのように読みに行けば取れる値なら、非同期Gaugeのコールバックへ寄せます。同期Gaugeは収集間隔の中で最後に記録した値だけが残るため、高頻度の書き込みには向きません。

Exemplarが表示されないときは何を確認すべきですか?

確認は3段階です。第一に、計器の呼び出しがサンプリングされたスパンのコンテキスト内にあるか。既定のフィルタはtrace_basedのため、スパンの外の測定は候補になりません。第二に、トレースのサンプリング率が低すぎないか。第三に、Prometheus経由ならenable_open_metricsが有効か。既定のfalseではOpenMetrics形式にならずExemplarが載りません。

関連記事

資料請求

RELATED POSTS 関連記事