
ReactアプリケーションにSplunk RUMを組み込んでSplunk APMとのリンクを確認してみました
初めに
以前の記事でSplunk APMのセットアップを実施してみました。
延長となる概念としてリアルユーザモニタリング(RUM)というものがSplunkでも提供されており、こちらを使うことでバックエンドだけではなくフロントエンドのパフォーマンス情報を収集することができます(AWSでのではCloudWatch RUMでが同立ち位置のサービス)。
Splunk RUMを使うことフロントエンドから外部・バックエンドへの通信に経過した時間のトレース、javaScriptのエラー、ページ内コンテンツのロード時間などバックエンドのみでは収集できなかったブラウザ上の挙動をモニタリングし、問題を追跡可能・改善が可能となります。
Splunk RUMはSplunk RUMと合わせて導入することでフロントエンドからバックエンドまで一気通貫でモニタリングすることが可能となりますので、今回は先日実装したAPMを組み込んだAPIを叩くReactアプリにSplunk RUMを組み込んでみます。
RUMトークンの発行
APM同様RUMでもトークンが必要となります。
発行方法はAPMの記事同様Settings > Access Tokens
より発行しますがAPMで利用するAPIトークンとは別の種類になっております。
概ね発行手順は同じですが選択肢でRUM Tokenを選び発行してください。
React側のセットアップ
ちょっと古い方法ですがcreate-react-app
でベースを作成します。
npx create-react-app my-app-splunk --templete=typescript
最新のドキュメントを見る限りnpx create-react-router@latest
あたりで作成するのが良さそうでしたが、どうもなにかのかみ合いのせいか今回導入する@splunk/otel-web
内で指定する拡張子なし相対パスimportで落ちるようで断念しました。
(vite.config.tsあたりやtsconfig.json周りを含め色々触ってもどうにもならず...本筋からだいぶ外れそうなので)
16:26:24 [vite] Internal server error: Cannot find module '/xxxxx/my-react-router-appyy/node_modules/@splunk/otel-web/dist/esm/polyfill-safari10' imported from /xxxxx/my-react-router-appyy/node_modules/@splunk/otel-web/dist/esm/index.js
at finalizeResolution (node:internal/modules/esm/resolve:283:11)
at moduleResolve (node:internal/modules/esm/resolve:952:10)
at defaultResolve (node:internal/modules/esm/resolve:1188:11)
at nextResolve (node:internal/modules/esm/hooks:864:28)
at o (file:///xxxxx/my-react-router-appyy/node_modules/@tailwindcss/node/dist/esm-cache.loader.mjs:1:74)
at nextResolve (node:internal/modules/esm/hooks:864:28)
at Hooks.resolve (node:internal/modules/esm/hooks:306:30)
at MessagePort.handleMessage (node:internal/modules/esm/worker:196:24)
at [nodejs.internal.kHybridDispatch] (node:internal/event_target:831:20)
at MessagePort.<anonymous> (node:internal/per_context/messageport:23:28)
いっそのことCommonJSにしてしまえば...と思いましたが同梱されているTailwindがESModuleのみの提供だったので無理でした。
READMEを見る限り本記事執筆時点ではまだベータバージョンみたいなのでこういうこともあるのかもしれません。
...さて少し横道にそれましたがこの辺りのエラーが起きなければ導入自体は比較的お手軽です。
まずnpmで@splunk/otel-web
をインストール。
npm install @splunk/otel-web
全てのページ全体で読み込むようなファイルに以下のようなコードを追加するのみです。今回はindex.tsxに導入しています。
import SplunkOtelWeb from '@splunk/otel-web';
SplunkOtelWeb.init({
realm: 'jp0',
rumAccessToken: process.env.REACT_APP_SPLUNK_RUM_TOKEN,
applicationName: 'splunk-rum-sample', //Splunk上の表示なので識別できるもの
version: '1.0', // 同上、バージョンごとにデータが分けられる
deploymentEnvironment: 'dev' // 同上。環境ごとに
});
RUMの動作確認
一旦ここまでセットアップすればSplunk RUM単体としては動作するようになります。
うまくいっていればrum関連のリクエストが飛んでいるっぽいものが見えます。トークンに誤りがある場合は403になるので設定値を確認しましょう。
リクエストの中身を見るとブラウザ上の操作っぽいような文言がいくつか見えるのでこれでトレースデータを送ってそうです。
Observability Cloud上を見るとデータが取れていることが確認できます。
...ビルドインサーバ側のログでWARNINGがちらほら見えますがSorce MAPっぽいので本動作上は問題ないでしょう。
WARNING in ./node_modules/@splunk/otel-web/dist/esm/version.js
Module Warning (from ./node_modules/source-map-loader/dist/cjs.js):
Failed to parse source map from '/xxx/my-app-splunk/node_modules/@splunk/otel-web/src/version.ts' file: Error: ENOENT: no such file or directory, open '/xxx/my-app-splunk/node_modules/@splunk/otel-web/src/version.ts'
WARNING in ./node_modules/@splunk/otel-web/dist/esm/webvitals.js
Module Warning (from ./node_modules/source-map-loader/dist/cjs.js):
Failed to parse source map from '/xxx/my-app-splunk/node_modules/@splunk/otel-web/src/webvitals.ts' file: Error: ENOENT: no such file or directory, open '/xxx/my-app-splunk/node_modules/@splunk/otel-web/src/webvitals.ts'
RUMとAPMとのリンク
さてRUMはフロントエンド側、APMはバックエンド側となりますがこれを連携しみましょう。
バックエンドには先日作成したAPI Gateway + Lambda関数(記事冒頭参照)を使います。
https://docs.splunk.com/observability/ja/rum/intro-to-rum.html
デフォルトでは、Splunk Distribution of OpenTelemetryはすでに Server-Timing ヘッダーを送信しています。このヘッダーは、ブラウザからのスパンとバックエンドのスパンおよびトレースをリンクさせます。
Splunk RUM for Browser は、server-timing ヘッダーの応答時間を使用して、RUM スパンと対応する APM トレースを関連付けます。Server-Timing ヘッダーを制御するための APM 環境変数は SPLUNK_TRACE_RESPONSE_HEADER_ENABLED=true です。Splunk APM にリンクするには SPLUNK_TRACE_RESPONSE_HEADER_ENABLED=true を設定します。
SPLUNK_TRACE_RESPONSE_HEADER_ENABLED=true
をAPMのコレクター側で設定することでリンク可能となりますが、デフォルトがtrueであるため特に追加の設定は必要はないのでAPIを叩くページを作るだけでOKです。
API Gatewayを叩いてそのままレスポンスを表示するコンポーネントを作成し、
import { useEffect, useState } from "react";
export default function Hello() {
const [resBody, setResBody] = useState("");
useEffect(() =>{
//API GatewayのURLを環境変数に設定しておく
fetch(`${process.env.REACT_APP_BACKEND_BASE_URL}/hello`).then(async (res) =>{
setResBody(await res.text());
});
}, []);
return (
<div> {resBody}</div>
);
}
index側で/hello
に来た場合に上記のコンポーネントを表示するようにしておきます(ルーター入れてないのでちょっと乱雑ですが)。
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import Hello from './Hello';
import SplunkOtelWeb from '@splunk/otel-web';
SplunkOtelWeb.init({
realm: 'jp0',
rumAccessToken: process.env.REACT_APP_SPLUNK_RUM_TOKEN, //先ほどのトークンを環境変数もしくは.envに設定
applicationName: 'splunk-rum-sample', //Splunk上の表示なので識別できるもの
version: '1.0', // 同上、バージョンごとにデータが分けられる
deploymentEnvironment: 'dev' // 同上。環境ごとに
});
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
{window.location.pathname === '/hello'? <Hello />: <App />}
</React.StrictMode>
);
上記で作成したページにアクセスし情報を確認しに行きます。
今回はRUMの右上のほうの「Search Session」からセッション一覧に遷移します。
この画面で時間やURLを絞り込むとそれにマッチするセッションのみが表示されるようになるみたいなので、実際の利用の時にはここである程度絞り込みを行う形になりそうです。
該当アプリを利用したセッションの一覧が表示されるのでこちらから該当のセッションを表示します。
詳細画面左側では該当セッションIDユーザのフロントでのページ遷移、右側で詳細を確認すると各ページのレンダリングやロードにかかった時間が表示されます。
極端に時間がかかったものがあればその部分が赤枠で表示されますのでぱっと見でこの辺りが遅い、というのが特定できるのは良いですね。
うまくリンクできていれば「APM」と表示されている部分をマウスオーバーすると、対応するAPM側のリソースが表示されその先の処理を追うことが可能です。
今回は外部処理を叩いてないのであまり見所はない...という形になっていますがDBアクセスや別コンポーネントがあればそれと連動し、どの処理にどのくらい時間がかかというところをよりブレイクダウンしてみていくことが可能となります。
画面右側の「RUM Session」の部分より逆にAPMからRUM側にジャンプすることも可能です。
サービスマップの方でRUMとAPMリソースのつながり見れるかなと思って確認してみましたが、現時点ではまだそこまでは対応してなさそう?です(何か組み込めばできるんでしょうか?)。
終わりに
RUMの組み込みおよびAPMとの連携部分を確認してみました。
今回も変なところでハマった気がしますが、通常であればちょっとタグを組み込んでAPMでバックエンドまでのモニタリングだったものをフロント側の世界まで広げられるので組み込み自体はお手軽そうです。
また、さらにこの先があるようでSplunk Synthetic Monitoring
(AWSでいうCloudWatch Synthetics)と組み合わせていろいろ見れるようなのでこちらも機会があれば試してみようかなと思います。
なおAPMとRUMは一応機能上別のものになっており、プランによってはAPMは使えるけどRUMは使えないということもありますのでその点はご注意ください。
余談: 問題になったプロジェクト側にHTMLタグ方式で埋め込んでみた
先に書いた通りnpx create-react-router@latest
で実装されたものについて、うまくnpmで導入しimportする場合うまくいかなかったのでScriptタグ方式で埋め込んでみました。
スクリプトはサイト全体で動く箇所に組み込みたいため今回の場合はフレーム部分であるroot.tsx
のLayout
に組み込みます。
この場合以下のような形で埋め込みます。
root.tsx
にScriptタグを2つ追加し以下のようにします(Layout
コンポーネントのみ抜粋)。
export function Layout({ children }: { children: React.ReactNode }) {
console.debug(import.meta.env);
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://cdn.signalfx.com/o11y-gdi-rum/latest/splunk-otel-web.js" crossOrigin="anonymous"></script>
<script
dangerouslySetInnerHTML={{
__html: `SplunkRum.init({
realm: 'jp0',
rumAccessToken: '${import.meta.env.VITE_SPLUNK_RUM_TOKEN}',
applicationName: 'splunk-rum-sample', //Splunk上の表示でアプリを識別できるもの
version: '1.0', // ほぼ同上。バージョン毎に見る必要があれば
deploymentEnvironment: 'dev' // 同上。環境毎に見る必要があれば
});`
}}
crossOrigin="anonymous"
>
</script>
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
バージョンに関しては今回latestで埋め込んでいますが、具体的に現在使えるバージョンを確認したい場合はリポジトリのReleaseあたりから確認する形になります。
一応latest指定は良くも悪くも最新のバージョンが常に取れてしまうので本番では非推奨のようです。ふとした時にバージョン上がって予期せぬ動作引き起こす可能性もありますからね...。
うまく組み込めていれば先ほどのように/rumへのリクエストが確認できます。
こちらの呼び方でも今回試した範囲では特に動作上は変わりないものの、あくまでページ表示に外付け的にコードを読み込んでいる関係で、カスタイムイベント処理を組み込むのが厳しいので解決できそうであれば可能な限りnpmで入れてしまうのが良いでしょう。