エンジニアリング

Elastic RUM(リアルユーザー監視)の概要

RUMと聞いて、ラム酒で作られた素敵なカクテルを一杯飲みたいという気分になったでしょうか。残念ながら、私が話しているRUMはラム酒のことではありません。ただし、Elastic RUMも同じように素敵なものです。早速、見ていきましょう。このブログで詳細な内容を説明するには時間が必要なため、少し長くなることにご注意ください。

RUMとは

Elasticのリアルユーザー監視(real user monitoring、RUM)は、Webブラウザでのユーザーインタラクションをキャプチャし、パフォーマンスの観点から、Webアプリケーションの「リアルユーザーエクスペリエンス」の詳細なビューを提供します。ElasticのRUMエージェントは、任意のJavaScriptベースアプリケーションをサポートするJavaScriptエージェントです。RUMは、アプリケーションに関する貴重なインサイトを提供します。RUMの一般的な利点には次のようなものがあります。

  • RUMのパフォーマンスデータは、ボトルネックの特定と、サイトのパフォーマンスに関する問題が訪問者のエクスペリエンスにどのような影響を与えるかを把握するために役立ちます。
  • RUMによってキャプチャされたユーザーエージェント情報により、お客様が最もよく使用しているブラウザ、デバイス、プラットフォームを特定することができ、その情報を基にしてアプリケーションを最適化することができます。
  • RUMによって収集した各ユーザーのパフォーマンスデータを、場所の情報とともに使用することで、世界中の各地域でのWebサイトのパフォーマンスを把握できます。
  • RUMは、アプリケーションのサービスレベル契約(SLA)に関するインサイトおよび計測結果を提供します。
  • RUMによって収集したお客様の訪問やクリック数に関する時系列情報は、開発チームが新しい機能のインパクトを特定するのに役立ちます。

Elastic APMでRUMを開始する

このブログでは、ReactフロントエンドとSpring BootバックエンドのシンプルなWebアプリケーションのインストルメントプロセス全体を手順ごとに説明します。そうすることで、RUMエージェントが簡単に使用できることが分かるようになります。またボーナスとして、Elastic APMでフロントエンドおよびバックエンドのパフォーマンス情報を1つの包括的な分散トレースビューにまとめる方法についても説明します。詳細についてご興味がある場合は、Elastic APMと分散トレーシングの概要を説明した前回のブログをご覧ください。

Elastic APMリアルユーザー監視を使用するには、APMサーバーがインストールされたElastic Stackが必要です。もちろん、最新のElastic StackとAPMサーバーをダウンロードして、コンピューターでローカルにインストールすることもできますが、最も簡単なアプローチは、 Elastic Cloudのトライアルアカウントを作成することです。この場合は数分でクラスターを準備できます。APMはデフォルトのI/O最適化テンプレートで有効化されます。以降の説明は、クラスターが準備できているものとして進めます。

サンプルアプリケーション

インストルメントするアプリケーションは、Reactフロントエンドと、インメモリの自動車データベースへのAPIアクセスを提供するSpring Bootバックエンドで構成される、シンプルな自動車データベースアプリケーションです。このアプリケーションは意図的にシンプルにしてあります。以下と同じ手順に従ってご自身のアプリケーションをインストルメントできるように、詳細なインストルメンテーション手順をゼロから示していきます。

A simple application with a React frontend and Spring backend

ラップトップの任意の場所に、「CarApp」という名前のディレクトリを作成します。そして、そのディレクトリに、フロントエンドおよびバックエンドアプリケーションの両方のクローンを作成します。

git clone https://github.com/carlyrichmond/carfront
git clone https://github.com/carlyrichmond/cardatabase

