2022年9月5日

OpenTelemetry と Prisma を使用したトレースによるサーバーの監視

トレースは、アプリケーションのパフォーマンスを分析し、ボトルネックを特定するための強力なツールです。このチュートリアルでは、トレースのコアコンセプトと、OpenTelemetry および Prisma のトレース機能 を使用してトレースをアプリケーションに統合する方法を学びます。

Monitor Your Server with Tracing Using OpenTelemetry & Prisma

目次

はじめに

このチュートリアルでは、Prisma と Express を使用して構築された既存の Web アプリケーションにトレースを統合する方法を学びます。トレースは、トレースやその他のテレメトリデータ (ログ、メトリクスなど) を収集するためのベンダーニュートラルな標準である OpenTelemetry を使用して実装します。

最初は、HTTP エンドポイントの手動トレースを作成し、コンソールに出力します。次に、Jaeger を使用してトレースを可視化する方法を学びます。また、Prisma のトレース機能 を使用してデータベースクエリのトレースを自動的に生成する方法も学びます。最後に、自動インストルメンテーションとトレース使用時のパフォーマンスに関する考慮事項について学びます。

トレースとは?

トレースは、リクエストがアプリケーションを伝播する経路を記録する可観測性ツールです。トレースは、システムが特定のリクエストに応じて実行しているアクティビティを関連付けるのに役立ちます。トレースは、これらのアクティビティに関するタイミング情報 (開始時刻、期間など) も提供します。

単一のトレースは、ユーザーまたはアプリケーションによってリクエストが行われたときに何が起こるかについての情報を提供します。各トレースは、リクエスト中に発生する単一のステップまたはタスクに関する情報を含む、1 つ以上のスパンで構成されています。

Jaeger などのトレースツールを使用すると、トレースを次のような図として可視化できます

Visualization of a single trace

単一のスパンは、親スパン中に発生するサブタスクを表す複数の子スパンを持つことができます。たとえば、上の図では、PRISMA QUERY スパンには、PRISMA ENGINE という子スパンがあります。最上位のスパンはルートスパンと呼ばれ、開始から終了までのトレース全体を表します。上の図では、GET /ENDPOINT がルートスパンです。

トレースは、システムへのより深い理解と可視性を得るための素晴らしい方法です。アプリケーションに影響を与えているエラーやパフォーマンスのボトルネックを正確に特定できます。トレースは、各リクエストが複数のサービスに関与し、特定の問題をローカルで再現することが難しい分散システムのデバッグに特に役立ちます。

注: トレースは、システムの可観測性を向上させるために、メトリクス と組み合わせて使用​​されることがよくあります。メトリクスの詳細については、メトリクスのチュートリアル をご覧ください。

使用する技術

このチュートリアルでは、次のツールを使用します

  • トレースライブラリ/API としての OpenTelemetry
  • オブジェクト関係マッパー (ORM) としての Prisma
  • データベースとしての SQLite
  • トレース可視化ツールとしての Jaeger
  • Web フレームワークとしての Express
  • プログラミング言語としての TypeScript

前提条件

前提知識

これは初心者向けのチュートリアルです。ただし、このチュートリアルでは以下を前提としています

  • JavaScript または TypeScript の基本的な知識 (推奨)
  • バックエンド Web 開発の基本的な知識

: このチュートリアルでは、トレースと可観測性に関する事前の知識は前提としていません。

開発環境

このチュートリアルに従うには、次のものが必要です

  • ... Node.js がインストールされていること。
  • ... DockerDocker Compose がインストールされていること。
  • ... オプションで Prisma VS Code 拡張機能 がインストールされていること。Prisma VS Code 拡張機能は、Prisma に非常に優れた IntelliSense と構文の強調表示を追加します。
  • ... オプションで、このシリーズで提供されるコマンドを実行するために、(Linux および macOS のターミナル/シェルのような) Unix シェルにアクセスできること。

Unix シェルがない場合 (たとえば、Windows マシンを使用している場合) でも、手順に従うことはできますが、シェルコマンドをマシンに合わせて変更する必要がある場合があります。

