🈷️

MoonBitでOpenTelemetryのライブラリを作っている途中の学び

に公開

↓の続編です。
https://zenn.dev/4245ryomt/articles/d1755b87d02fe9

前回まででJS/Native/WASMで使えそうなライブラリとして作り出しました。
使いっぷりとしては以下のようにSpanを作ってendしたら裏側でよしなにotlpエンドポイントへSpanが送られるという雰囲気です。
https://github.com/ryota0624/moonbit_otel/blob/main/example/server/main.mbt#L11-L76

WASM対応は一旦置いてJS/NativeでTraceができるところまで作れて公開してみたのでここまでの学びを書きます。

https://mooncakes.io/docs/ryota0624/moonbit_otel

opentelemetry-protoを使うことにしてフォーマットにハマる

なんとopentelemetryにはインターフェースが受け付ける入力フォーマットがprotobufで宣言されていたのです。
https://github.com/open-telemetry/opentelemetry-proto

そしてMoonBitにはprotobufのための実装がすでに存在していました。
https://github.com/moonbitlang/protoc-gen-mbt

途中でバグに気づいてPRを出したりしていました。
https://github.com/moonbitlang/protoc-gen-mbt/pull/73

そんな中なかなか時間を溶かしたのがspanId/traceIdの扱いでした。
この2つはprotobuf上でbytes型で宣言されています。どちらの値もprotobufに閉じない概念として、バイト配列が表現上の仕様になっているので納得の宣言です。
https://github.com/open-telemetry/opentelemetry-proto/blob/main/opentelemetry/proto/trace/v1/trace.proto#L95-L104

一方でprotobufでも値をJSONにエンコードして扱う時には特別扱いするようになっています。
protobufのbytes型はJSONの値で扱う時にはbase64 stringで扱うことになっています。
https://github.com/open-telemetry/opentelemetry-proto/blob/main/docs/specification.md#json-protobuf-encoding

最初はシンプルにprotobufライブラリのtoJSON的なメソッドを使っていましたが、このことに気づいてSpanをprotobufで定めた形式のJSONで送る際には特別に実装しているエンコードのロジックを呼び出すことにしました。

https://github.com/ryota0624/moonbit_otel/blob/main/span.mbt#L349-L355

fire and forgetには非同期キューを使うことにした

Spanの送信というのは基本的には非同期で行いたいです。
たとえばHTTPリクエストを処理してレスポンスを返すまでの間にSpanの送信がOKを返すのを待つ時間は基本あってほしくないです。

JavaScriptではそんなとき以下のように非同期関数の呼び出しを関数の中で結果を待たずに行えます。(良し悪しはおいておき。)

async function sendSpan(span) {
  // use fetch
}

async function handleHttpRequest(request) {
  const queryResult = await db.query(...);
  const span = new Span(...);
  // sendSpanの結果をまたずに次へ進む
  sendSpan(span);
  queryResult++;
  await db.exec(queryResult);
  return {statusCode: 200};
}

しかしMoonBitの標準な非同期処理のライブラリの範囲ではJavaScriptのようにシュッと非同期処理の打ちっぱなし(fire and forget)ができないようです。

前提としてMoonBit標準の範囲ではasync関数の呼び出しは以下の2つの方法になります。
基本的に呼び出したらそこで待つことを求められるようです。

moonbitlang/asyncにはTaskGroupという非同期処理をいくつも開始できる実装がありますが、これでも呼び出した全ての非同期処理が結果を出すまで呼び出し側は待つことになるので非同期処理の打ちっぱなしはできないようです。
https://github.com/moonbitlang/async?tab=readme-ov-file#structured-concurrency-and-error-propagation

TaskGroupがすべての非同期処理の完了または失敗を待つことは、ドキュメントにあるようにリソースリークの余地を残さないためなんですね。よさそう。

When with_task_group returns, it is guaranteed that all tasks spawned in the group already terminate, leaving no room for orphan task and resource leak.

ではどうしたかというと非同期キューを利用してみました。
https://mooncakes.io/docs/moonbitlang/async/aqueue

キューへのpushはasync関数になっています。キューにメッセージを詰めるだけなので実際にspanを送るより明らかにすぐ終わることでしょう。
今回メッセージは「送信予定のSpanを追加したからN秒後にflushよろしく」という感じにしてみました。
https://github.com/ryota0624/moonbit_otel/blob/main/span_processor.mbt#L146-L160

「よろしく」する側は「手続き」をよろしくするのではなく、「メッセージ」を送ってよろしくするようになったとでも言いましょうか。
「メッセージ」はプレーンなデータと捉えてみると、手続きをよろしくするよりリソースリークに強そうな気がしますね。


OpenTelemetryの仕様とMoonBitの非同期処理について時間を溶かしたり、頭を捻ってなんとなく動きそうな感じになってきました。
引き続き作ってみる予定です。

Discussion