ご覧のとおり、アプリケーションはきわめてシンプルです。Reactフロントエンドにいくつかのコンポーネントがあり、バックエンドのSpring Bootアプリケーションにいくつかのクラスがあるのみです。フロントエンドとバックエンドの両方について、GitHubのインストラクションに従ってアプリケーションを構築し、実行すると、次のように表示されるはずです。ブラウズ、自動車のフィルター、およびデータに対するCRUDオプションの実行ができます。

The simple React user interface

このようにアプリケーションを実行すると、RUMエージェントを使用したインストルメンテーションの準備が整ったことになります。

RUMですぐに使える豊富なインストルメンテーション機能

この機能を利用するには、Elastic APMサーバーが必要です。さらに、APMサーバーでRUMを有効にして、RUMエージェントからイベントをキャプチャできるようにしておく必要があります。RUMエージェントを設定する方法は2つあります。

  1. npmなどのパッケージマネージャーを使用し、RUMエージェントをプロジェクトの依存関係としてインストールする。
    npm install @elastic/apm-rum --save
  2. HTMLのscriptタグにRUMエージェントを含める。こちらはドキュメントのとおり、他のリソースをブロックする形とブロックしない形の2種類があります
    <script 
    src="https://unpkg.com/@elastic/apm-rum@5.12.0/dist/bundles/elastic-apm-rum.umd.min.js">
    </script>
    <script>
    elasticApm.init({
    serviceName: 'carfront',
    serverUrl: 'http://localhost:8200',
    serviceVersion: '0.90'
    })
    </script>

ここでは、フロントエンドがReactアプリケーションであるため、上記1のアプローチを使用します。@elastic/apm-rumをプロジェクトにインストールしたら、rum.jsで初期化コードを確認してください。初期化コードはindex.jsと同じディレクトリ内にあり、その中身は以下のようになっています。ただし、serverUrlについては、ユーザー独自のAPMサーバーエンドポイントになります。

import { init as initApm } from '@elastic/apm-rum'
var apm = initApm({
//必要なサービス名を設定します(使用可能な文字はa-z、A-Z、0-9、-、_、スペースです)
serviceName: 'carfront',
// アプリケーションのバージョンを設定します
// APMサーバーが適切なソースマップを探す際に使用します
serviceVersion:'0.90',
// カスタムのAPMサーバーURLを設定します(デフォルト:http://localhost:8200)
serverUrl:'APM_URL',
// distributedTracingOrigins: ['http://localhost:8080'],
})
export default apm;

RUMエージェントの初期化に必要な手順はこれだけです。React、Angular、Vueにおけるルーティングのような、フレームワーク固有の機能を利用する場合は、フレームワーク固有の統合機能群もインストールし、構成した方がいいでしょう。これについては、こちらのドキュメントで取り上げています。今回の例は、React固有のインストルメンテーションを必要としないシングルページであるため、追加の依存関係はインストールしていません。

distributedTracingOriginsについては、今は心配しなくて大丈夫です。それ以外の構成の簡単な説明は以下のとおりです。

  1. サービス名:サービス名を設定する必要があります。APM UIでアプリケーションを表します。意味のある名前にしましょう。
  2. サービスバージョン:アプリケーションのバージョンです。このバージョン番号は、適切なソースマップを見つけるためにAPMサーバーでも使用されます。ソースマップの詳細についてはのちほど説明します。
  3. サーバーURL:APMサーバーのURLです。APMサーバーURLは通常、公共インターネットからアクセスできることに注意してください。RUMエージェントはインターネット上のエンドユーザーのブラウザから、データをAPMサーバーにレポートするからです。

Elastic APMバックエンドエージェントについて精通している方は、ここでAPMトークンが渡されていないことを不思議に思うかもしれません。その理由は、RUMエージェントが実際には秘密のAPMトークンを使用していないからです。トークンはバックエンドエージェントでのみ使用されます。フロントエンドコードはパブリックであり、秘密のトークンでは追加のセキュリティは提供されないからです。

