ブログに戻る

フォロー&ご登録

OpenTelemetry 第3回 : Compute で OpenTelemetry を使用する

大室克之

Developer Relations、Senior Software Engineer, Fastly

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 は、JaegarZipKin のインスタンス、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.yamlexamples/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 module
const webpackHelpers = require("@fastly/compute-js-opentelemetry/webpack-helpers");

module.exports = {
  entry: "./src/index.js",
  /* ... other configuration */
};

// Add this line
module.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 が非常にパワフルで魅力的な理由のひとつは、システムを構成するさまざまなコンポーネント間 (プロセスやサーバーも含みます) をシームレスにトレースすることによって、システム全体のアーキテクチャを可視化できることにあります。これを可能にしているのは、トレースコンテキストの伝播、すなわち、あるコンポーネントで開始されたトレースが、そのコンポーネントによって呼び出された他のコンポーネントにコンテキストを伝播する機能です。このブログシリーズの第2回でご紹介したように、traceparent ヘッダーを利用することで、この機能を Web API で利用できます。undefined 

@fastly/compute-js-opentelemetry に含まれるインストルメント化は、送受信するリクエストに対してこのメカニズムをサポートしています。すなわち、既存のトレースコンテキスト内でアプリケーションが呼び出されると、受信したリクエストのヘッダーからこのライブラリによってトレース情報が抽出され、生成されるあらゆるスパンの親としてそのトレースが使用されます。同様に、アプリケーションがバックエンドフェッチを行う場合、フェッチを実行する際にこのライブラリによって現在のトレースのコンテキストがリクエストヘッダーに挿入されます。非常に便利なことに、getComputeJsAutoInstrumentations でトレースメカニズムを始動させることで、これらが自動的に有効になります。

このように、OpenTelemetry を出力する API を呼び出すだけで、追加作業なしで上記のようなネスト化されたトレースを生成できます。

すべてのコンポーネントがトレースを生成し、最終的にコレクターがそれらを収集してまとめてスクリーンショットのようなグラフを生成し、アプリケーション全体を通じた一連のエンドツーエンドの動作に関するライフタイム全体をトレースできることが、OpenTelemetry の強みです。

このようなデータは任意の分析ツールやインサイトツールで利用できます。私たちは、Fastly のツールが皆さまのシステムアーキテクチャの一部として大きく貢献することが可能であると信じています。

現在の課題

OpenTelemetry は新しいテクノロジーです。公式ライブラリーが成熟し、このテクノロジーをサポートするツールやプラットフォーム、フレームワークが増える中、解決すべき課題をまだ抱えています。

その中のひとつが、JavaScript OpenTelemetry ライブラリのメトリクス API と SDK が未だに開発ステータスにあることです。そのため、現時点では私たちのライブラリはメトリクスを完全にサポートすることができません。公式ライブラリの進捗に応じて私たちもライブラリをアップデートし、近いうちにメトリクスを使用できるようになることを期待しています。

ライブラリをぜひご確認ください

OpenTelemetry に興味がおありですか?私は、自分で作成したデモによって生成されたトレースダイアグラムを初めて見たとき、ワクワクしました。この記事でご紹介した Compute で OpenTelementry を使用するための JavaScript ライブラリのドキュメントとソースコードをぜひご確認ください。このライブラリは GitHub で fastly/compute-js-opentelemetry として公開されています。

私たちは、Compute で OpenTelemetry を使用できるようになったことをとても嬉しく思います。このライブラリを使用して Compute アプリケーションでインストルメント化を行った方は、ぜひご感想をお聞かせください。

このシリーズの最終回にあたる第4回では、OpenTelemetry を使って Fiddle ツールをインストルメント化するケーススタディをご紹介します。ご期待ください!


OpenTelemetry に関する全4回にわたるブログシリーズの記事をぜひご覧ください。