OpenTelemetryでブラウザとサーバー間のトレースを試す

この記事は[All About Group（株式会社オールアバウト） Advent Calendar 2022](https://qiita.com/advent-calendar/2022/allabout) の20日目の記事です。

## はじめに
[OpenTelemetry](https://opentelemetry.io/)について調べていて、Node.jsだけでなくブラウザにも対応していることを知りました。
これらを使えばブラウザからアプリケーションサーバー、APIの一連の処理をトレースできそう？と考えたので試してみました。

:::note warn
ローカル環境で試しましたが、この記事では解決できていないことが多くあります。
実際にOpenTelemetryを利用するときは[公式ドキュメント](https://opentelemetry.io/docs/)をお読み下さい。
:::

## OpenTelemetryの概要
オブザーバビリティのキーワードで調べると主要な3要素として「ログ」「分散トレーシング」「メトリクス」が出てきます。OpenTelemetryでは、これらの3要素の計測からデータのエクスポートまでを標準化してツールの提供を進めています。

この記事では「分散トレーシング」を試してみます。
分散トレーシングは処理の全体像を理解したり、性能のボトルネックを特定することに役立てることができます。特に複数のサービスをまたいだリクエストの流れを追跡できるのが特徴的です。

OpenTelemetryの各言語ごとの開発進行状況は[Statusのページ](https://opentelemetry.io/status/)をご確認ください。

### OpenTelemetryの範囲外
OpenTelemetryは収集したテレメトリーデータの格納やWeb UIによる可視化は行いません。
トレーシングでは、[Jaeger](https://www.jaegertracing.io/)や[Cloud Trace](https://cloud.google.com/trace?hl=ja)といった別のサービスと組み合わせて利用します。

### TraceとSpan
開始時間と終了時間を持つ「Span」と呼ばれる処理単位の一連の流れ（Trace）を追うことで、
どのような処理が、どこから呼ばれて、どれくらいの時間をかけて進んだのかを把握することができます。
1つのTraceに複数の親子関係を持つSpanが関連づけられます。

```text:Spanのイメージ
　Trace
|------------------------- RootSpan (100ms) -------------------------->
  |------------- /users (60ms)------------>  |---- /search (40ms) --->
    |---DB (30ms)--->  |---DB (20ms)--->       |---- DB (30ms) ---->
```

### Automatic Instrumentation（自動計装）
Traceを開始し、Spanを計測したいポイントごとに一つ一つ手動で実装することでデータを収集できますが、さまざまなユースケースに向けた「Automatic Instrumentation」と呼ばれるライブラリ群の開発も進められています。
参考：[JavaScriptのAutomatic Instrumentation](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node)

### Trace Context
各サービスでTraceやSpanを作るだけでなく、複数のサービス間で情報を伝搬する必要があります。
[Trace Context](https://www.w3.org/TR/trace-context/) Propagationと呼ばれる仕組みを利用して、TraceIDやParentSpanIDといったコンテキスト情報をサービス間で伝搬することで、分散環境下でも複数サービスのTraceを関連づけることができます。

## ローカル環境で試してみる
下記のような流れをローカル環境で試してみました。

1. ブラウザからアプリサーバーにリクエスト
2. アプリサーバーからAPIサーバーにリクエスト
3. 各トレースデータを[Otel Collector](https://opentelemetry.io/docs/collector/)を経由して[Jaeger](https://www.jaegertracing.io/)にエクスポートする

```
Request
[Browser] --> [Application　Server] --> [API Server]
     |                ↓                  |
     +-------> [Otel Collector] <--------+
                      ↓ Exporter
                   [Jaeger] 
```

## 試してみて理解したこと
今回はHTTPリクエストの自動計装に、ブラウザでは[FetchInstrumentation](https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-fetch)、サーバー（Node.js）では[HttpInstrumentation](https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-http)を利用しました。

これらのInstrumentationにはサービス間のContextの自動受け渡し処理が組み込まれています。
Contextの受け渡しのための処理は特に記述しなくても、Auto Instrumentationを利用すれば、ブラウザ、Webアプリ、APIの間でトレースを紐づけることができました。

![スクリーンショット 2022-12-19 8.19.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314282/505a8d84-1d0c-0d04-a910-d29a04f35689.png)

### サーバーサイドの例(Node.js)

```javascript:instrumentationの設定例
const sdk = new opentelemetry.NodeSDK({
    serviceName: 'Application_Service',
    traceExporter: new OTLPTraceExporter({
        url: 'http://otel-collector:4318/v1/traces'
    }),
    // ↓↓↓ 自動計測「HttpInstrumentation」を登録 ↓↓↓
    instrumentations: [new HttpInstrumentation()],
});
sdk.start();
```

```javascript:Request投げるだけでContextが共有される
app.get('/message', async (req, res) => {
    const response = await axios.get('http://api:8080');
    res.json(response.data);
})
```

### ブラウザの例

```javascript:instrumentationの設定例
const provider = new WebTracerProvider();
provider.register({ contextManager: new ZoneContextManager() });

const exporter = new OTLPTraceExporter({ url: 'http://localhost:4318/v1/traces' });
provider.addSpanProcessor(new SimpleSpanProcessor(exporter));

// ↓↓↓ 自動計測「FetchInstrumentation」を登録 ↓↓↓
registerInstrumentations({
  instrumentations: [
    new FetchInstrumentation()
  ]
});
```

```javascript:Request投げるだけでContextが共有される
fetch('http://localhost:8080/message');
```

## 分からなかったこと

### システム時刻の環境差異を吸収する方法が分からなかった

環境ごとのシステム時刻の調整が上手くいきませんでした。
特にブラウザは、ユーザーによって利用環境が異なることが想定されますが、環境差異があるままでは不整合が起きてしまいます。筆者のローカル環境では利用するブラウザによっても挙動が変わることがありました。

OpenTelemetryにカバーする仕組みがあるかもしれませんが、今回は見つけられていません。
ブラウザとサーバー間のトレース紐づけは、この問題の解決方法が分かってからにしたほうが良さそうです。