次に、アプリケーションがロードされるときにこのJavaScriptファイルをロードし、それを、カスタムインストルメンテーションを実行する場所に含めます。ここでは、カスタムインストルメンテーションを含める前の、何も設定を追加していない状態から見ておきましょう。そのためには、rum.jsをindex.jsに含める必要があるだけです。index.jsファイルはrum.jsをインポートして、ページロード名を設定します。ページロード名を設定しない場合、APM UIにページロードが「/」としてリストされるため、分かりにくくなってしまいます。index.jsは次のようになります。

import apm from './rum'
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
apm.setInitialPageLoadName("Car List")
ReactDOM.render(, document.getElementById('root'));
serviceWorker.unregister();

ページにアクセスし、自動車を追加または削除してアプリケーションにトラフィックを発生させます。次に、Kibanaにログインして[Observability](オブザーバビリティ)タイルをクリックします。続いて、以下に示すように[APM]サブメニューから[Services]オプションを選択します。

「carfront」という名前のサービスがリストされているはずです。そのサービス名をクリックすると、トランザクションページが表示されます。デフォルトの時間範囲である[Last 15 minutes](過去15分間)について、レイテンシーやスループットなどのメトリックの概要が表示されるはずです。 表示されない場合は、タイムピッカーをこの時間範囲に変更してください。

[Transactions]セグメントには、[Car List]というトランザクションが表示されているはずです。[Car List]のリンクをクリックすると、このサンプルトランザクションに関わる統計を確認できる[Transactions]タブに移動します。ページの一番下までスクロールすると、次のようにブラウザインタラクションのウォーターフォールビューが表示されます。

RUMエージェントによってデフォルトでキャプチャされている情報量に驚きませんか?特に、トップに表示されるtimeToFirstByte、domInteractive、 domComplete、およびfirstContentfulPaintなどのマーカーに注目しましょう。黒の点にマウスを合わせて名前を見ます。コンテンツの取得およびそれらのコンテンツのブラウザレンダリングについて、きわめて詳細な情報が提示されます。また、ブラウザからのリソースのローディングに関するすべてのパフォーマンスデータについても注目してください。カスタムインストルメンテーションなしで、RUMエージェントを初期化するだけで、これらすべての詳細なパフォーマンスメトリックを入手することができます。パフォーマンスに問題がある場合、その問題の原因はバックエンドサービスが遅いからなのか、ネットワークが遅いからなのか、または単にクライアントブラウザが遅いからなのかが、これらのメトリックによって簡単に判断できるため、非常に便利です。

確認が必要な方のために、以下にWebパフォーマンスのメトリックについて簡単に説明します。注意が必要なのは、Reactのような最新のWebアプリケーションフレームワークにおいて、これらのメトリックはWebページの「静的」な部分を表すだけの場合があることです。Reactは非同期だからです。たとえば、動的コンテンツの場合、domInteractiveの後にもまだロードされる可能性があります(のちほど説明します)。

  • timeToFirstByte:情報をリクエストした後、Webサーバーから最初の情報を受信するまでの待機時間です。これは、ネットワークおよびサーバーサイドの処理スピードの組み合わせとなります。
  • domInteractive:ユーザーエージェントが現在のドキュメントの準備状況を「interactive」(インタラクティブ)に設定する直前の時間です。そのように設定されたということは、ブラウザですべてのHTMLおよびDOM構造のパースが完了したことを意味します。
  • domComplete: ユーザーエージェントが現在のドキュメントの準備状況を「complete」(完了)に設定する直前の時間です。そのように設定されたということは、ページおよびそれらのサブリソース(画像など)のダウンロードが完了し、準備が整ったことを意味します。ローディングのスピナーが停止します。
  • firstContentfulPaint:DOMからの最初のコンテンツをブラウザがレンダリングする時間です。これは、ページが実際にロードされていることのフィードバックとなるため、ユーザーにとって重要なマイルストーンです。

柔軟なカスタムインストルメンテーション