リポジトリのクローン

トレースをデモンストレーションするために使用する Web アプリケーションが必要です。このチュートリアルのために構築した既存の Express Web アプリケーションを使用できます。

開始するには、次の操作を実行します

  1. リポジトリ をクローンします
  1. クローンしたディレクトリに移動します
  1. 依存関係をインストールします
  1. prisma/migrations ディレクトリからデータベースの移行を適用します

: このコマンドは、Prisma Client を生成し、データベースにシードも設定します。

  1. プロジェクトを開始します

: アプリケーションを開発中は、サーバーを実行し続ける必要があります。dev スクリプトは、コードに変更があるたびにサーバーを再起動します。

アプリケーションには、http://localhost:4000/users/random というエンドポイントが 1 つだけあります。このエンドポイントは、データベースから 10 人のユーザーのランダムサンプルを返します。上記 URL にアクセスするか、次のコマンドを実行してエンドポイントをテストします

プロジェクトの構造とファイル

クローンしたリポジトリの構造は次のとおりです

このリポジトリの注目すべきファイルとディレクトリは次のとおりです

  • prisma
    • schema.prisma: データベーススキーマを定義します。
    • migrations: データベース移行履歴が含まれています。
    • seed.ts: ダミーデータで開発データベースにシードを設定するスクリプトが含まれています。
    • dev.db: SQLite データベースの状態を保存します。
  • server.ts: GET /users/random エンドポイントを備えた Express サーバー。
  • tsconfig.json および package.json: 構成ファイル。

アプリケーションへのトレースの統合

Express アプリケーションには、すべてのコア「ビジネスロジック」(つまり、10 人のランダムなユーザーを返す) がすでに実装されています。パフォーマンスを測定し、アプリケーションの可観測性を向上させるために、トレースを統合します。

このセクションでは、トレースを初期化し、手動でトレースを作成する方法を学びます。

トレースの初期化

OpenTelemetry トレース を使用してトレースを実装します。OpenTelemetry は、幅広いプラットフォームと言語間で互換性のあるオープンソース実装を提供します。さらに、トレースを実装するためのライブラリと SDK が付属しています。

次の OpenTelemetry パッケージをインストールして、トレースを開始します

これらのパッケージには、OpenTelemetry トレースの Node.js 実装が含まれています。

次に、新しい tracing.ts ファイルを作成してトレースを初期化します

tracing.ts 内で、次のようにトレースを初期化します

initializeTracing 関数はいくつかのことを行います

  1. それは、トレーサープロバイダー を初期化します。これは、トレーサー を作成するために使用されます。トレーサーは、アプリケーション内にトレース/スパンを作成します。
  2. それは、トレースエクスポーター を定義し、プロバイダーに追加します。トレースエクスポーターは、トレースをさまざまな宛先に送信します。この場合、ConsoleSpanExporter はトレースをコンソールに出力します。
  3. それは、.register() 関数を呼び出すことによって、OpenTelemetry API で使用するためにプロバイダーを登録します。
  4. 最後に、関数への引数として渡された指定された名前でトレーサーを作成して返します。

次に、既存の server.tsinitializeTracing をインポートして呼び出します

これで、最初のトレースを作成する準備ができました!

最初のトレースの作成

前のセクションでは、トレースを初期化し、トレーサーをサーバーにインポートしました。ここで、tracer オブジェクトを使用して、サーバー内にスパンを作成できます。まず、GET /users/random リクエストをカプセル化するトレースを作成します。リクエストハンドラーの定義を次のように更新します

ここでは、startActiveSpan() を使用して新しいスパンを作成し、それが提供するコールバック関数内にすべてのリクエストハンドラーロジックを囲んでいます。コールバック関数には、span オブジェクトへの参照が付属しており、それを requestSpan という名前で指定しました。これを使用して、スパンの属性を変更または追加できます。このコードでは、リクエストの結果に基づいて、http.status という属性をスパンに設定しています。最後に、リクエストが処理されたら、スパンを終了します。

新しく作成したスパンを表示するには、http://localhost:4000/users/random にアクセスします。または、ターミナル内で次を実行することもできます

