【Go】OpenTelemetry SDK で Cloud Trace にスパン属性として配列を送る

今年も師走ということでアドベントカレンダー2024が始まりました。カヤックSREの市川です。

初回は、Google Cloud における分散トレースの話です。前半は関連知識のおさらいになるので、「オブザーバビリティだいたい分かるよ〜」という方は本題まで飛ばしてください。

ちなみに本記事の内容は、SDK で直接送信する前提です。Collector を立てている場合についても最後で少し触れますが、記事が役立つユースケースとしては App Engine Standard 環境を利用している場合が多いのかなと思います。

分散トレースのおさらい

免責:以下、おさらいの内容はある程度ラフな説明になりますので、詳しくはそれらを主題として扱っている資料をご参照ください。

分散トレースとは

分散トレースとは、言ってしまえばログです。ただし「開始と終了」の概念を持ち、「親子関係」で整理されています。

ログにおいては、たくさんのログがある中での各行のことを「エントリ」と言いますが、分散トレースにおいては「スパン」がこれに対応します。つまり下の図(Cloud Traceの画面)において、青い四角がスパンということです。

スパンの属性とは

それぞれのスパンに対して紐付けるメタデータ的な補足情報です。とても遅い挙動や新しいエラーが検出されたとき、「どの処理か」以外にもユーザー等のIDやパラメータの類を一緒に取得できると便利ですよね。属性とは、そういったトラブルシューティングのヒントになるかもしれない情報*1をスパンにぶら下げるための仕組みです。

属性は Key-Value 形式で構造化されています。下の例ではスラッシュ区切りの属性とドット区切りの属性がありますが、これは前者が OpenCensus、後者が OpenTelemetry の慣習に倣っているためです。

OpenCensus と OpenTelemetry

どちらもオブザーバビリティの標準化プロジェクトです。

トレースやメトリック等のデータを APM だったりクラウドサービスに送信するとき、それぞれ異なるインタフェースだとしんどいですよね。ということで、そういったデータを統一的なプロトコル・SDKで送ろう!というプロジェクトが作られました。

それが最初は OpenCensus と OpenTracing が双璧を成していたのですが「標準化プロジェクトが複数あるのも変だよね」ということで統一されて、OpenTelemetry になりました。

先ほどの Cloud Trace のスクショにスラッシュ区切りが入っていたのは「OpenCensus は Google が作ったものなので、それが内部で今も使われている」ということです。納得ですね(?)

スパン属性として受け入れ可能なデータ型

本題に入るのですが、OpenTelemetry と OpenCensus ではスパンの属性として受け入れ可能なデータ型が異なります。

Protocol Buffers のスキーマを見てみると、OpenCensusOpenTelemetryと異なり配列を受け入れないようです。

また、Cloud Trace の APIリファレンスを確認すると、小数も受け入れていないように見えます。

ただし、Go で OpenTelemetry SDK を使う場合、opentelemetry-operations-goexporter/trace パッケージを import することが殆どでしょうから、小数に関しては問題ありません。下記のコードで送信前に文字列化されているためです。

github.com

これに対して、配列はそのような処理が(SDKが利用する Exporter では)施されていないので、何もせずに配列の属性*2を付与すると「知らぬ間にドロップされていた」ということが起こります。

配列を文字列化して送信する

同じように配列も文字列化して送信してしまえばこの問題は解決するので、サクッと Processor を書きました。github.com/ebi-yade/spans/gcp というパッケージでご利用いただけます。とても簡単なコードなので、コピペ→アレンジして頂いても構いません。

利用方法としては、opentelemetry-operations-go の Exporter を初期化したら、それを直接 sdktrace.WithBatcher() に代入するのではなく、以下のステップで TracerProvider の初期化オプションに渡してあげる必要があります。

  1. Exporter 初期化
  2. sdktrace.NewBatchSpanProcessor() に代入
  3. 今回紹介した gcp.NewProcessor() に代入
  4. sdktrace.WithSpanProcessor() に代入

簡単なコードにまとめると以下のようになります*3

import ( // 一部抜粋
    exporter "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace"
    "github.com/ebi-yade/spans/gcp"
    "google.golang.org/api/option"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

exp, err := exporter.New(
    exporter.WithTraceClientOptions([]option.ClientOption{option.WithTelemetryDisabled()}), // avoid recursive spans
)
if err != nil {
    return errors.Wrap(err, "error NewExporter")
}

batchProcessor := sdktrace.NewBatchSpanProcessor(exp)
tp := sdktrace.NewTracerProvider(
    sdktrace.WithSpanProcessor(gcp.NewProcessor(batchProcessor)), // <= IMPORTANT!
)

実際にこの Processor を使った結果、送れなかった配列も文字列として受け取ってもらうことができました。

めでたしめでたし。

おまけ:Collector を利用する場合

最近の Google Cloud Observability*4ドキュメントのサンプル等では OpenTelemetry の Collector*5利用が前提となっていることが多く、個人的にも専属のインフラチームがあるプロジェクトでは Collector の採用も良い選択と考えております。

Collector の config.yamlexporters: ["googlecloud"] として指定する Contrib の Exporter も、結局は opentelemetry-operations-go を利用しているのですが、exporter/trace パッケージを直接 import するのではなく、 exporter/collector パッケージを介しています。

exporter/trace では先述のとおり小数を文字列化しているだけに留まりましたが、その前段で利用している exporter/collector で配列が文字列化されるようになっています。この変更は2024年の4月ごろに以下のPRで入ったようです。

github.com

筆者は実際には試していないのですが、おそらく比較的新しいバージョンの Collector を使えば、文字列化された配列が Cloud Trace の画面で確認できることでしょう。

まとめ

今回は Go 向けの OpenTelemetry SDK で Cloud Trace にトレースを送信するとき、スパンの属性に配列を含める方法について解説しました。

よくよく考えれば「同じリポジトリだし、Collector 側じゃなくて Exporter 側で配列の文字列化を実装してくれればな〜」みたいな話でもあるのですが、そもそも Collector がなんだかんだ主流の(=あんまり困っている人が多くなさそうな)世界で、あれこれ英語で説明してメンテナの合意を取って・・・といった心理的な障壁から逃げてサッと道具を作りたくなってしまうのは、カヤック社員の性なのかもしれません。

カヤックでは、小道具を作るのが得意なエンジニアも募集しています!

hubspot.kayac.com

*1:書籍オブザーバビリティ・エンジニアリングでは、とりあえず全てを前もって突っ込んでおくのが良いと書かれています。

*2:GoのSDKではスライスを付与しますが、まぁスライスも可変長の配列なので、そのツッコミはご容赦ください。

*3:もちろんこのままではコンパイルが通りません。特に処理の部分は、何かしらの関数内に記述する必要があります。

*4:以前は Stackdriver や Operations Suite という名前で親しまれていたサービスです。

*5:OTLPを用いてローカルネットワーク内で専用プロセスに転送するアレです。