すでに見たとおり、RUMエージェントは、何も設定しない状態で、 ブラウザインタラクションについての詳細なインストルメンテーションを提供します。必要な場合は、カスタムインストルメンテーションを実行することもできます。たとえば、Reactアプリケーションはシングルページアプリケーションのため、自動車を削除しても「ページロード」はトリガーされず、RUMはデフォルトではこの自動車の削除に関するパフォーマンスデータをキャプチャすることはありません。このような場合にカスタムトランザクションを使用することができます。

最新のリリース(APM Real User Monitoring JavaScriptエージェント5.x)では、AJAXコールおよびクリックイベントがエージェントによってキャプチャされ、APMサーバーに送信されます。インタラクションのタイプは、disableInstrumentation設定を使用して構成できます

独自のカスタムインストルメンテーションを追加して意味のあるトレースを増やすことも可能です。これは、新しい機能をトレースするのに特に便利です。今回のサンプルアプリケーションでは、フロントエンドアプリケーションの[New Car]ボタンにより、データベースに新しい自動車を追加することができます。自動車の新たな追加に関するパフォーマンスをキャプチャするために、そのコードをインストルメントします。コンポーネントのディレクトリでCarlist.jsファイルを開きます。次のようなコードがあります。

//新しい自動車を追加します
addCar(car) {
// RUMクリックトランザクションにラベルとして自動車のメタデータを追加します
var transaction = apm.startTransaction("Add Car", "Car");
transaction.addLabels(car);
fetch(SERVER_URL + 'api/cars',
{
method:'POST',
headers: {
'Content-Type': 'application/json',
},
body:JSON.stringify(car)
})
.then(res => this.fetchCars())
.catch(err => console.error(err))
}
fetchCars = () => {
fetch(SERVER_URL + 'api/cars')
.then((response) => response.json())
.then((responseData) => {
this.setState({
cars: responseData._embedded.cars,
});
})
.catch(err => console.error(err));
// 現在のトランザクションを応答コールバックの最後で終了させます
var transaction = apm.getCurrentTransaction()
if (transaction) transaction.end()
}

このコードの内容をおおまかに説明すると、まず「Car」タイプの「Add Car」という名前の新しいトランザクションを作成しています。次に、コンテキスト情報を提供するために、自動車にトランザクションがタグ付けされます。そして、メソッドの最後でトランザクションを明示的に終了しています。

アプリケーションWeb UIから新たに自動車を追加します。KibanaのAPM UIをクリックします。「Add Car」トランザクションがリストされているはずです。[Filter by type]ドロップダウンで[Car]を選択していることを確認します。デフォルトでは「page-load」トランザクションが表示されます。

[Add Car]トランザクションリンクをクリックします。カスタムトランザクション「Add Car」のパフォーマンス情報が表示されるはずです。

[Metadata](メタデータ)タブをクリックします。エージェントによってキャプチャしたデフォルトのラベルと並んで、今回追加したラベルが確認できます。ラベルとログは、APMトレースに貴重なコンテキスト情報を追加します。

カスタムインストルメンテーションを実行するために必要な作業はこれだけです。作業は簡単ですがその機能は強力です。詳細に関しては、APIドキュメントを参照してください。

ユーザーエクスペリエンスのダッシュボード

Elastic APMは、洗練されたAPM UIと内蔵のAPMダッシュボードを提供します。これにより、何も設定を加えていない状態のエージェントによってキャプチャされたすべてのAPMデータを可視化できます。

このほか、Elastic内に独自のカスタムビジュアライゼーションを作成することもできます。これには、APMデータをエンリッチおよび変換するためのインジェストノードパイプラインを使用します。たとえば、RUMエージェントによってキャプチャされたユーザーIPおよびユーザーエージェントデータは、お客様に関するきわめて豊富な情報となります。ユーザーIPとユーザーエージェントに関するすべての情報があれば、以下のようなビジュアライゼーションを作成することが可能です。以下では、Webトラフィックがどこから来ているのかが地図上に示されており、また、お客様がどのオペレーティングシステムおよびブラウザを使用しているかが示されています。