Express サーバーを実行しているターミナルウィンドウに移動します。次のようなオブジェクトがコンソールに出力されているはずです

このオブジェクトは、作成したばかりのスパンを表します。ここで注目すべきプロパティは次のとおりです

  • id は、この特定のスパンの一意の識別子を表します。
  • traceId は、特定のトレースの一意の識別子を表します。特定のトレース内のすべてのスパンは、スペインの traceId を持ちます。現在、トレースは単一のスパンのみで構成されています。
  • parentId は、親スパンの id です。この場合、ルートスパンには親スパンがないため、undefined です。
  • name は、スパンの名前を表します。これは、スパンを作成したときに指定しました。
  • timestamp は、スパンの作成時刻を表す UNIX タイムスタンプです。
  • duration は、スパンの期間 (マイクロ秒単位) です。

Jaeger でのトレースの可視化

現在、コンソールでトレースを表示しています。これは単一のトレースの場合は管理可能ですが、多数のトレースの場合はあまり役に立ちません。トレースをより深く理解するには、トレースを可視化できるトレースソリューションが必要です。このチュートリアルでは、この目的のために Jaeger を使用します。

Jaeger のセットアップ

Jaeger は、次の 2 つの方法でセットアップできます

このチュートリアルでは、Docker Compose を使用して Jaeger の Docker イメージを実行します。まず、新しい docker-compose.yml ファイルを作成します

ファイル内で次のサービスを定義します

このイメージを実行すると、Docker コンテナ内に Jaeger の必要なすべてのコンポーネントがセットアップおよび初期化されます。Jaeger を実行するには、新しい ターミナルウィンドウを開き、プロジェクトのメインフォルダーで次のコマンドを実行します

: Docker コンテナを実行しているターミナルウィンドウを閉じると、コンテナも停止します。コマンドの最後に -d オプションを追加すると、これを回避できます。例: docker-compose up -d

すべてがスムーズに進むと、http://localhost:16686 で Jaeger にアクセスできるようになります。

Jaeger user interface

アプリケーションがまだ Jaeger にトレースを送信していないため、Jaeger UI は空になります。

Jaeger トレースエクスポーターの追加

Jaeger でトレースを表示するには、アプリケーションから Jaeger にトレースを送信する新しいトレースエクスポーターをセットアップする必要があります (コンソールに出力するだけでなく)。

まず、エクスポーターパッケージをプロジェクトにインストールします

次に、エクスポーターを tracing.ts に追加します

ここでは、新しい JaegerExporter を初期化し、トレーサープロバイダーに追加しました。JaegerExporter コンストラクターの endpoint プロパティは、Jaeger がトレースデータをリッスンしている場所を指します。また、コンソールエクスポーターは不要になったため削除しました。

