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

それぞれの役割は以下の通り。
| 機能 | 役割 |
|---|---|
| コレクター |
アプリケーションから送られてきたシグナルを受け取り、加工して、次の場所へ送り出す中継地点(プロキシ)。 コレクターの役割は大きく以下3つ
|
| ストレージ | 送信されたテレメトリーデータを蓄積する場所 |
| バックエンド | エンドユーザー(我々)に対してUIを通してデータを見えるようにする場所 |
コレクターには、公式が提供している OpenTelemetry Collector の他、各ベンダーが提供する Agent を利用する構成もあります。
具体的な構成例があったほうがイメージしやすいため、以下に構成例を示します。
なお、私が担当したシステムでは Datadog を利用していたため、Datadog Agent を利用した構成例も記載しています。
コレクター
コレクターは先にも書いた通り、アプリケーションから送られてきたシグナルを受け取り、加工して、次の場所へ送り出す中継地点(プロキシ)です。
アプリケーションからコレクタへデータを送信する際の通信プロトコルはgRPC / HTTPが使用できます。
送信するデータフォーマットは OTLP (OpenTelemetry Protocol) というプロトコルに準拠しています。
OTLP はアプリケーションからコレクター、またはコレクターからバックエンドへテレメトリデータを送信・転送するためのプロトコル/データ構造を定めたものです。
また、サービス間(HTTPヘッダーやgRPCメタデータ)でトレースの文脈( traceparent , tracestate )を伝播するための標準仕様として Trace Context が定義されています。
これは W3C (Web技術の標準化を進める国際組織)の仕様として定義されているものです。
今後このフォーマットが当たり前になっていくものと思われます。
通信手段と使用するポート番号は以下の通り。
| プロトコル | ポート | 用途・特徴 |
|---|---|---|
| OTLP/gRPC | 4317 | ほとんどの言語用SDKで標準的に使用される高速なバイナリ通信 |
| OTLP/HTTP | 4318 | 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: COLLECTOR_HOST
valueFrom:
fieldRef:
fieldPath: status.hostIP
status.hostIP で Kubernetes の機能を使って、このアプリ Pod が今乗っているワーカーノードの物理 IP アドレスを環境変数 COLLECTOR_HOST に設定している。
アプリケーションでは COLLECTOR_HOST:4317 をデータの送信先とすることで、コレクターにデータを送信し、これを受けたコレクターがバックエンドにデータを転送する。
コレクターの話はこれぐらいにして次は実装の話
アプリケーションの話(Goの実装の話)
ここからは、各シグナルをアプリケーションで計測する際の実装方法の話をして行きます。
OpenTelemetry のログの実装については私はよくわかっていないので、トレースとメトリクスについて記載します。
TraceとSpan
トレース (Trace) は、リクエストが分散システム(マイクロサービスなど)を通過する際の一連の流れ、経路を記録するものです。
セットで覚えるべき概念としてスパン (Span) があります。
スパンは、システム内で行われた「単一の処理」の記録です。
例えば、「認証APIを呼び出す」「データベースからユーザー情報を取得する」「関数Aを実行する」といった個々の作業が1つのスパンになります。
ユーザーの1回の操作(リクエスト)から始まる処理は、どれだけ多くのサーバーやマイクロサービスをまたいでも Trace は複数の Span から構成され、それらが同じ Trace ID によって関連付けられます。
Datadog APM でトレースを確認すると以下画像のような画面になりますが、この1つ1つの横棒がスパンを表し、全体が1つのトレースを構成します。
トレースはスパンの集合とも言える。

どう実装するのか
呼び出し元から受け取ったコンテキストを元にSpanを生成すると、それは同一トレース内のスパンという扱いになります。
Goのサンプルコードは以下。
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()
...
}
このような形で、受け取ったコンテキストを元にスパンを生成していきます。
引数で受け取ったコンテキストを無視して、新規にコンテキストを生成してSpanを開始したら、それは別のトレースになるし、そこがルートスパンになります。
プロパゲーター
プロパゲーターは、分散トレーシング用のデータ伝播形式です。
OpenTelemetry は TraceContext や Baggage という W3C の標準フォーマットに準拠しており、HTTP/gRPC のヘッダーなどを介してコンテキストを受け渡すことができます。
実装イメージ package main
import (
otelpropagation "go.opentelemetry.io/otel/propagation"
)
// propagatorの設定を返す。OTelのHTTPサーバー/クライアントのミドルウェアで使用
func newPropagation() otelpropagation.TextMapPropagator {
return otelpropagation.NewCompositeTextMapPropagator(
otelpropagation.TraceContext{},
otelpropagation.Baggage{},
)
}
追加で b3 などのベンダー固有のフォーマットにも、上記と合わせて複合的に対応できるようになっています。
OpenTelemetry ライブラリの NewCompositeTextMapPropagator を使って、複数の分散トレース用伝播形式(Propagator)を1つにまとめ、異なる規格(W3C TraceContextやBaggage、b3)を同時に扱えるように合成することもできます。
TextMapPropagator の概要
- 役割: 渡された複数の
TextMapPropagatorを統合し、1つの統一されたPropagatorとして機能させる。 - 用途: 異なるシステムやベンダー(Datadog、GCP Cloud Trace、標準のW3Cなど)が混在する環境で、トレース情報を正しく引き継ぐために使用する。
実装イメージ package main
import (
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との互換性維持のため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() への移行が最も推奨される方法です。
カスタムメトリクス
収集するメトリクスを独自実装してデータ収集することもできる。
メトリクスにもいくつか種類があるのでその話。
公式はリンク先参照。
メトリクスの計装の種類は以下の通り。
テンポラリティ
OpenTelemetry にはテンポラリティ(時間性)というものがあります。
カウンターやヒストグラムなどのメトリクスが、時間経過に対してどのように集計・報告されるかを定義する概念です。
cumulative(累積)とdelta(差分)の2つがあり、Counter, Histogram が影響を受けます。
- cumulative: アプリケーション起動時からの合計値を送信し続ける方式。途中でメトリクスがリセットされないため、長期的な傾向分析時に使用。
- delta: 測定期間内に変化した値のみを送信する方式。データ送信毎に状態がリセットされる。
OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCEという環境変数の値を cumulativeまたはdeltaに設定することで処理方式を切り替えることができる。
OpenTelemetry のデフォルトは cumulative です。
注意点として、Prometheus や Datadog など、受ける側の監視ツールによって推奨・対応しているテンポラリティが異なります。
どのバックエンドでどちらが推奨されるかはバックエンドの仕様を確認する必要があります。
参考:OpenTelemetry テンポラリティとカーディナリティ制限
参考
書籍
- 入門 OpenTelemetry ―現代的なオブザーバビリティシステムの構築と運用
- 実践 OpenTelemetry ―オープンなオブザーバビリティ標準を組織に導入する
- OpenTelemetryではじめるテレメトリーサンプリング ――シグナルデータ膨張に対応しながらオブザーバビリティを確保する
サイト
- OpenTelemetry
- OpenTelemetry Documentation
- OpenTelemetry Metrics Exporter - OTLP
- OpenTelemetry OTLPエクスポーター設定
- OpenTelemetry テンポラリティとカーディナリティ制限
- OpenTelemetry メトリクス
- OpenTelemetry Semantic Conventions
- 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