もっとも、ユーザーに関する重要なデータの多くは、Elasticオブザーバビリティのユーザーエクスペリエンスダッシュボードで見ることができます。ビジュアライゼーションのサンプルは以下のとおりです。

分散トレーシングで全体像を見る

ボーナスポイントとして、ここでバックエンドのSpring Bootアプリケーションもインストルメントします。そうすることで、Webブラウザからバックエンドデータベースまでの全体のトランザクションを1つのビューで包括的に見ることができます。Elastic APMの分散トレーシングならそれが可能です。

RUMエージェントで分散トレーシングを構成

分散トレーシングは、RUMエージェントでデフォルトで有効化されます。ただし、同じオリジンに対して行われたリクエストのみが含まれます。オリジン間リクエストを含めるためには、distributedTracingOrigins構成オプションを設定する必要があります。また、バックエンドアプリケーションにCORSポリシーを設定する必要もあります。これについては次のセクションで説明します。

ここでのアプリケーションでは、フロントエンドは http://localhost:3000から提供されます。http://localhost:8080へのリクエストを含めるためには、distributedtracingOrigins構成をReactアプリケーションに追加する必要があります。これはrum.js内でできます。すでにそこにコードがあります。単にコメントアウトを解除するだけです。

var apm = initApm({
...
distributedTracingOrigins: ['http://localhost:8080']
})

新しいバージョンのエージェントでは、http://localhost:8080に対するリクエストにW3C Trace Context仕様とtraceparentヘッダーが実装されています。これに対して、以前はリクエストにカスタムヘッダー(elastic-apm-traceparent)を追加する必要がありました。

最新バージョンのドキュメントによると、サーバーサイドのインストルメンテーションの構成方法には以下の3種類があります。

  1. apm-agent-attach-cli.jarを使用して、実行中のJVMに自動的にアタッチする。
  2. apm-agent-attachを使用し、プログラムを記述して設定する。この場合、Javaアプリケーションのコードを変更する必要があります。
  3. -javaagentフラグを使用して手動で設定する。以下の例では、この方法を使用します。

サーバーサイドで手動設定のインストルメンテーションアプローチを使用するには、Javaエージェントをダウンロードし、それとともにアプリケーションを起動する必要があります。お好みのIDEで、ローンチ構成に以下のvmArgsを追加してください。

-javaagent:apm/wrapper/elastic-apm-agent-1.33.0.jar 
-Delastic.apm.service_name=cardatabase
-Delastic.apm.application_packages=com.packt.cardatabase
-Delastic.apm.server_urls=
-Delastic.apm.secret_token=

Elastic Cloudを使用している場合、RUMエージェントとAPMエージェントの構成の全容はデプロイのAPM統合で確認できます。例は以下のとおりです。

エージェントを構成する場所は、お好きなIDEでかまいません。以下のスクリーンショットは、私のVS CodeでSpring Bootアプリケーションのローンチ構成を開いて撮影したものです。

では、ブラウザで自動車のリスト(car list)を更新して新たにリクエストを生成します。Kibana APM UIにアクセスし、最新の「car list」ページロードを確認します。以下のスクリーンショットに示したように、Javaメソッドの呼び出しを含めた完全なトレースが表示されます。

ご覧のとおり、JDBCアクセスを含む、ブラウザからのクライアントサイドのパフォーマンスデータとサーバーサイドのパフォーマンスデータがすべて、1つの分散トレースできれいに表示されます。異なる色が、分散トレースの異なる部分に使用されています。これは、アプリケーションとエージェントを起動しているだけであって、サーバーサイドで何もカスタムインストルメンテーションを追加していないデフォルトのトレーシングであることにご注意ください。Elastic APMと分散トレーシングの力を感じることができます。

