OSS コードリーディング〜Jaegerのコードを読んで 4317 ポート以外でトレースを受け付ける〜
はじめに
Jaeger のソースコードを読んでやりたいことを実現するにはどのオプションを指定すればいいのかを調べました。皆さんのおすすめのソースコードリーディングのやり方があれば教えてください。
やりたいこと
OpenTelemetry を使った分散トレーシングを試そうとしていました。アプリを OpenTelemetry SDK で計装し、OpenTelemetry Collector でテレメトリを収集し、何かしらのバックエンドで可視化することを考えます。アプリと Collector は Docker Compose を使ってローカルで動かします。OpenTelemetry で計装していればアプリのコード変更を抑えながらバックエンドを切り替えることができます。可視化に使うツールはなんでもよくて AWS なら X-Ray, ローカル環境なら何かしらの OSS を使おうと思っていました。一度 AWS にデプロイして X-Ray で可視化できることが確認できれば、それからあとはローカル環境で完結させたい(アプリを書き換えるたびにコンテナをビルドしてクラウド上にデプロイするのは時間がかかるので)ので、ローカル環境でも動かせる Jaeger を今回は使うことにしました。Jaeger なら UI も組み込まれていて Jaeger all-in-one を使えば UI も collector も全部入りのコンテナイメージを利用できます。
Jaeger は OpenTelemetry SDK で計装されたテレメトリデータを OpenTelemetry Protocol で受け取ることができるのでちょうどいいと思っていたのですが今回ひとつ悩むことが出てきました。Jaeger ドキュメントの getting started ページを見る限りだと 4317 ポートで OpenTelemetry Protocol でのテレメトリを待ち受けそうですが、このポートは OpenTelemetry Collector receiver のデフォルトポートと同じです。このままだと Docker Compose で OpenTelemetry Collector と Jaeger all-in-one コンテナを動かした時に 4317 ポートを取り合ってしまいます。どちらかのポートを変更しないといけません。
OpenTelemetry 側のポートを変更する方法はすぐ見つかって receiver の設定を変更し endpoint
を 0.0.0.0:5000
とかにすればポートを変えられそうです。Jaeger の方はパッと検索した限りだとどうすればいいか分かりませんでした。Jaeger サーバーを起動するときの CLI flag で制御できそうですが flag がたくさんあってどれを指定すればいいかよく分かりません。
アプリのコードの中で OpenTelemetry Collector のエンドポイントを指定するのでそのエンドポイントをデフォルトから全然違うポートにするのは個人的になんとなく避けたくて、OpenTelemetry Collector のエンドポイントは 4317 ポートのまま使いトレースの可視化に用いる Jaeger のポートはデフォルトから変更して使いたいです。幸い Jaeger のコードは GitHub 上で公開されているのでコードを読んで設定方法を調べようと思いました。
ソースコードを読んでいく
まず cmd ディレクトリから見ていきます。今回使うのは Jaeger All-in-one コンテナなので all-in-one ディレクトリに注目します。そこにある main.go をみてみましょう。
all-in-one/main.go
を読んでいく
main.go
のなかの main
関数をざっとみてみます。Go の CLI でよく使われる cobra でフラグを制御しているようです。all-in-one コンテナは collector や query などのコンポーネントを内包していて collector の初期化はこの辺りで行われています。collector の詳細は collector ディレクトリを見ると良さそうなのでそちらのディレクトリに移動します。
collector/main.go
を読んでいく
main.go
のなかの main
関数をざっとみてみます。この辺りで構造体が初期化されてこの辺りで Start メソッドが呼ばれています。
// Start all Collector services
if err := collector.Start(collectorOpts); err != nil {
logger.Fatal("Failed to start collector", zap.Error(err))
}
この時の引数で渡されている collectorOpts
が名前からして collector のオプションを管理していそうなのでこの構造体を見ていきます。ここで flags.CollectorOptions
から生成されているようです。
collectorOpts, err := new(flags.CollectorOptions).InitFromViper(v, logger)
というわけで initFromViper
を見てみましょう。
collector/app/flag.go
を読んでいく
この関数は CollectorOptions
を初期化しています。
// CollectorOptions holds configuration for collector
type CollectorOptions struct {
// 中略
// OTLP section defines options for servers accepting OpenTelemetry OTLP format
OTLP struct {
Enabled bool
GRPC GRPCOptions
HTTP HTTPOptions
}
}
コメント("CollectorOptions holds configuration for collector")を読む限りだとこの構造体が Collector の設定を表しているようなので詳しく見ていきます。書き忘れていたのですが今回は OTLP over gRPC でアプリから Collector に接続しようとしているので特に GRPC の設定を見ていきます。ここら辺を読むと OTLP はそもそも有効化しているかどうか、有効かしているなら gRPC か HTTP どちらを使うのかを構造体で管理していそうです。というわけで OTLP.GRPC を初期化しているところを探すとここで初期化していることが見つかります。
if err := cOpts.OTLP.GRPC.initFromViper(v, logger, otlpServerFlagsCfg.GRPC); err != nil {
return cOpts, fmt.Errorf("failed to parse OTLP/gRPC server options: %w", err)
}
引数で渡されている otlpServerFlagsCfg
の定義を見てみるとここで定義されています。
var otlpServerFlagsCfg = struct {
GRPC serverFlagsConfig
HTTP serverFlagsConfig
}{
GRPC: serverFlagsConfig{
prefix: "collector.otlp.grpc",
tls: tlscfg.ServerFlagsConfig{
Prefix: "collector.otlp.grpc",
},
},
HTTP: serverFlagsConfig{
prefix: "collector.otlp.http",
tls: tlscfg.ServerFlagsConfig{
Prefix: "collector.otlp.http",
},
},
}
OTLP.GRPC.initFromViper の定義はここです。この中のこの行でポート番号を設定しています。
opts.HostPort = ports.FormatHostPort(v.GetString(cfg.prefix + "." + flagSuffixHostPort))
<serverFlagsConfig.prefix>.<flagSuffixHostPort>
の形式のフラグからポートが指定されていることがわかりました。serverFlagsConfig
が otlpServerFlagsCfg.GRPC
であることと flagSuffixHostPort
が host-port
である(ここの定義より)と合わせて考えると最終的に Collector が受け付けるポートは --collector.otlp.grpc.host-port
であることがわかりました。
つまりどうすれば
Docker Compose で立ち上げる場合は以下のように書けばいいです。
version: "3.1"
services:
jaeger:
image: jaegertracing/all-in-one:1.49
command:
- "--collector.otlp.grpc.host-port=5000"
おわりに
Go のコードは型があるのでディレクトリ構成の感覚がつかめればあとは関数を追っていくことで処理の流れを掴むことができました。ただもっと良いコードリーディングのやり方もある気がします。
Discussion