はじめに
最近 OpenTelemetry を使ったシステム監視機能を構築する機会がありました。
ほぼ何も知らない状態からの構築で、いろいろ調べながら推進したのですが、その際に得られた知見をまとめてみました。
OpenTelemetryとは
OpenTelemetry(OTel)とは、Googleが主導して作成したOpenCensusと、CNCF(Cloud Native Computing Foundation コンテナ技術の推進を目的とした組織)が主導していたOpenTracingを統一し、テレメトリ仕様の業界標準としたもの。
(テレメトリ:サーバーの稼働状況(メトリクス)やエラーログを収集し、システムの安定稼働や障害の早期解決(オブザーバビリティ)に利用されます)
テレメトリの三本柱と呼ばれているログ、トレース、メトリクスに加え、バゲッジ、プロファイルというデータの標準仕様をOTLP(OpenTelemetry Protocol)というプロトコルとして定めた。
ログ、トレース、メトリクスはシグナルとも呼ばれる。システムやアプリケーションの動作状況やパフォーマンスを把握するために外部へ出力されるデータの種類のこと。
プロファイルもシグナルだが、現在(2026/7/13)開発中のステータス。
バゲッジは補助的なデータを伝播させる仕組み。
各データ分類の概要と役割は以下の通り。
これらの概念自体はOpenTelemetry登場前から存在していたが、ベンダー(DatadogやPrometheus, Jeagerなど)によって仕様が異なっていた。
それらをベンダーに依存しない統一フォーマットとして提供し、データの生成から転送までの方式を標準化した。
さらに、これらのシグナルをトレースIDというユニークなIDで、コンテキスト(Goなら context.Context )を介して紐づけることで、障害などが発生した際のログ、トレース、メトリクスなどの紐付けが容易にできるようにした。
これまでは、例えば障害があった時に各シグナルを結びつけるためには、おおよその時間から推測するというのが主な調査方法だったが、コンテキストで結びつけることで、明確な紐付けをすることができるようになった。
(これまでも一部繋がってはいるが、それはベンダーの仕様に依存している。OTelはそれを全面的に標準化した。)
ここまでがOpenTelemetryの概要です。
OpenTelemetryの全体構成
OpenTelemetryの構成を俯瞰して見ると以下の図のようになっている。

それぞれの役割は以下の通り。
具体的な構成例があったほうがよりピンと来る気がするので以下構成例
コレクター
コレクターとの通信について整理します。
ストレージとバックエンドはDatadogのことだと思えば大体わかると思われる。
コレクターはOpenTelemetry固有の概念なのでここを深掘る。
コレクターは、先にも書いた通り、アプリケーションから送られてきたシグナルを受け取り、加工して、次の場所へ送り出す中継地点(プロキシ)です。
アプリケーションからコレクタへデータを送信する際の通信プロトコルはgRPC / HTTPが使用できます。
送信するデータフォーマットはOTLP (OpenTelemetry Protocol) というプロトコルに準拠しています。
OTLP はアプリケーションからコレクター、またはコレクターからバックエンドへテレメトリデータを送信・転送するためのプロトコル/データ構造を定めたものです。
また、サービス間(HTTPヘッダーやgRPCメタデータ)でトレースの文脈( traceparent , tracestate )を伝播するための標準仕様として Trace Context が定義されています。
これは W3C (Web技術の標準化を進める国際組織)の仕様として定義されているものです。
今後このフォーマットが当たり前になっていくものと思われます。
アプリケーションからコレクターへの通信は基本的にはgRPCを使い、gRPCが使用できない環境下ではHTTPを使用する。
通信手段と使用するポート番号は以下の通り。
| プロトコル | ポート | 用途・特徴 |
|---|---|---|
| OTLP/gRPC | 4317 | ほとんどの言語用SDKで標準的に使用される高速なバイナリ通信 |
| OTLP/HTTP | 4318 |
gRPCに未対応の環境向け (Webブラウザなどのフロントエンドやスマホアプリなど) |
コレクターへの通信手段の話はこれぐらいで、次はインフラ構成の話
k8s環境下でOpenTelemetryコレクターはどこに置くのか
その前にKubernetesの構成について一瞬おさらい
k8sは、Clusterの中にNodeがあってNodeの中にPodがいくつかあってPodの中に1つ以上のコンテナが入ってるという構成になっている。
スケールイン、スケールアウトはPod単位(hpaのminReplicas/maxReplicas)。