これで、Jaeger でトレースを表示できるようになります。最初のトレースを表示するには

  1. GET /users/random エンドポイントを再度クエリします (curl http://localhost:4000/users/random)。
  2. http://localhost:16686 に移動します。
  3. 左側の 検索 タブの、サービス ドロップダウンで、express-server を選択します。
  4. 検索 タブの下部付近にある トレースを検索 をクリックします。
  5. トレースのリストが表示されるはずです。リストの最初のトレースをクリックします。
  6. トレースの詳細ビューが表示されます。GET /users/random という名前の単一のスパンがあるはずです。スパンをクリックして詳細情報を取得します。
  7. 期間開始時刻 など、トレースに関するさまざまな情報を確認できるはずです。また、複数の タグ も表示されるはずです。そのうちの 1 つは手動で設定しました (http.status)。

Viewing traces inside Jaeger

Prisma クエリのトレースの追加

このセクションでは、データベースクエリをトレースする方法を学びます。最初は、スパンを自分で作成して手動で行います。Prisma では手動トレースはもはや必要ありませんが、手動トレースを実装すると、トレースの仕組みをより深く理解できます。

次に、Prisma の新しい トレース機能 を使用して、同じことを自動的に行います。

Prisma クエリの手動トレース

Prisma クエリを手動でトレースするには、各クエリをスパンでラップする必要があります。server.ts ファイルに次のコードを追加することで、これを行うことができます

Prisma クエリの prisma.user.findmany という新しいスパンを作成しました。また、コードの残りの部分との一貫性を保つために、users 変数の宣言方法にもいくつかの変更を加えました。

GET /users/random エンドポイント (curl http://localhost:4000/users/random) を再度クエリし、Jaeger で新しく生成されたトレースを表示して、新しいスパンをテストします。

Child span visualized in Jaeger

生成されたトレースに、親の GET /users/random スパンの下にネストされた prisma.user.findmany という新しい子スパンがあることがわかります。これで、リクエストのどのくらいの期間が Prisma クエリの実行に費やされたかを確認できます。

手動 vs 自動インストルメンテーション

これまでのところ、トレースをセットアップし、アプリケーションのトレースとスパンを手動で生成する方法を学びました。このようにスパンを手動で定義することを手動インストルメンテーションと呼びます。手動インストルメンテーションを使用すると、アプリケーションのトレース方法を完全に制御できますが、いくつかの欠点があります

  • 特にアプリケーションが大きい場合、アプリケーションを手動でトレースするには非常に時間がかかります。
  • サードパーティライブラリを適切に手動でインストルメント化することが常に可能とは限りません。たとえば、手動インストルメンテーションでは、Prisma の内部コンポーネントの実行をトレースすることはできません。
  • 手動で多くのコードを記述する必要があるため、バグやエラー (不適切なエラー処理、破損したスパンなど) が発生する可能性があります。

幸いなことに、多くのフレームワークとライブラリは自動インストルメンテーションを提供しており、これらのコンポーネントのトレースを自動的に生成できます。自動インストルメンテーションは、コードの変更をほとんどまたはまったく必要とせず、セットアップが非常に迅速で、すぐに使用できる基本的なテレメトリを提供できます。

自動インストルメンテーションと手動インストルメンテーションは、相互に排他的ではないことに注意することが重要です。両方の手法を同時に使用すると有益な場合があります。自動インストルメンテーションは、すべてのエンドポイントで高いカバレッジを備えた優れたベースラインテレメトリを提供できます。その後、特定のきめ細かいトレースとカスタムメトリクス/メタデータに対して手動インストルメンテーションを追加できます。

Prisma の自動インストルメンテーションのセットアップ

このセクションでは、新しいトレース機能を使用して Prisma の自動インストルメンテーションをセットアップする方法を説明します。開始するには、schema.prisma ファイルのジェネレーターブロックでトレース機能フラグを有効にします

: トレースは現在 プレビュー機能 です。そのため、トレースを使用する前に tracing 機能フラグを追加する必要があります。

次に、Prisma Client を再生成します

自動インストルメンテーションを実行するには、npm で 2 つの新しいパッケージをインストールする必要があります

これらのパッケージが必要な理由は次のとおりです

  • @opentelemetry/instrumentation は、自動インストルメンテーションをセットアップするために必要です。
  • @prisma/instrumentation は、Prisma Client の自動インストルメンテーションを提供します。

OpenTelemetry 用語によると、インストルメント化されたライブラリ は、トレースを収集するライブラリまたはパッケージです。一方、インストルメンテーションライブラリ は、特定のインストルメント化されたライブラリのトレースを生成するライブラリです。この場合、Prisma Client がインストルメント化されたライブラリであり、@prisma/instrumentation がインストルメンテーションライブラリです。

次に、Prisma インストルメンテーションを OpenTelemetry に登録する必要があります。これを行うには、tracing.ts ファイルに次のコードを追加します

registerInstrumentations 呼び出しは、次の 2 つの引数を取ります

  • instrumentations は、登録するすべてのインストルメンテーションライブラリの配列を受け入れます。
  • tracerProvider は、トレーサーのトレーサープロバイダーを受け入れます。

自動インストルメンテーションをセットアップしているので、Prisma クエリのスパンを手動で作成する必要はなくなりました。server.ts を更新して、Prisma クエリの手動スパンを削除します

自動インストルメンテーションを使用する場合、トレースを初期化する順序が重要です。インストルメント化されたライブラリをインポートする前に、トレースをセットアップし、インストルメンテーションを登録する必要があります。この場合、initializeTracing 呼び出しは、PrismaClientimport ステートメントの前に来る必要があります。

もう一度、GET /users/random エンドポイントにリクエストを送信し、Jaeger で生成されたトレースを確認します。

Visualization of automatic instrumentation with Prisma

今回は、同じ Prisma クエリが複数のスパンを生成し、クエリに関するはるかにきめ細かい情報を提供します。自動インストルメンテーションを有効にすると、アプリケーションに追加する他のクエリも自動的にトレースを生成します。

注: Prisma によって生成されるスパンの詳細については、トレースドキュメントのトレース出力セクション を参照してください。

Express の自動インストルメンテーションのセットアップ

現在、スパンを手動で作成してエンドポイントをトレースしています。Prisma クエリと同様に、エンドポイントの数が増えるにつれて、手動トレースは管理できなくなります。この問題に対処するために、Express の自動インストルメンテーションもセットアップできます。

まず、次のインストルメンテーションライブラリをインストールします

tracing.ts 内で、これら 2 つの新しいインストルメンテーションライブラリを登録します

最後に、server.tsGET /users/random エンドポイントの手動スパンを削除します

GET /users/random エンドポイントにリクエストを送信し、Jaeger で生成されたトレースを確認します。

Visualization of automatic instrumentation with Express and Prisma

リクエストがコードを通過するさまざまなステップを示す、はるかにきめ細かいスパンが表示されるはずです。特に、リクエストがさまざまな Express ミドルウェアと GET /users/random リクエストハンドラーを通過することを示す ExpressInstrumentation ライブラリによって生成された新しいスパンが表示されるはずです。

: 利用可能なインストルメンテーションライブラリのリストについては、OpenTelemetry レジストリ を確認してください。

トレースのパフォーマンスへの影響の軽減

アプリケーションが大量のスパンをコレクター (Jaeger など) に送信している場合、アプリケーションのパフォーマンスに大きな影響を与える可能性があります。これは通常、開発環境では問題になりませんが、本番環境では問題になる可能性があります。これを軽減するためにいくつかの手順を実行できます。

トレースの一括送信

現在、SimpleSpanProcessor を使用してトレースを送信しています。これは、スパンを一度に 1 つずつ送信するため、非効率です。代わりに、BatchSpanProcessor を使用してスパンをバッチで送信できます。

本番環境で BatchSpanProcessor を使用するには、tracing.ts ファイルで次の変更を行います

開発環境では、パフォーマンスの最適化はそれほど重要ではないため、依然としてSimpleSpanProcessorを使用していることに注意してください。これにより、開発中にトレースが生成されるとすぐに表示されるようになります。

サンプリングで送信するスパンを減らす

確率サンプリングは、OpenTelemetryトレースユーザーがランダム化されたサンプリング手法を使用して、スパン収集のパフォーマンスコストを削減できるようにする手法です。この手法を使用すると、コレクターに送信されるスパンの数を減らしながら、アプリケーションで何が起こっているかを適切に把握できます。

tracing.tsを更新して確率サンプリングを使用する

バッチ処理と同様に、確率サンプリングは本番環境でのみ組み込んでいます。

まとめと最終的な注意

おめでとうございます! 🎉

このチュートリアルでは、以下を学びました。

  • トレースとは何か、そしてなぜそれを使用する必要があるのか。
  • OpenTelemetryとは何か、そしてトレースとどのように関係しているのか。
  • Jaegerを使用してトレースを視覚化する方法。
  • 既存のWebアプリケーションにトレースを統合する方法。
  • 自動計測ライブラリを使用してコードの可観測性を向上させる方法。
  • 本番環境でのトレースのパフォーマンスへの影響を軽減する方法。

このプロジェクトのソースコードはGitHubにあります。問題に気づいた場合は、リポジトリに issue を立てるか、PR を送信してください。また、Twitterで直接連絡することもできます。

次の投稿をお見逃しなく!

Prismaニュースレターに登録する