OpenTelemetry 第3回 : Compute で OpenTelemetry を使用する
Fastly 初の Compute 向け OpenTelemetry ライブラリが利用可能になりました。これにより、Compute アプリケーションで仕様に準拠したトレースを生成し、パフォーマンスとリソースに関するより詳細なインサイトが得られるようになりました。このブログ記事では、この機能をエッジアプリケーションに簡単に追加する方法をご紹介します。
Computeプラットフォームでは、Fastly のエッジサーバーのグローバルネットワーク全体で任意の言語からコンパイルされた WebAssembly モジュールを実行できます。現時点では Rust、AssemblyScript、JavaScript が公式にサポートされ ていますが、C++ や Swift、Zig でアプリケーションを作成している器用なお客様もいらっしゃいます。
このブログシリーズの第2回では、OpenTelemetry を VCL サービスに追加するためにプロトコル言語をそのまま使用する必要がありましたが、OpenTelemetry は上記の言語の多くのライブラリをサポートし、開発者が SDK オブジェクトやコールを使用して作業できる利便性を備えています。Compute は安全上の理由から、SDK を通じて公開するホストコールに外部との通信を制限しているため、OpenTelemetry ライブラリが提供するデフォルトのエクスポーターを直接使用することはできません。しかし、OpenTelemetry のモジュールアプローチのおかげで、OpenTelemetry の公式ライブラリを Compute と互換のコンポーネントで拡張できます。
そのため、まずは JavaScript 向けに Compute で OpenTelemetry をサポートすることに取り組みました。
コレクターの設定
データの出力について考える前に、アプリケーションが送信するトレースを受け入れるコレクターが必要です。通常、本番環境のアーキテクチャでは、OTLP over HTTP レシーバーが有効で、 アプリケーションから通信可能なサーバーで動作する OpenTelemetry コレクターが使用されます。コレクターは受信したデータをバックエンドにエクスポートします。OpenTelemetry は、Jaegar や ZipKin のインスタンス、Honeycomb などのサービスを含むさまざまな種類のバックエンドにエクスポートできます。
ローカル環境でテストする場合、コレクターを動作させる最も簡単な方法として、コレクターに加えて Jaeger などのバックエンドも起動させることができる OpenTelemetry Collector Demo の利用をお勧めします。ちなみにこの記事では、この設定を用いて、Jaeger UI から取得したスクリーンショットを使用しています。
この記事の例を参考にしながら Collector Demo を使用するには、Docker に加えて、デモを実行するための手順に従います。また、OTLP over HTTP を有効にするために、設定にいくつか変更を加える必要があります。otel-collector-config.yaml
を変更して http プロトコルを有効にします。
receivers: otlp: protocols: grpc: http: # Add this
exporters:...
Collector Demo は Docker で動作するため、ポートを利用可能にし、必要のないコンポーネントの一部を無効にします。docker-compose.override.yaml
を examples/demo/
に追加します。
version: "2"services: otel-collector: ports: - "4318:4318" # OTLP HTTP receiver demo-server: profiles: - disabled demo-client: profiles: - disabled
これらの変更を行った後、Collector Demo を開始します。
$ cd examples/demo$ docker-compose up
これで、トレースをコレクターに送信する準備ができました。コレ クターはトレースを Jaeger と ZipKin の両方にエクスポートできます。Jaeger UI には http://localhost:16686/ から、ZipKin UI には http://localhost:9411/ からアクセスできます。
データを収集する準備ができたところで、早速 Compute で OpenTelemetry を実際に使ってみましょう。
OpenTelemetry をアプリケーションに追加する
通常、OpenTelemetry で計測される JavaScript アプリケーションは、トレースの初期化ファイルを追加し (「tracing.js」などのファイル名が使用されます)、これによって以下のインスタンスが作成されます。
リソース : アプリケーションをコレクターに説明します。
インストルメント化 : 計測データを生成するためにプラットフォームやフレームワークイベントにフックするモジュール
エクスポーター : OTel データを取り込んで外部コレクターに何らかの方法で送信できるモジュール
SDK : 上記をすべてまとめて実装するエージェント
私は Compute 向けに、これらを実装するパッケージを JavaScript で作成し、オープンソースにして @fastly/compute-js-opentelemetry として npm で公開しています。このパッケージには、以下のカスタムコンポーネントなどが含まれます。
Fastly のリクエストライフサイクルと、エッジコードからのバックエンドフェッチのインストルメント化
Fastly のバックエンドフェッチまたはリアルタイムログのメカニズムを使用して OTel データをコレクターに送信するカスタムエクスポーター
エクスポーター、インストルメント化、リソースの実装プロセスを簡素化するカスタム SDK
OpenTelemetry が簡単に追加できることを示すために、JavaScript で書かれたシンプルな Compute アプリケーションを例に見てみましょう。
/// <reference types='@fastly/js-compute' />
async function handleRequest(event) { const backendResponse = await fetch('https://httpbin.org/json', { backend: 'httpbin', });
const data = await backendResponse.text(); return new Response(data.length, { status: 200, Headers: { 'Content-Type': 'text/plain', }, });}addEventListener('fetch', (event) => event.respondWith(handleRequest(event)));
この例では、リクエストを受信すると、バックエンドの httpbin からデータをフェッチした後、データのサイズを測定し、その結果を含むレスポンスを作成してクライアントに送信するという処理が行われます。
OpenTelemetry を追加するため、@fastly/compute-js-opentelemetry
と OpenTelemetry ライブラリをいくつか追加してみましょう。
$ npm install @fastly/compute-js-opentelemetry$ npm install @opentelemetry/resources @opentelemetry/semantic-conventions
これらのライブラリを Compute で使用するには、若干のポリフィルとシムが必要です。幸い、ビルドプロセスでヘルパーパッケージの fastly/compute-js-opentelemetry/webpack-helpers
を適用することで、これらを簡単にインポートできます。以下のように、プロジェクトの webpack.config.js
ファイルに変更を加えます。
// Reference the helper moduleconst webpackHelpers = require("@fastly/compute-js-opentelemetry/webpack-helpers");
module.exports = { entry: "./src/index.js", /* ... other configuration */};
// Add this linemodule.exports = webpackHelpers.apply(module.exports);
次に、以下のような tracing.js
というトレースの初期化ファイルを作成します。
import { Resource } from '@opentelemetry/resources';import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import { FastlySDK } from '@fastly/compute-js-opentelemetry/sdk-fastly';import { OTLPTraceExporter } from '@fastly/compute-js-opentelemetry/exporter-trace-otlp-fastly-backend';import { getComputeJsAutoInstrumentations } from '@fastly/compute-js-opentelemetry/auto-instrumentations-compute-js';
const sdk = new FastlySDK({ traceExporter: new OTLPTraceExporter({backend: 'otlp-collector'}), instrumentations: [getComputeJsAutoInstrumentations(),], resource: new Resource({ [SemanticResourceAttributes.SERVICE_NAME]: 'my-fastly-service', }),});
await sdk.start();
これは、OpenTelemetry の Node.js チュートリアルで使用される典型的なトレースの初期化ファイルとよく似ていますが、一部のコンポーネントが Compute の実装に置き換えられています。リストされているエクスポーターは、バックエンドの otlp-collector として Fastly サービスに登録されているコレクターを想定します。
トレースを有効化するため、tracing.js
をアプリケーションにインポートします。
/// <reference types='@fastly/js-compute' />import './tracing.js';
async function handleRequest(event) {...
これだけで、アプリケーションを実行すると、リクエストに対するスパンを取得し始めます。
このトレースには、興味深い点がいくつかあります。
まず、FetchEvent と名付けられたリクエスト全体のスパンと、リ スナー関数、(example.com などに対する) バックエンドフェッチ、event.respondWith コールの3つの子スパンが、1つの呼び出しに関するグラフを構成しているのが確認できます。リスナー関数と event.respondWith の実行が非常に短時間で行われたことが示されています。理由は、ハンドラー関数がすぐに Promise を返すためです。ハンドラー関数から Promise を返すことで、Compute サービスはリスナー関数の実行後に処理を続けることができます。プログラムはこの間、Backend Fetch を実行します。SDK もこのメカニズムのメリットを利用して、抽象化されたデータのコレクターへの送信が完了するまでイベントのライフサイクルを延長するので、心配ありません。
tracing.js がアプリケーションコードから完全に独立しているのは、非常に素晴らしいです。トレースメカニズムとこのファイルに設定されたデフォルトのインストルメント化だけで、アプリケーションに認識されることなくテレメトリを生成できます。
カスタムスパン
ただし場合によっては、カスタムスパンとイベントでコードをインストルメント化したいこともあるでしょう。たとえば、開始時間と完了時間を記録したい関数がある場合や、関数が呼び出される頻度を確認することが必要な場合があるかもしれません。
標準の OpenTelemetry を実装しているので、それも可能です。
$ npm install @opentelemetry/api
/// <reference types='@fastly/js-compute' />import './tracing.js';import { context, trace } from "@opentelemetry/api";
async function handleRequest(event) { const tracer = trace.getTracerProvider() .getTracer('my-tracer');
const mySpan = tracer.startSpan('my-task'); context.with(trace.setSpan(context.active(), mySpan), () => { doTask(); // spend some time in a task }); mySpan.end();
return new Response('OK', { status: 200, headers: new Headers({"Content-Type": "text/plain"}), });}
addEventListener("fetch", (event) => event.respondWith(handleRequest(event)));
生成されたトレースは以下のようになります。
この場合は Backend Fetch の代わりに、my-task というカスタムスパ ンがあり、19ミリ秒かかったことが確認できます。
唯一 Fastly 独自のコードが使用されているのはトレースの初期化モジュールにおいてのみであり、OpenTelemetry を使用するプログラムのメインソースで行うすべての動作には標準の OpenTelemetry API が使用されます。ここでは、Fastly 独自のものは何も必要ありません。実際、すでに OpenTelemetry でインストルメント化されている Compute アプリケーションに組み込まれるコードもすべて問題なく機能し、セットアップしたトレースメカニズムによってそれらのデータも収集されます。
トレースコンテキストの伝播
このトレースメカニズムでは、Compute アプリケーションに組み込まれた他のコードや、独自のコードによって生成された OpenTelemetry データを収集できると上記で説明しましたが、OpenTelemetry を使ってさらに、Fastly アプリケーションによって呼び出される他のプロセスや Web API からデータを収集することも可能です。
OpenTelemetry が非常にパワフルで魅力的な理由のひとつは、システムを構成するさまざまなコンポーネント間 (プロセスやサーバーも含みます) をシームレスにトレースすることによって、システム全体のアーキテクチャを可視化できることにあります。これを可能にしているのは、