上記のタイムラインのビジュアライゼーションに注目している方は、バックエンドからまだデータが提供されているのに、なぜ「Car List」ページロードトランザクションがdomInteractiveの時間の193 msで終了しているのかについて疑問があるかもしれません。良い質問です。これは、デフォルトでは取得の呼び出しが非同期だからです。Webサーバーから提供される「静的」なHTMLコンテンツのすべてをロードしたため、ブラウザはすべてのHTMLおよびDOM構造の解析が193 msで完了したとみなします。一方、Reactはまだバックエンドサーバーから非同期にデータをロードし続けています。

オリジン間リソース共有(CORS)

RUMエージェントは、分散トレースのパズルの1ピースに過ぎません。分散トレーシングを使用するためには、その他のコンポーネントも適切に構成する必要があります。通常、構成する必要のあるものの1つはオリジン間リソース共有、つまり「悪名高い」CORSです。この構成が必要なのは、フロントエンドおよびバックエンドサービスは通常、別々に展開されているからです。same-originポリシーを使用すると、CORSが適切に構成されていない場合、異なるオリジンからバックエンドへのフロントエンドリクエストは失敗します。基本的にCORSは、異なるオリジンからのリクエストが許可されているかどうかをサーバーサイドで確認する方法です。オリジン間リクエストの詳細について、およびこのプロセスが必要な理由については、オリジン間リソース共有に関するMDNのページをご確認ください。

CORSは具体的に何を意味するのでしょうか。それは次の2つを意味します。

  1. distributedTracingOrigins構成オプションを設定する必要があります。これはすでに完了しています。
  2. その構成により、RUMエージェントは実際のHTTP要求の前にHTTP OPTIONS要求も送信して、すべてのヘッダーとHTTPメソッドがサポートされていること、およびオリジンが許可されていることを確認します。具体的には、http://localhost:8080は以下のヘッダーを使用することで OPTIONSリクエストを受信します。
    Access-Control-Request-Headers: traceparent, tracestate
    Access-Control-Request-Method: [request-method]
    Origin: [request-origin]
    そして、APMサーバーは以下のヘッダーと200ステータスコードで応答します。
    Access-Control-Allow-Headers: traceparent, tracestate
    Access-Control-Allow-Methods: [allowed-methods]
    Access-Control-Allow-Origin: [request-origin]

Spring BootアプリケーションのMyCorsConfigurationクラスは、まさにこれを実行するものです。このためのSpring Bootの構成方法は異なるものであり、ここではフィルターベースのアプローチを使用します。これは、任意のオリジンからの任意のHTTPヘッダーおよび任意のHTTPメソッドによるリクエストを許可するように、サーバーサイドのSpring Bootアプリケーションを構成するものです。本番アプリケーションで開かないことを推奨します。

@Configuration
public class MyCorsConfiguration {
@Bean
public FilterRegistrationBean corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
bean.setOrder(0);
return bean;
}
}

まとめ

Elastic RUMでアプリケーションをインストルメントする方法はシンプルかつ簡単でありながら、機能はパワフルです。本ブログ記事で、その点をお伝えできれば幸いです。バックエンドサービス向けに他のAPMエージェントを組み合わせて使うことにより、分散トレーシングを通じて、エンドユーザーの視点によるアプリケーションパフォーマンスの包括的なビューを獲得できます。

もう一度お伝えしておきますが、Elastic APMサーバーをダウンロードしてローカルで実行、またはElastic Cloudトライアルアカウントを作成して数分でクラスターを準備することで、Elastic APMの使用を開始できます。

ディスカッションを開始する場合またはご質問がある場合は、Elastic APMフォーラムをご利用ください。RUMを是非ご活用ください。

本ブログ記事の初稿公開日は2019年4月1日です。2022年10月20日に記事の内容を更新いたしました。