トレース機能は、アプリケーションのパフォーマンスを分析し、ボトルネックを特定するための強力なツールです。このチュートリアルでは、トレース機能の核心概念と、OpenTelemetry および Prisma のトレース機能を使用してアプリケーションにトレース機能を統合する方法を説明します。
目次
- はじめに
- 前提条件
- リポジトリのクローン
- アプリケーションへのトレース機能の統合
- Jaeger でトレースを可視化する
- Prisma クエリのトレースを追加する
- Express の自動インスツルメンテーションのセットアップ
- トレース機能によるパフォーマンス影響の軽減
- まとめと最終コメント
はじめに
このチュートリアルでは、Prisma と Express を使用して構築された既存のウェブアプリケーションにトレース機能を統合する方法を学びます。OpenTelemetry は、トレースやその他のテレメトリーデータ(ログ、メトリクスなど)を収集するためのベンダー中立な標準であり、これを使用してトレース機能を実装します。
まず、HTTP エンドポイントの手動トレースを作成し、それらをコンソールに出力します。次に、Jaeger を使用してトレースを可視化する方法を学びます。また、Prisma のトレース機能を使用して、データベースクエリのトレースを自動的に生成する方法も学びます。最後に、自動インスツルメンテーションと、トレース機能使用時のパフォーマンスに関する考慮事項について学びます。
トレース機能とは?
トレース機能は、リクエストがアプリケーションを伝播する際にたどるパスを記録する可観測性ツールです。トレースは、特定の要求に応じてシステムが実行しているアクティビティを関連付けるのに役立ちます。また、トレースはこれらのアクティビティに関するタイミング情報(開始時間、継続時間など)も提供します。
1つのトレースは、ユーザーまたはアプリケーションによってリクエストが行われたときに何が起こるかについての情報を提供します。各トレースは1つ以上のスパンで構成されており、これらはリクエスト中に発生する単一のステップまたはタスクに関する情報を含んでいます。
Jaeger のようなトレースツールを使用すると、トレースは以下のような図として可視化できます。
単一のスパンは複数の子スパンを持つことができ、これらは親スパン中に発生するサブタスクを表します。例えば、上記の図では、PRISMA QUERY スパンには PRISMA ENGINE と呼ばれる子スパンがあります。最上位のスパンはルートスパンと呼ばれ、最初から最後までトレース全体を表します。上記の図では、GET /ENDPOINT がルートスパンです。
トレース機能は、システムをより深く理解し、可視性を得るための素晴らしい方法です。これにより、アプリケーションに影響を与えているエラーやパフォーマンスのボトルネックを正確に特定できます。トレース機能は、各リクエストが複数のサービスに関与し、特定の問題をローカルで再現することが難しい分散システムをデバッグする際に特に役立ちます。
注: システムの可観測性を高めるために、トレース機能は メトリクスと組み合わせて使用されることがよくあります。メトリクスについてさらに学ぶには、私たちのメトリクスチュートリアルをご覧ください。
使用する技術
このチュートリアルでは、以下のツールを使用します。
- トレースライブラリ/API として OpenTelemetry
- オブジェクト関係マッパー (ORM) として Prisma
- データベースとして SQLite
- トレース可視化ツールとして Jaeger
- ウェブフレームワークとして Express
- プログラミング言語として TypeScript
前提条件
想定される知識
これは初心者向けのチュートリアルですが、以下の知識を前提としています。
- JavaScript または TypeScript の基本的な知識 (推奨)
- バックエンドのウェブ開発の基本的な知識
注: このチュートリアルは、トレース機能と可観測性に関する事前の知識がないことを前提としています。
開発環境
このチュートリアルを進めるには、以下が必要です。
- ... Node.js がインストールされていること。
- ... Docker および Docker Compose がインストールされていること。
- ... 任意で Prisma VS Code Extension がインストールされていること。Prisma VS Code Extension は、Prisma の非常に優れた IntelliSense とシンタックスハイライトを追加します。
- ... 任意で Unix シェル(Linux および macOS のターミナル/シェルなど)にアクセスできること、このシリーズで提供されるコマンドを実行するために。
Unix シェルがない場合(例えば、Windows マシンを使用している場合)でもチュートリアルを進めることはできますが、シェルコマンドを自分のマシンに合わせて変更する必要があるかもしれません。
リポジトリのクローン
トレース機能のデモンストレーションにはウェブアプリケーションが必要です。このチュートリアルのために構築した既存の Express ウェブアプリケーションを使用できます。
開始するには、以下の操作を実行してください。
- リポジトリをクローンする
- クローンしたディレクトリに移動する
- 依存関係をインストールする
prisma/migrations
ディレクトリからデータベースマイグレーションを適用します。
注: このコマンドは、Prisma Client の生成とデータベースのシーディングも行います。
- プロジェクトを開始する
注: アプリケーションを開発中はサーバーを起動したままにしてください。コードに変更があるたびに、
dev
スクリプトがサーバーを再起動します。
アプリケーションには、https://: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
関数はいくつかのことを行います。
- これは、トレーサープロバイダーを初期化します。これは、トレーサーを作成するために使用されます。トレーサーは、アプリケーション内でトレース/スパンを作成します。
- トレースエクスポーターを定義し、それをプロバイダーに追加します。トレースエクスポーターは、さまざまな宛先にトレースを送信します。この場合、
ConsoleSpanExporter
はトレースをコンソールに出力します。 .register()
関数を呼び出すことにより、OpenTelemetry API で使用するためにプロバイダーを登録します。- 最後に、関数に引数として渡された名前を持つトレーサーを作成して返します。
次に、既存の server.ts
で initializeTracing
をインポートして呼び出します。
これで、最初のトレースを作成する準備ができました!
最初のトレースを作成する
前のセクションでは、トレース機能を初期化し、トレーサーをサーバーにインポートしました。これで、tracer
オブジェクトを使用してサーバー内にスパンを作成できます。まず、GET /users/random
リクエストをカプセル化するトレースを作成します。リクエストハンドラーの定義を次のように更新します。
ここでは、startActiveSpan()
を使用して新しいスパンを作成し、提供されるコールバック関数内にすべてのリクエストハンドラーロジックを囲んでいます。コールバック関数には、requestSpan
と名付けた span
オブジェクトへの参照が含まれています。それを使用して、スパンの属性を変更または追加できます。このコードでは、リクエストの結果に基づいて http.status
と呼ばれる属性をスパンに設定します。最後に、リクエストが処理されたら、スパンを終了します。
新しく作成されたスパンを見るには、https://: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
ファイルを作成します。
ファイル内に以下のサービスを定義します。
このイメージを実行すると、Jaeger の必要なすべてのコンポーネントが Docker コンテナ内にセットアップされ、初期化されます。Jaeger を実行するには、新しいターミナルウィンドウを開き、プロジェクトのメインフォルダーで以下のコマンドを実行します。
注: Docker コンテナを実行しているターミナルウィンドウを閉じると、コンテナも停止します。これを避けるには、コマンドの末尾に
-d
オプションを追加します(例:docker-compose up -d
)。
すべてがスムーズに進めば、https://:16686 で Jaeger にアクセスできるはずです。
アプリケーションがまだ Jaeger にトレースを送信していないため、Jaeger UI は空です。
Jaeger トレースエクスポーターの追加
Jaeger でトレースを見るには、アプリケーションから Jaeger にトレースを送信する新しいトレースエクスポーターをセットアップする必要があります(コンソールに表示するだけでなく)。
まず、プロジェクトにエクスポーターパッケージをインストールします。
次に、tracing.ts
にエクスポーターを追加します。
ここでは、新しい JaegerExporter
を初期化し、それをトレーサープロバイダーに追加しました。JaegerExporter
コンストラクターの endpoint
プロパティは、Jaeger がトレースデータをリッスンしている場所を指します。不要になったため、コンソールエクスポーターも削除しました。
これで、Jaeger でトレースを見ることができるはずです。最初のトレースを見るには
GET /users/random
エンドポイントを再度クエリします(curl https://:4000/users/random
)。- https://:16686 にアクセスします。
- 左側の Search タブで、Service ドロップダウンから express-server を選択します。
- Search タブの下部にある Find Traces をクリックします。
- これでトレースのリストが表示されるはずです。リストの最初のトレースをクリックします。
- トレースの詳細ビューが表示されます。GET /users/random と呼ばれる単一のスパンがあるはずです。スパンをクリックすると、より詳細な情報が得られます。
- トレースに関するさまざまな情報、例えば Duration(継続時間)や Start Time(開始時間)を見ることができるはずです。また、複数の Tags(タグ)も表示されるはずで、そのうちの1つは手動で設定したものです(
http.status
)。
Prisma クエリのトレースを追加する
このセクションでは、データベースクエリをトレースする方法を学びます。まず、スパンを自分で作成することにより、手動でこれを行います。Prisma では手動トレースはもはや必要ありませんが、手動トレースを実装することで、トレース機能の動作をよりよく理解できます。
その後、Prisma の新しいトレース機能を使用して、同じことを自動的に行います。
Prisma クエリを手動でトレースする
Prisma クエリを手動でトレースするには、各クエリをスパンでラップする必要があります。これを行うには、以下のコードを server.ts
ファイルに追加します。
Prisma クエリ用に prisma.user.findmany
という新しいスパンを作成しました。また、users
変数の宣言方法にもいくつかの変更を加え、残りのコードとの一貫性を保っています。
GET /users/random
エンドポイントを再度クエリし(curl https://:4000/users/random
)、Jaeger で新しく生成されたトレースを表示して、新しいスパンをテストします。
生成されたトレースには、親の GET /users/random
スパンの下にネストされた prisma.user.findmany
という新しい子スパンがあるはずです。これで、リクエストの期間のうち Prisma クエリの実行にどれくらいの時間が費やされたかを確認できます。
手動と自動インスツルメンテーション
これまで、トレース機能をセットアップし、アプリケーションの手動トレースとスパンを生成する方法を学びました。このように手動でスパンを定義することを手動インスツルメンテーションと呼びます。手動インスツルメンテーションは、アプリケーションのトレース方法を完全に制御できますが、いくつかの欠点があります。
- アプリケーションを手動でトレースするのは非常に時間がかかります。特にアプリケーションが大きい場合はそうです。
- サードパーティライブラリを適切に手動でインスツルメントできるとは限りません。例えば、Prisma の内部コンポーネントの実行を手動インスツルメンテーションでトレースすることはできません。
- 手動で多くのコードを書くことになるため、バグやエラー(不適切なエラー処理、壊れたスパンなど)につながる可能性があります。
幸いなことに、多くのフレームワークとライブラリは自動インスツルメンテーションを提供しており、それらのコンポーネントのトレースを自動的に生成できます。自動インスツルメンテーションは、コードの変更がほとんど不要で、セットアップが非常に迅速であり、すぐに基本的なテレメトリーを提供できます。
自動インスツルメンテーションと手動インスツルメンテーションは相互に排他的ではないことに注意することが重要です。両方の手法を同時に使用すると有益な場合があります。自動インスツルメンテーションは、すべてのエンドポイントで高いカバレッジを持つ優れたベースラインテレメトリーを提供できます。その後、特定かつ詳細なトレースとカスタムメトリクス/メタデータのために手動インスツルメンテーションを追加できます。
Prisma の自動インスツルメンテーションのセットアップ
このセクションでは、新しいトレース機能を使用して Prisma の自動インスツルメンテーションをセットアップする方法を説明します。開始するには、schema.prisma
ファイルの generator ブロックでトレース機能フラグを有効にします。
注: トレース機能は現在、プレビュー機能です。そのため、トレース機能を使用する前に
tracing
機能フラグを追加する必要があります。
次に、Prisma Client を再生成します。
自動インスツルメンテーションを実行するには、npm
で2つの新しいパッケージもインストールする必要があります。
これらのパッケージが必要な理由は次のとおりです。
@opentelemetry/instrumentation
は、自動インスツルメンテーションをセットアップするために必要です。@prisma/instrumentation
は、Prisma Client の自動インスツルメンテーションを提供します。
OpenTelemetry の用語によれば、インスツルメンテッドライブラリは、トレースを収集する対象となるライブラリまたはパッケージです。一方、インスツルメンテーションライブラリは、特定のインスツルメンテッドライブラリのトレースを生成するライブラリです。この場合、Prisma Client がインスツルメンテッドライブラリであり、@prisma/instrumentation
がインスツルメンテーションライブラリです。
次に、OpenTelemetry に Prisma Instrumentation を登録する必要があります。これを行うには、以下のコードを tracing.ts
ファイルに追加します。
registerInstrumentations
の呼び出しは2つの引数を取ります。
instrumentations
は、登録したいすべてのインスツルメンテーションライブラリの配列を受け取ります。tracerProvider
は、あなたのトレーサーのトレーサープロバイダーを受け取ります。
自動インスツルメンテーションをセットアップしているため、Prisma クエリのスパンを手動で作成する必要はなくなります。server.ts
を更新し、Prisma クエリの手動スパンを削除します。
自動インスツルメンテーションを使用する場合、トレース機能を初期化する順序が重要です。インスツルメンテッドライブラリをインポートする前に、トレース機能とインスツルメンテーションをセットアップし、登録する必要があります。この場合、initializeTracing
の呼び出しは PrismaClient
の import
ステートメントよりも前に来る必要があります。
もう一度、GET /users/random
エンドポイントにリクエストを送信し、Jaeger で生成されたトレースを確認してください。
今回は、同じ Prisma クエリが複数のスパンを生成し、クエリに関するはるかにきめ細かい情報を提供します。自動インスツルメンテーションが有効になっている場合、アプリケーションに追加する他のクエリも自動的にトレースを生成します。
注: Prisma によって生成されるスパンの詳細については、トレースドキュメントのトレース出力セクションを参照してください。
Express の自動インスツルメンテーションのセットアップ
現在、手動でスパンを作成してエンドポイントをトレースしています。Prisma クエリと同様に、エンドポイントの数が増えるにつれて手動トレースは管理不能になります。この問題に対処するため、Express の自動インスツルメンテーションもセットアップできます。
以下のインスツルメンテーションライブラリをインストールして開始します。
tracing.ts
内で、これら2つの新しいインスツルメンテーションライブラリを登録します。
最後に、server.ts
内の GET /users/random
エンドポイントの手動スパンを削除します。
GET /users/random
エンドポイントにリクエストを送信し、Jaeger で生成されたトレースを確認してください。
コードをリクエストが通過するにつれて、さまざまなステップを示すはるかにきめ細かいスパンが表示されるはずです。特に、さまざまな Express ミドルウェアと GET /users/random
リクエストハンドラーをリクエストが通過する様子を示す、ExpressInstrumentation
ライブラリによって生成された新しいスパンが表示されるはずです。
注: 利用可能なインスツルメンテーションライブラリのリストについては、OpenTelemetry Registry を確認してください。
トレース機能によるパフォーマンス影響の軽減
アプリケーションが多数のスパンをコレクター(Jaegerなど)に送信している場合、アプリケーションのパフォーマンスに大きな影響を与える可能性があります。これは通常、開発環境では問題になりませんが、本番環境では問題になる可能性があります。これを軽減するためにいくつかの対策を講じることができます。
トレースを一括で送信する
現在、SimpleSpanProcessor
を使用してトレースを送信しています。これは、スパンを一度に1つずつ送信するため非効率です。代わりに、BatchSpanProcessor
を使用してスパンを一括で送信できます。
tracing.ts
ファイルで以下の変更を行い、本番環境で BatchSpanProcessor
を使用します。
開発環境では、パフォーマンスの最適化が大きな懸念事項ではないため、引き続き SimpleSpanProcessor
を使用していることに注意してください。これにより、開発環境でトレースが生成されるとすぐに表示されることが保証されます。
サンプリングによるスパン数の削減
確率的サンプリングは、OpenTelemetry トレースユーザーがランダム化されたサンプリング技術を使用することで、スパン収集のパフォーマンスコストを削減できる手法です。この技術を使用すると、コレクターに送信されるスパンの数を減らしながらも、アプリケーションで何が起こっているかを適切に把握できます。
tracing.ts
を更新して確率的サンプリングを使用します。
バッチ処理と同様に、確率的サンプリングも本番環境のみで組み込みます。
まとめと最終コメント
おめでとうございます!🎉
このチュートリアルでは、以下を学びました。
- トレース機能とは何か、そしてなぜそれを使用すべきなのか。
- OpenTelemetry とは何か、そしてそれがトレース機能とどのように関連しているか。
- Jaeger を使用してトレースを可視化する方法。
- 既存のウェブアプリケーションにトレース機能を統合する方法。
- コードの可観測性を向上させるために自動インスツルメンテーションライブラリを使用する方法。
- 本番環境でトレース機能によるパフォーマンスへの影響を軽減する方法。
このプロジェクトのソースコードは GitHub で見つけることができます。問題に気づいた場合は、遠慮なくリポジトリで Issue を開くか、PR を提出してください。また、Twitter で直接私に連絡することもできます。
次の投稿をお見逃しなく!
Prisma ニュースレターに登録する