課題:フロントエンドパフォーマンスにおける死角
バックエンドのダッシュボード上ではすべてが完璧に見えるかもしれません。APIのレイテンシは45ms未満で安定し、CPU使用率は20%以下を維持、NginxのHTTP 200レスポンス率は99.98%を記録しています。しかし、サポートへの問い合わせは絶えません。モバイル版Safariでチェックアウトボタンが何も起きずに失敗する、SPAの画面遷移がレンダリング途中でフリーズする、ページの動作が重くてユーザーがカートを離脱してしまうなどの問題です。
サーバー側のメトリクスでは、ユーザーのブラウザ内部で何が起きているかを把握できません。キャッチされなかったReactエラーによってコンポーネントツリーがアンマウントされたり、3MBのバナー画像が0.45のCLS(Cumulative Layout Shift)を引き起こしたりしても、バックエンドログには何も記録されません。Grafana Faroは、クライアントブラウザという問題が発生したその場でイベントをキャプチャし、ランタイムログ、パフォーマンス測定値、ネットワークトレースを既存のGrafanaスタックへ直接送信します。
フロントエンドテレメトリ:3つのアプローチの比較
クライアント側のパフォーマンスやバグを追跡する際、エンジニアリングチームは通常、以下の3つのアプローチを検討します。
1. 商用SaaS APM(Datadog RUM、Sentry、New Relic)
SaaSベンダーは、導入の手間が少なく洗練されたダッシュボードを提供します。しかし難点は従量課金のコストが急増しやすいことです。月間500万セッション規模のトラフィックがある消費者向けWebアプリでは、APMの利用料金だけで毎月数千ドルに達することも珍しくありません。また、機密性の高いクライアントのテレメトリデータをサードパーティに預けることにもなります。
2. 自社製の独自ビーコンエンドポイント
GoやNode.jsで軽量なエンドポイントを構築し、navigator.sendBeacon()のペイロードを受け取ってElasticsearchやLokiに流し込むチームもあります。導入当初はうまく機能しますが、ユーザーセッションの解析、OpenTelemetryのトレースヘッダーのサポート、Core Web Vitalsの仕様変更への追従などが必要になると、運用保守の負担が急速に膨らみます。
3. Grafana FaroとOpenTelemetryの組み合わせ
Grafana Faroは、コンソールエラー、未ハンドルの例外、Web Vitals、ユーザージャーニーを計測できる、モジュール式の軽量Web SDK(gzip圧縮で約14KB)を提供します。Grafana AgentまたはAlloyを経由して、Faroはログ行を直接Lokiへ、分散トレースをTempoへとルーティングします。すべてのデータは自社のインフラ内に保持されます。
Grafana Faroをセルフホストするトレードオフ
メリット
- 完全なデータ所有権: テレメトリデータがVPCの外部に出ることはありません。GDPRやHIPAA、社内の厳格なセキュリティポリシーへのコンプライアンス対応が容易になります。
- 予測可能なコスト: セッション数に応じた従量課金に悩まされることはありません。費用はすでに管理しているディスクストレージとコンピュートリソースにのみ比例します。
- 統合されたトレースコンテキスト: Faroは標準的なW3C
traceparentヘッダーを伝播します。Reactのイベントハンドラーで検知したユーザーの低速なクリック操作から、バックエンドのPostgreSQLクエリまで一気通貫でトレース可能です。 - Grafanaネイティブのワークフロー: 標準のダッシュボード変数やExploreクエリを使用して、Loki内のクライアントエラーログとTempo内の分散トレースの間をシームレスに行き来できます。
デメリット
- パイプラインの管理負担: ストレージの保持期間設定、コレクターの稼働監視、データベースのディスクサイジングを自前で行う必要があります。
- ソースマップの取り込み: 本番環境向けに圧縮・難読化されたバンドルのスタックトレースを復元するには、アクセス可能なソースマップエンドポイントをホストするか、ストレージバケットを手動で設定する必要があります。
アーキテクチャ概要
信頼性の高いセルフホスト環境では、Docker上で5つのコンポーネントを連携させます。
- Grafana Faro Web SDK: ブラウザ上で動作し、エラー、ユーザーのクリック、Core Web Vitals(LCP、INP、CLS)を収集します。
- Grafana Agent(またはGrafana Alloy): HTTPポートでリッスンし、FaroのJSONバッチを受信してメタデータをパースし、後続のシステムへデータを振り分けます。
- Grafana Loki: フロントエンドのコンソールログ、ネットワークエラー、セッションイベントのインデックスを作成・保存します。
- Grafana Tempo: フロントエンドのfetchやXHRリクエストによって生成されたOpenTelemetryトレーススパンを取り込みます。
- Grafana Dashboard: RUMメトリクスを可視化し、トレースとログをワンクリックで相互に参照できるようにします。
フロントエンドでのクリックからバックエンドの低速クエリまでを関連付けることで、デバッグ時間を数時間から数分へと短縮できます。それでは実際にスタックを構築していきましょう。
Docker Composeを使用したデプロイ手順
ステップ1:Docker Composeスタックの作成
grafana-faro-stackというディレクトリを作成し、以下のdocker-compose.ymlファイルを追加します。
version: '3.8'
services:
grafana:
image: grafana/grafana:10.4.0
container_name: faro-grafana
ports:
- "3000:3000"
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
volumes:
- ./grafana/datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml
depends_on:
- loki
- tempo
loki:
image: grafana/loki:2.9.4
container_name: faro-loki
ports:
- "3100:3100"
command: -config.file=/etc/loki/local-config.yaml
tempo:
image: grafana/tempo:2.3.1
container_name: faro-tempo
ports:
- "3200:3200"
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
command: [ "-config.file=/etc/tempo.yaml" ]
volumes:
- ./tempo/tempo.yaml:/etc/tempo.yaml
grafana-agent:
image: grafana/agent:v0.40.0
container_name: faro-agent
ports:
- "12347:12347" # Faroレシーバー用ポート
- "12345:12345" # Agent内部メトリクス用ポート
volumes:
- ./agent/agent-config.yaml:/etc/agent/agent-config.yaml
command:
- -config.file=/etc/agent/agent-config.yaml
- -server.http.address=0.0.0.0:12345
depends_on:
- loki
- tempo
ステップ2:Faroテレメトリ向けにGrafana Agentを設定
agent/agent-config.yamlを作成します。app_agent_receiverブロックでブラウザからのペイロードを受け付け、セッションメタデータを抽出して、ログをLokiへ、トレースをTempoへとルーティングします。
server:
log_level: info
integrations:
event_handler:
enabled: true
app_agent_receiver:
instance: production-web
server:
host: 0.0.0.0
port: 12347
cors_allowed_origins:
- "*"
sourcemaps:
download: true
logs:
send_to: loki
traces:
send_to: tempo
logs:
configs:
- name: loki
clients:
- url: http://loki:3100/loki/api/v1/push
traces:
configs:
- name: tempo
remote_write:
- endpoint: tempo:4317
insecure: true
ステップ3:Tempoストレージの設定
ローカルファイルストレージを設定したtempo/tempo.yamlを作成します。
server:
http_listen_port: 3200
distributor:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
ingester:
trace_idle_period: 10s
max_block_bytes: 1_000_000
max_block_duration: 5m
compactor:
compaction:
block_retention: 24h
storage:
trace:
backend: local
local:
path: /tmp/tempo/traces
wal:
path: /tmp/tempo/wal
ステップ4:Grafanaデータソースのプロビジョニング
Grafanaの起動時にLokiとTempoが自動的に登録されるよう、grafana/datasources.yamlを作成します。
apiVersion: 1
datasources:
- name: Loki
type: loki
access: proxy
url: http://loki:3100
isDefault: true
- name: Tempo
type: tempo
access: proxy
url: http://tempo:3200
jsonData:
httpMethod: GET
tracesToLogs:
datasourceUid: 'Loki'
tags: ['job', 'instance', 'app']
mappedTags:
- key: 'service.name'
value: 'app'
ステップ5:スタックの起動
すべてのコンテナをバックグラウンド(デタッチモード)で起動します。
docker compose up -d
すべてのコンテナが正常に動作していることを確認します。
docker compose ps
ステップ6:フロントエンドへのFaro Web SDKの組み込み
FaroのコアSDKとトレーシングパッケージをインストールします。
npm install @grafana/faro-web-sdk @grafana/faro-web-tracing
フロントエンドバンドルの可能な限り早い段階(src/index.tsxやsrc/main.jsなど)でFaroを初期化します。
import { initializeFaro, getWebInstrumentations } from '@grafana/faro-web-sdk';
import { TracingInstrumentation } from '@grafana/faro-web-tracing';
export const faro = initializeFaro({
url: 'http://localhost:12347/collect',
app: {
name: 'storefront-ui',
version: '2.1.4',
environment: 'production',
},
instrumentations: [
// コンソールエラー、キャッチされなかったPromise拒否、Web Vitalsを計測
...getWebInstrumentations({
captureConsole: true,
capturePerformance: true,
}),
// fetch/XHRリクエスト間でW3Cトレースヘッダーを伝播
new TracingInstrumentation(),
],
});
// カスタムビジネスイベントを追跡
faro.api.pushEvent('checkout_completed', {
cartTotal: '142.50',
itemCount: '3',
currency: 'USD',
});
ステップ7:データ受信の検証と確認
ブラウザのコンソールからテスト用のエラーを送信してパイプラインを検証します。
faro.api.pushError(new Error('StripeElementsError: カード要素の初期化に失敗しました'));
http://localhost:3000でGrafanaを開きます。Exploreに移動し、Lokiデータソースを選択して、次のLogQLクエリを実行します。
{app="storefront-ui"} |= "StripeElementsError"
ログレコードには、正確なブラウザのバージョン、OS、URLパス、ソースコードの行番号が表示されます。ユーザー全体のパフォーマンスを集計して監視するには、GrafanaダッシュボードID 17997をインポートしてください。実際のデバイス環境におけるLargest Contentful Paint(目標値: 2.5秒未満)、Interaction to Next Paint(INP、目標値: 200ms未満)、Cumulative Layout Shift(0.1未満)の本番環境での分布を即座に確認できます。