図はリンク先のもの。
で、k8s環境下でOpenTelemetryコレクターはどこに置くの
k8s環境でOpenTelemetry Collectorを構成する際の主要パターンは以下の通り。
| パターン | 構成 |
|---|---|
| サイドカー構成 |
アプリと同一Podに常駐させる構成。 アプリとコレクターが同一Podのためlocalhostでデータを送信。 アプリごとの独立性が高いが、ポッド数に比例してメモリ/CPU消費が増える。 ![]() |
| デーモンセット構成 (ノードエージェントモデル) |
ノードに1つだけ配置し、ノード上で稼働する複数のアプリPodが共同で利用する構成。 同一ノード上のコレクターへNode IP経由で送信。 ノードあたりのリソース効率が良いが、マルチテナントでの分離や設定変更の柔軟性に制限あり。 ![]() |
| ゲートウェイ構成 |
コレクター専用のDeploymentを構成し、負荷状況に応じてオートスケール。 データ送信元はデーモンセット/サイドカー混合可。 ClusterIP Service経由でGateway Collectorへ集約。 重い処理(Tail-based Sampling や集約処理)を担当させるのに向いている。 ![]() |
デーモンセット構成の場合、アプリケーションPodからコレクターPodへの通信はNode IP経由で行うので、NodeのIPアドレスを取得する必要がある。
k8sであれば deployment.yml に以下のような設定をすることでNode IPを環境変数に設定することができる。
k8s設定 env:
- name: DD_AGENT_HOST
valueFrom:
fieldRef:
fieldPath: status.hostIP
status.hostIPでKubernetesの機能を使って、このアプリPodが今乗っているワーカーノードの物理IPアドレスを環境変数DD_AGENT_HOSTに設定している。
アプリケーションでは DD_AGENT_HOST:4317 をデータの送信先とすることで、Datadog Agentにデータを送信し、これを受けたDatadog AgentがDatadog本体にデータを転送する。
コレクターの話はこれぐらいにして次は実装の話
アプリケーションの話(Goの実装の話)
各シグナルをアプリケーションで計測する際の実装方法の話。
以下にトレースとメトリクスについて記載する。
OTel宛のログの実装についてはよくわかっていない。
TraceとSpan
トレースは3本柱の一つ。セットで覚えるべき概念としてスパン。
トレースは、リクエストが分散システム(マイクロサービスなど)を通過する際の一連の流れ、経路を記録するもの。
ユーザーの1回の操作(リクエスト)から始まる処理は、どれだけ多くのサーバーやマイクロサービスをまたいでも、すべて同じ「1つのTrace ID」を共有します。
スパンは、システム内で行われた「単一の処理」の記録。
例えば、「認証APIを呼び出す」「データベースからユーザー情報を取得する」「関数Aを実行する」といった個々の作業が1つのスパンになります。
Datadog APMの画面で言うとこんな感じ。
トレースはスパンの集合とも言える。
どう実装するのか
呼び出し元から受け取ったコンテキストを元にSpanを生成すると、それは同一トレース内のスパンという扱いになる。
サンプルコードを以下に示す。
ctx, span := tracer.Start(ctx, "xxx")でSpan生成。
以下のコードがDatadog APMのスパン1行分にあたる。
サンプルコード(Go) import "go.opentelemetry.io/otel"
var tracer = otel.Tracer("yyy")
func (a *aAdapter) GetAaa(ctx context.Context) error {
ctx, span := tracer.Start(ctx, "xxx")
defer span.End()
...
}
他にも以下のようなコードのパターンもある。
(コンテキストの取り出し方が違うだけではある)
usecase層では、paramsに格納されたコンテキストをparams.HTTPRequest.Context()で取り出し(nilなら新規生成)、それを元にSpanを生成。
ctx, span := tracer.Start(ctx, "xxx")の部分。
サンプルコード(Go) import "go.opentelemetry.io/otel"
var tracer = otel.Tracer("yyy")
func (s *service) GetAaa(params aaa.PostXxxParams) aaa {
ctx := params.HTTPRequest.Context()
ctx, span := tracer.Start(ctx, "xxx")
defer span.End()
...
}
このような形で、受け取ったコンテキストを元にスパンを生成していく。
引数で受け取ったコンテキストを無視して、新規にコンテキストを生成してSpanを開始したら、それは別のトレースになるし、そこがルートスパンになると言うこと。
プロパゲーター
プロパゲーターは、分散トレーシング用のデータ伝播形式。
OpenTelemetryはTraceContextやBaggageというW3Cの標準フォーマットに準拠しており、HTTP/gRPCのヘッダーなどを介してコンテキストを受け渡すことができる。
追加でb3などのベンダー固有のフォーマットにも、上記と合わせて複合的に対応できるようになっている。
OpenTelemetryライブラリのNewCompositeTextMapPropagatorを使って、複数の分散トレース用伝播形式(Propagator)を1つにまとめ、異なる規格(W3C TraceContextやBaggage、b3)を同時に扱えるように合成することもできる。
TextMapPropagator の概要
- 役割: 渡された複数の
TextMapPropagatorを統合し、1つの統一されたPropagatorとして機能させる。 - 用途: 異なるシステムやベンダー(Datadog、GCP Cloud Trace、標準のW3Cなど)が混在する環境で、トレース情報を正しく引き継ぐために使用する。
実装イメージ package main
import (
"go.opentelemetry.io/otel"
otelpropagation "go.opentelemetry.io/otel/propagation"
)
// b3形式で伝播されたトレース情報をコンテキストに読み込むためのPropagator
type propagationStruct struct {
b3 otelpropagation.TextMapPropagator
}
// b3形式で伝播されたトレース情報をコンテキストに読み込む
func (p *propagationStruct) Extract(ctx context.Context, carrier otelpropagation.TextMapCarrier) context.Context {
return p.b3.Extract(ctx, carrier)
}
// b3形式のトレース情報をキャリア(HTTPヘッダー等)へ書き込む
func (p *propagationStruct) Inject(ctx context.Context, carrier otelpropagation.TextMapCarrier) {
p.b3.Inject(ctx, carrier)
}
// b3で利用するヘッダーキー一覧を返す
func (p *propagationStruct) Fields() []string {
return p.b3.Fields()
}
// propagatorの設定を返す。OTelのHTTPサーバー/クライアントのミドルウェアで使用
func newPropagation() otelpropagation.TextMapPropagator {
// OTelの標準伝播であるTraceContext/Baggageを優先しつつ、
// 既存(OpenCensus)との互換性維持のためb3も受け付ける。
return otelpropagation.NewCompositeTextMapPropagator(
otelpropagation.TraceContext{},
otelpropagation.Baggage{},
&propagationStruct{b3: b3.New()},
)
}
メトリクス
ランタイムのメトリクス自動収集
OpenTelemetryでは、アプリケーションの初期化時に go.opentelemetry.io/contrib/instrumentation/runtime パッケージを使用して、ランタイムメトリクスの収集を開始することができる。
GoのGCの状況とかgoroutineの数とかを自動収集してくれる。
初期化コードの例 import (
"go.opentelemetry.io/contrib/instrumentation/runtime"
"go.opentelemetry.io/otel"
)
func initRuntimeMetrics() {
// runtime.Start() を呼ぶだけで、バックグラウンドで
// heap_alloc_bytes を含むランタイム統計の収集が始まります。
err := runtime.Start(
runtime.WithMinimumReadMemStatsInterval(time.Second * 10), // 収集間隔の設定(任意)
)
if err != nil {
log.Fatalf("failed to start runtime metrics: %v", err)
}
}
上記は収集間隔を10秒に設定しているが、省略した場合は15秒。
(リンク先のコードで15秒が設定されている)
単にGoのランタイム統計を取りたい場合は、この runtime.Start() への移行が最も推奨される方法。
特定のメトリクスだけ取りたい場合や、上記で取得できないメトリクスを収集したい場合は、メトリクス名を指定して収集する。
メトリクス名はこちら:
以下サンプルコード。
サンプルコード // 1. Goアプリのアロケートヒープメモリ量
// OTel標準名: process.runtime.go.mem.heap_alloc
goMemstatsHeapAllocBytes, err = meter.Float64ObservableGauge(
"process.runtime.go.mem.heap_alloc",
metric.WithDescription("Go application allocated heap memory"),
metric.WithUnit("by"), // OTelのバイト単位の標準表記は小文字の "by" です
)
if err != nil {
return err
}
// 2. GoアプリのGC所要時間
// OTel標準名: process.runtime.go.gc.duration
// ※ 累積時間(Sum)として扱う場合は、Gaugeではなく「Counter (Sum)」として定義するのがOTelの標準的なアプローチですが、
// 既存の非同期(Observable)な値をそのまま移送する場合は ObservableCounter または ObservableGauge を使用します。
// ここでは単純な名前と単位の置換を行っています。
goGcDurationSecondsSum, err = meter.Float64ObservableGauge(
"process.runtime.go.gc.duration",
metric.WithDescription("Go application total duration time of GC"),
metric.WithUnit("s"),
)
if err != nil {
return err
}
// 3. Goアプリのgoroutine数
// OTel標準名: process.runtime.go.goroutines
goGoroutines, err = meter.Int64ObservableGauge(
"process.runtime.go.goroutines",
metric.WithDescription("Go number of goroutines"),
)
if err != nil {
return err
} カスタムメトリクス
収集するメトリクスを独自実装してデータ収集することもできる。
メトリクスにもいくつか種類があるのでその話。
公式はリンク先参照。
メトリクスの計装の種類は以下の通り。
テンポラリティ
OTelにはテンポラリティ(時間性)というものがあります。
カウンターやヒストグラムなどのメトリクスが、時間経過に対してどのように集計・報告されるかを定義する概念です。
cumulative(累積)とdelta(差分)の2つがあり、Counter, Histogram が影響を受けます。
- cumulative: アプリケーション起動時からの合計値を送信し続ける方式。途中でメトリクスがリセットされないため、長期的な傾向分析時に使用。
- delta: 測定期間内に変化した値のみを送信する方式。データ送信毎に状態がリセットされる。
OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCEという環境変数の値を cumulativeまたはdeltaに設定することで処理方式を切り替えることができる。
OpenTelemetry のデフォルトは cumulative です。
注意点として、Prometheus や Datadog など、受ける側の監視ツールによって推奨・対応しているテンポラリティが異なります。
具体的には、Prometheus は Cumulative 選好、Datadog 等のメトリクスAPIは Delta が扱いやすいケースがあるようです。
システムの要件や監視ツールの仕様に応じて、適切なテンポラリティを選択することが重要です。
参考:OpenTelemetry テンポラリティとカーディナリティ制限
参考
書籍
- 入門 OpenTelemetry ―現代的なオブザーバビリティシステムの構築と運用
- 実践 OpenTelemetry ―オープンなオブザーバビリティ標準を組織に導入する
- OpenTelemetryではじめるテレメトリーサンプリング ――シグナルデータ膨張に対応しながらオブザーバビリティを確保する
サイト
- OpenTelemetry
- OpenTelemetry Documentation
- OpenTelemetry Metrics Exporter - OTLP
- OpenTelemetry OTLPエクスポーター設定
- OpenTelemetry テンポラリティとカーディナリティ制限
- OpenTelemetry メトリクス
- Datadog OpenTelemetry ランタイムメトリクス
- Datadog OTLP メトリクスタイプ
- Datadog Metrics Data Model
- Semantic conventions for Go runtime metrics
- CNCF
- W3C Trace Context
- Rworks Kubernetesの重要な構成要素である「Node」について解説します!
- Google が構築した OpenTelemetry Collector
- GKE 用マネージド OpenTelemetry
- GKEでマネージドな OpenTelemetry Collector が登場
- UCUM Specification



