ログ収集の重要性
近年、サイバー攻撃の増加に伴い、ログ収集と監視はシステムの防御戦略として不可欠です。特に、攻撃の予兆検知やインシデント対応などの観点からログ管理はセキュリティ強化の鍵を握っています。
またビジネス観点からも、ユーザーの行動分析や使用状況に合わせたサービス改善など、ログを適切に収集することの重要性が高まっています。
この記事ではフロントエンドのログ収集をより効率的なものにする方法を提案します。以下のような方々を主な対象読者としています。
- 主にフロントエンドの開発をされている方
- ログについて興味のある方
- 何かしらのサービスを用いてすでにログ収集を実施している方
- フロントエンドのログ収集方法に悩んでいる方
フロントエンド特有の問題
フロントエンドのログ収集は、バックエンドと異なりユーザーのブラウザ環境やクライアントサイドでの挙動に依存するため、特有の問題を抱えています。特に以下のような課題が挙げられます。
Source Mapを配布できない
フロントエンドのランタイムコードは、通常ビルドプロセス(webpack, Vite, esbuild など)によって minify(圧縮・難読化)されます。その結果、ランタイムエラーの発生箇所がオリジナルコードと一致しないため、エラーの調査が困難になります。
例えば以下のような場合があります。
// 開発時のコード function fetchUserData(userId) { return fetch(`/api/users/${userId}`) .then(response => response.json()) .catch(error => console.error("Failed to fetch user data", error)); } // Minify 後のコード function a(b){return fetch(`/api/users/${b}`).then(c=>c.json()).catch(d=>console.error("Failed to fetch user data",d));}
エラーログに a is not defined
というメッセージが出ても、どの関数が問題なのか判別が難しくなります。
通常このような場合、Source Mapによりランタイムコードとオリジナルコードを照らし合わせることで、問題の発見が容易になります。しかしフロントエンドの場合、Source MapをWeb上に公開することは理想的ではありません。
Source Mapがあればオリジナルコードを復元できる可能性があり、その結果、会社の資産が流出するおそれがあります。 またそれによりシステムの弱点や脆弱性を発見されやすくなる可能性があります。したがって、Source Mapについては安全に取り扱う必要があります。
Source Mapとは何か
Source Mapは、ブラウザ上で実行されるminifyされたJavaScriptコードを、人間が読めるオリジナルのソースコードに対応付けるためのマッピングファイルです。
フロントエンドのコードは、本番環境でのパフォーマンスを最適化するために以下のような変換がされます。
- 難読化(Obfuscation): 変数や関数名が短縮される(例:
fetchUserData()
→a()
) - 圧縮(Minification): 余分なスペースや改行が削除される
- バンドル(Bundling): 複数のモジュールを1つのファイルに統合する
この結果、本番環境でのエラーログは以下のように読みにくい形になります。
TypeError: a is not a function at bundle.js:1:12345
Source Mapを使用すると、これをオリジナルのコードと対応付け、デバッグやログ解析がしやすくなります。
デバッグやログ解析での役割
Source Mapは、以下の用途で活用されます。
開発者ツールでのデバッグ
ブラウザの開発者ツール(DevTools)がSource Mapを読み込むと、オリジナルのソースコードを表示できます。例えば、console.log()
の出力や debugger
の停止位置が元のコードの行番号で確認できるようになります。
エラーログの解析
本番環境で発生したエラーのスタックトレース(stack trace)は、難読化されたコードの行番号を示します。Source Mapを使うことで、実際のコードのどこでエラーが発生したのか特定できるようになります。
Source Mapなし
TypeError: Cannot read properties of undefined (reading 'map') at a (bundle.js:1:5678)
Source Mapあり
TypeError: Cannot read properties of undefined (reading 'map') at fetchUserData (src/utils/api.js:42:10)
ブラウザ拡張機能によるノイズ
もう1つの問題として、ユーザーがインストールしているブラウザ拡張機能がログにノイズを混入させることがあります。
これにより、アプリケーションとは無関係なエラーが大量に記録され、ログの可読性が低下します。
例えば以下はブラウザの拡張機能がchrome
APIを使用しようとして発生するエラーですが、アプリ側とは関係ありません。
Uncaught ReferenceError: chrome is not defined at <anonymous>:1:1
改善案
今回実施したプロジェクトの主な技術スタックはこのような感じです。 なおログ管理ツールとしてRollbarを使用しましたが、おそらく他のツールでも同じことが実現できると思います。
- Rollbar (ログ管理)
- Next.js (フロントエンドフレームワーク)
- CircleCI (CI)
上記で説明してきた制約の中でより安全で効率的なログ収集を行うために、以下のような仕組みを導入しました。
それぞれのステップについて説明していきます。
CIからSource Mapをアップロード
まずデプロイのCIワークフローの中でSource Mapを生成します。
Next.jsの場合、以下の設定をすれば本番環境のSource Mapを生成することができます。開発環境に関してはデフォルトで生成されるようになっているので特に設定は必要ありません。
module.exports = { productionBrowserSourceMaps: true, }
そして生成されたSource MapをRollbarへアップロードしますが、このときどのビルドバージョンのSource Mapであるかを明示する必要があります。
これはRollbar内ではcode_version
として管理されます。今回はビルドバージョンにCIRCLE_SHA1
を使用しました。
ビルド成果物を配信
次に、通常のデプロイと同じようにAWSなどを用いてビルド成果物を配信します。この際の注意点として、ビルドされたJSだけでなく、すでにRollbarへアップロード済みのSource Mapも配信する必要があります。
この理由は先ほどのSource Mapアップロード時、ファイルの実体をアップロードしているのではなく、あくまでSource Mapへのパスを共有しているだけだからです。
例えば./.next/static/chunks/main.js
に対応するSource Mapが./.next/static/chunks/main.js.map
であることを明示するようなイメージです。
こうすることでランタイムでmain.jsからエラーが発生した場合、main.js.mapを参照すれば良いということをRollbarに伝えることができます。
RollbarにSource Mapの実体がアップロードされない挙動については筆者が観測しただけでアップロード方法が間違っている可能性があります。 ご存知の方がいらっしゃいましたらコメントしていただけると嬉しいです。
Source Mapリクエスト
ブラウザからRollbarへエラー情報が送られると、Rollbarは先ほどアップロードした情報を元にNext.jsサーバーへSource Mapをリクエストします。
Next.jsサーバーはSource MapへのリクエストがRollbarのものからであることを確認し、Source Mapを返します。RollbarのIPについては以下を参照しています。
このときSource Mapの取得に失敗すると以下のようなエラーログが表示されます。例ではSource Mapのパスが指定されていない際のエラーです。
エラー解析
RollbarはSource Mapを取得するとビルドされたJSと突き合わせ、エラーの解析を行います。
Rollbarへ適切にSource Mapが共有されているかは設定タブのSource Maps
から確認することができます。
実装
上記のいくつかのセクションについて、サンプルコードを元に詳細な実装を紹介していきます。
CIからのアップロード
- run: name: Setup Rollbar code version command: | # Set CIRCLE_SHA1 as the code_version for both the SourceMap and Client. echo 'export ROLLBAR_CODE_VERSION="$CIRCLE_SHA1"' >> "$BASH_ENV"
Setup Rollbar code version
ではRollbarで使用されるcode versionを環境変数にしています。
- run: name: Install yq command: | wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O yq chmod +x yq
Install yq
ではyqをインストールし、権限を与えています。
yqはyamlファイルを操作するために使用しています。他の方法でも代替できるので無くてもよいです。
- run: name: Upload SourceMap to Rollbar command: | export APP_HOST=$(yq eval '.APP_HOST' ./workspace/.circleci/config/<< parameters.deploy_env >>.yml) for path in $(find ./workspace/.next -name "*.js" -or -name "*.css"); do if [ -e "$path.map" ]; then file=${path/.next/_next} url=${APP_HOST}/$file minified_url=$(echo "$url" | sed 's|./workspace/||') source_map="@$path.map" curl --show-error https://api.rollbar.com/api/1/sourcemap \ -F access_token=${ROLLBAR_TOKEN_DEV} \ -F version=${CIRCLE_SHA1} \ -F minified_url=$minified_url \ -F source_map=$source_map fi done
Upload SourceMap to Rollbar
は実際にSource MapをRollbarへアップロードする処理です。
ここで./workspace/
が出てきますが、これはビルド成果物を有効活用するためのCircleCIのワークスペースです。
./workspace/
が共有しているワークスペースになります。
ワークスペースによるジョブ間のデータ共有 - CircleCI
.circleci/config/stg.yml
に以下のようにAPP_HOST
を定義し、Source MapのURLを環境ごとに変更しています。
アップロードの詳細については3. Upload your source map file(s)にあるのでaccess_tokenなどはこれを参考に設定いただければと思います。
APP_HOST: https://example.com
全体的には以下のような流れになります。
実際に使用する時はこのjob以前にnpm run build
などによりSource Mapが生成されていることに注意してください。
- run: name: Setup Rollbar code version command: | # Set CIRCLE_SHA1 as the code_version for both the SourceMap and Client. echo 'export ROLLBAR_CODE_VERSION="$CIRCLE_SHA1"' >> "$BASH_ENV" - run: name: Install yq command: | wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O yq chmod +x yq - run: name: Upload SourceMap to Rollbar command: | export APP_HOST=$(yq eval '.APP_HOST' ./workspace/.circleci/config/<< parameters.deploy_env >>.yml) for path in $(find ./workspace/.next -name "*.js" -or -name "*.css"); do if [ -e "$path.map" ]; then file=${path/.next/_next} url=${APP_HOST}/$file minified_url=$(echo "$url" | sed 's|./workspace/||') source_map="@$path.map" curl --show-error https://api.rollbar.com/api/1/sourcemap \ -F access_token=${ROLLBAR_TOKEN_DEV} \ -F version=${CIRCLE_SHA1} \ -F minified_url=$minified_url \ -F source_map=$source_map fi done
Source Mapのリクエストハンドリング
先ほどのシーケンス図のSource Map
リクエストに関して以下のハンドリングを行っています。
ハンドリングする理由として、Source Mapをインターネットに公開すると、ビルドされたソースコードから元のソースコードを復元できてしまう可能性があるからです。
Source MapへのリクエストはRollbarからのみ受け付けるよう設定することで対策を行いました。
この対策方法はインフラ構成にもよりますが、今回はAWS ALBで*.js.map
への全てのリクエストを404へリダイレクトしています。
Source Mapをインターネットに公開すると、ビルドされたソースコードから元のソースコードを復元されてしまう可能性があります。そのためSource MapへのリクエストはRollbarからのみ受け付けるように設定します。
インフラ構成にもよりますが、今回はAWS ALBで*.js.map
への全てのリクエストを404へリダイレクトしています。
導入後の変化
最後にSource MapをRollbarなどログ収集サービスへアップロードすることで得られるメリットを紹介します。
1. エラー発生箇所がビルド前のコードでわかる
Source Mapがない状態であっても、ランタイムで動いているJSの情報からどんなエラーが発生したのかをできる限り表示してくれます。
しかし、ビルド前のソースコードの情報が得られないため、これでは/src/app
あたりでエラーが発生したことくらいしかわからず、エラーメッセージから「ユーザーがどんなユースケースでエラーに遭遇したのか」を推測しなければなりません。
しかしSource Mapが適切にアップロードされていれば元のソースコードの情報を詳細に知ることができ、どんな状況でエラーが起きたのかを調べやすくなります。
2. ノイズが減る
フロントエンドではソースコード起因ではないエラーを観測することがあるため、エラーログ監視作業に余分なコストが発生することがよくあります。
しかしSource Mapがあると以下のようにバンドラーでハンドルしたプロジェクトのJS以外で発生したエラーについてはnon-project frames
になり、デフォルトではたたまれて表示されるようです。
このフィルタリング機能によって、我々開発者はプロダクトで発生したエラーに絞って調査することができるようになります。
まとめ
以上、Source Mapの導入方法について説明してきました。
ログのノイズが多い問題に直面している方にとって、この記事が良い提案となれば幸いです。
実際のところ、まだ運用には至っていないため、これから実際に運用してみるとうまくいかないケースも出てくるかもしれません。しかしそういったケースにも臨機応変に対応していき、さらにログを効率よく有意義なものにしていきたいと思っています。
また知見が得られたらブログを書くかもしれないので、ご期待ください!
ここまで読んでくださりありがとうございました。