MicrometerのTracingで受け取ったtraceを継続する

## これはなに

SpringBoot Actuatorでは分散トレーシングの実装としてmicrometerの機能を使います。

分散トレーシングではあるアプリから別のアプリを呼び出す時、その呼び出し関係を復元するために、呼び出し元のTraceと最後のSpanの識別子(それぞれtrace id、span idと呼ぶ)を呼び出し先に伝達する必要があります。

HTTPリクエストの場合、SpringBootの自動構成とmicrometerのおかげで特に実装をせずとも、呼び出し元・呼び出し先のどちらも自動的に連携されます。

一方でそういったサポートがないプロトコルやクライアントもあります。[^1]

[^1]: 筆者の場合はMQTTクライアントに(Spring Integrationで利用されている)Eclipse Pahoを利用しましたが、instrumentの自動構成などはサポートされていないようでした。

そのようなプロトコル・クライアントライブラリを利用する場合、自身でtrace idやspan idを呼び出し側から呼び出し先に伝達し、呼び出し先でそれらを親とするspanを開始する必要があります。

その方法がmicrometerやSpringBoot Actuatorのドキュメントにもなかったので備忘録として残します。


## 呼び出しを行うspanの作成とtrace情報の送信

必須ではないですが、呼び出し側では呼び出しに関するspanを設けておくと良いと思います。

すでに呼び出し元アプリケーションでは一連のtraceが存在している前提で言うと以下のような感じです。

```java
class MessagePublisher {

    private final Tracer tracer; // DIなどで取得しておく

    private final MessagingClient client; // DIなどで取得しておく

    /**
     * メッセージを送信するメソッド
     * 
     * @param message 送信するメッセージ内容を保持するインスタンス
     */
    public void send(final Message message) {
        final var newSpan = this.tracer.nextSpan().name("send-remote");
        try (final var ignoredNewScope = this.tracer.withSpan(newSpan.start())) {
            final var currentTraceContext = this.tracer.currentTraceContext();

            final var traceId = currentTraceContext.traceId();
            final var spanId = currentTraceContext.spanId();
            final var sampled = currentTraceContext.sampled();

            final var traceEmbeddedMessage = this.embedTraceInfo(message, traceId, spanId, sampled);

            // traceId/spanIdなどを埋め込んだメッセージを送信する
            this.client.send(traceEmbeddedMessage);

        } finally {
            newSpan.end();
        }

    }

    /**
     * 送信するメッセージにtrace情報を付与したメッセージを作成する
     * 
     * @param originalMessage 元の送信するメッセージ
     * @param traceId trace id
     * @param spanId sample id
     * @param sampled sample対象かどうか
     */
    private Message embedTraceInfo(final Message originalMessage, String traceId, final String spanId, final boolean sampled) {
        // 送信するメッセージにtraceId、spanIdなどの情報を埋め込んで返す
        return null;
    }

}
```


## 受け取ったtraceを継続する

trace情報(trace id、span idなど)を受け取ったら受け取ったtraceを親としてspanを開始しなければいけません。そのためには、(micrometerの場合)受け取った情報からTraceContextインスタンスを作成して、`CurrentTraceContext#newScope(TraceContext)`メソッドでtraceコンテキストを受け取ったものに切り替えます。そのあと`Tracer#nextSpan()`で受け取った後の処理に対応するspanを作成します。

```java
class MessageConsumer {

    private final Tracer tracer; // DIなどで取得しておく

    /**
     * メッセージを受信したときに呼び出されるメソッド
     * @param message 受信したメッセージ
     */
    public void receive(final Message message) {
        // 受け取ったmessageからtraceId/spanId/sampled(オプション)を取り出す
        final var traceId = this.extractTraceId(message);
        final var spanId = this.extractSpanId(message);
        // sampledフラグはW3C Trace Contextでオプションと定められているので設定されていないかもしれない
        final var sampled = this.extractSampled(message)
                .orElse(true); // sampledフラグがない場合の挙動は標準化されていないのでアプリごとに決めてよい。ここではsampleしておく。

        // 受け取ったtrace情報で新しいTraceContextを作成する
        // sampled == falseのときの処理は省略
        final var propagatedContext = this.tracer.traceContextBuilder()
                .traceId(traceId)
                .spanId(spanId)
                .sampled(sampled)
                .build();

        // 作成したTraceContextに切り替える
        try (final var ignoredParentScope = this.tracer.currentTraceContext().newScope(propagatedContext)) {

            // 受け取ったメッセージを処理するspanをここで作成する
            // 受け取ったtrace/spanの子spanとして作成される
            final var newSpan = this.tracer.nextSpan().name("receive-remote");
            // 作成したspanを開始する
            try (final var ignoredNewScope = this.tracer.withSpan(newSpan.start())) {

                // 受け取ったmessageの処理をここで行う

            } finally {
                newSpan.end();
            }
        }
    }

    private String extractTraceId(final String message) {
        // messageからtrace idを取り出す
        return "";
    }

    private String extractSpanId(final String message) {
        // messageからspan idを取り出す
        return "";
    }

    private Optional<Boolean> extractSampled(final String message) {
        // messageからtrace idを取り出す
        return Optional.of(true);
    }

}
```

`sampled`フラグについてはW3Cの[仕様][w3c-trace-context-sampled-flag]を参考のこと。

[w3c-trace-context-sampled-flag]: https://www.w3.org/TR/trace-context/#sampled-flag


## 終わりに

分散トレースの伝搬仕様について、一連のリクエストに対する処理で共通して保持するtrace idと、その中の処理ごとに保持するspan idが伝達されることは共通していますが、trace取得を制限(フィルタリング)するときの方式では様々な方式があり、W3Cの仕様でも様々な方式について標準化はしていないものの言及があります。このあたりについては利用する分散トレーシングのベンダの仕様を確認してください。

メッセージへのtrace id/span idの埋め込み方について、W3Cの標準では、記事で触れたtrace id/span id/sampled flagを、`traceparent`という名前のフィールドに埋め込むことになっており、W3C Trace Contextのバージョン`00`と合わせて`00-4bf92f3577b34da6a3ce929d0e0e4736-4bf92f3577b34da6a3ce929d0e0e4736-01`のような形式[^2]の文字列として埋め込むことになっています。ただ、例えばMQTT v3.1.1ではユーザー定義ヘッダは存在しないため、メッセージ本文に含めるしかなく、これはアプリケーションレベルのプロトコルとして実装することになります。このように、まだHTTP以外のプロトコルへの適用方法は固まっていない場合もあると思います[^3]ので、最新情報を参照しつつ実装するようにする必要があります。

[openzipkin-b3-propagation]: https://github.com/openzipkin/b3-propagation

[^2]: ハイフン区切りで`00`がバージョン、`4bf92f3577b34da6a3ce929d0e0e4736`がtrace id、`4bf92f3577b34da6a3ce929d0e0e4736`がspan id、`01`がtrace flagと呼ばれ、最も右のbitがsampled flagとして使用されます。
[^3]: もしHTTP以外でのトレース伝搬について標準をご存じの方がいればコメント等でお教えください。