ネットワークエラーログとエンドユーザー
ユーザーの Webサイトやアプリケーションでのインタラクションについて知りたい場合、可視性は高いほどありがたいものです。そのため、ユーザーのエクスペリエンスやパフォーマンスの特徴、障害やエラー状況を示してくれる包括的なビューは間違いなく有益であり、これを実現できるメカニズムやメトリクスには非常に高い価値があります。
Fastly はグローバルなサーバーの大規模分散型ネットワークを運用しているため、サーバーの観点からは Web クライアントのネットワーク使用状況に関する優れた洞察が得られます。しかし、私たちは、状況を俯瞰して完全に把握することを目指しているため、クライアントの視点で何が起こっているのかを可視化できるメカニズムも念頭に置いています。その意味で、Navigation Timing や Resource Timing (Fastly インサイトで使用) などのブラウザテクノロジーには価値があります。これらは、クライアントのエクスペリエンスに関するパフォーマンス特性を理解するためのメトリクスを提供してくれるからです。詳しくは、クライアント側のブラウザメトリクスと併せてサーバーのメトリクスを収集できる、比較的新しい Server Timing の仕様に関するブログ記事をご覧ください。
本ブログ記事では、可視性を高めるもうひとつの仕様であるネットワークエラーログの詳細を考察し、その動作、仕組み、表面化するデータの収集で Fastly を活用する方法について説明します。
ネットワークエラーログとは
その名が示す通り、ネットワークエラーログ (流行りの呼び方で言えば「NEL」) は、ネットワークエラーや障害イベントのブラウザでの捕捉と収集に関して、推奨されている W3C 規格です。Navigation Timing、Resource Timing、および Server Timing の一部がクライアントパフォーマンスの可視性を提供するのに対し、NEL は可用性への洞察を提供します。
すなわち、NEL の目的は障害を顕在化し、クライアントが Web リソースへのアクセスに失敗する状況に関する報告メカニズムを提供することです。サーバーは、クライアントがアクセスできなかったことを知る術はないので、サーバー側のメトリクスでは何らかの問題が明らかになるまでそのことを把握できません。NEL が両者のギャップを埋める役割を果たすことで、完全な全体像に迫ることができます。
NEL の仕組み
端的に言うと、NEL はブラウザでの (すなわちリクエストの DNS、接続 (TCP と TLS) 、アプリケーション (HTTP) フェーズでの) ネットワークエラーと障害を検知し、それをひとつ以上のレポートのエンドポイントに報告します。ブラウザ向けの汎用レポートメカニズムを定義する別の W3C 仕様である Reporting API と連動することで、レポートをどこかに配信する必要のある機能が有効になります。NEL は、Reporting API のコンシューマーのひとつですが、他のコンシューマーも存在します (例えば、コンテンツセキュリティポリシーもこの API を使用します)。オリジンのエラーログポリシーを定義し、多様なエラー状況のレポートを作成するのは NEL の役割です。その後、それらのレポートをひとつ以上のエンドポイントへ配信するのは Reporting API の役割です。
このメカニズムは、2つのレスポンスヘッダーにより有効化されます。
いくらか変わった名前を持つ、少々おおげさな
NEL
レスポンスヘッダーは、ヘッダーを送信するオリジンの NEL ポリシーを定義します。Report-to
ヘッダーは、レポート配信用のひとつ以上のエンドポイントのグループを定義します。その後、NEL
ヘッダーが、ここで指定されたグループ名でレポートを送信するエンドポイントグループを指示します。
NEL ヘッダーは JSON オブジェクトです。最もシンプルな形式では以下のようになります。
NEL: {"report_to": "network-errors", "max_age": 2592000}
report_to
メンバーは必須で、レポートの送信先を指定します (この例では、「network-errors
」が Report-to
ヘッダーで定義されたエンドポイントグループの名前です。これはすぐ後に出てきます)。max_age
メンバーも必須で、このポリシーをブラウザで有効にする期間を秒単位で定義します。max_age
が0の場合、クライアントから NEL ポリシーを削除します (このときのみ report_to
の要素を省略できます)。
report_to
と max_age
に加え、NEL
ヘッダーに含まれる場合があるその他の任意メンバーは以下の通りです。
include_subdomains
メンバーは、このポリシーがこのオリジンとすべてのサブドメインに適用されるかどうかを示すブール型です。デフォルトはfalse
です。failure_fraction
メンバーは、障害レポートのサンプリングレートを定義します。すべてのエラーを記録したくない場合にレポート量を管理する方法としてお勧めです。0.0
と1.0
(両端含む) の間の値で、デフォルトは1.0
です。同様に、NEL では正常なトランザクションのレポートも可能です。
success_fraction
メンバーは、正常レポートのサンプリングレートを定義します。0.0
と1.0
の間で設定できますが、デフォルトは0.0
です。成功と失敗両方の状況を記録することで、失敗率の正確な測定につながります。
例えば https://example.com
からのレスポンスには、以下のような NEL
ヘッダーが付属します。
NEL: {"report_to": "network-errors", "max_age": 2592000, "include_subdomains": true, "success_fraction": 0.01, "failure_fraction": 0.05}
このヘッダーは、ブラウザで30日間有効な NEL ポリシーを登録し、example.com
とそのサブドメインのエラーを収集、報告して、1%の成功と5%の失敗を報告します。レポートは Report-to
ヘッダーが「network-errors
」のエンドポイントグループとして定義するすべての送信先に送られます。
Report-to
ヘッダーも JSON オブジェクトで、報告用のひとつ以上のエンドポイントのグループを定義します。これがグループなのは、フェイルオーバーとロードバランシング機能が Reporting API に構築されるためです。最もシンプルな形式では、以下のようになります。
Report-to: {"group": "network-errors", "max_age": 2592000, "endpoints": [{"url": "https://nel.example.com/report"}]}
group
メンバーは、定義されたひとつ以上のエンドポイントのグループの名前を指定します。これは任意で、デフォルトは「default
」ですが、有意の名前を使用することをお勧めします。このグループ名は NEL
ヘッダーが指定するものです。NEL
ヘッダーと同様、max_age
は必須です。このグループのライフタイムを秒単位で定義し、max_age
が0の場合、このグループをクライアントから削除します。さらに、任意の include_subdomains
のブール型メンバーがあり、これは NEL
ヘッダーとまったく同じ機能を持ちます。サブドメインのエラーをレポートする場合、どちらのヘッダーにもこれを含める必要があります。
endpoints
メンバーは、グループ内の各エンドポイントを定義する一連の JSON オブジェクトです。ここで指定された各エンドポイントは、最大3つのメンバーを持つことができます。
endpoints[].url
メンバーは必須で、レポートが送信される URL を定義します。endpoints[].priority
メンバーは、任意の非負整数で、フェイルオーバー動作を定義します。デフォルトは1です。レポート配信を行う際、グループ内で最も優先度の低いエンドポイントが最初に試行され、配信が失敗すると、レポートは次に優先度の高いエンドポイントに送信されます。endpoints[].weight
メンバーも任意の非負整数で、ロードバランシング動作を定義します。これもデフォルトは1です。グループの各エンドポイント (優先度が同じ場合) は、そのウェイトの割合によってロードバランシングが行われます。
では、https://example.com
からのレスポンスヘッダーについて考えてみましょう。
Report-to: {"group":"network-errors","max_age":2592000,"include_subdomains":true,"endpoints":[{"url":"https://nel1.example.com/report","priority":1,"weight":1},{"url":"https://nel2.example.com/report","priority":1,"weight":3},{"url":"https://nel3.example.com/report","priority":2}]}
このヘッダーは、「network-errors
」という名前の、example.com
のサブドメインのレポートを含む30日間有効なグループを定義します。レポートは、25%が https://nel1.example.com/report
に、75%が https://nel2.example.com/report
にロードバランシングされます。もしこれらのエンドポイントのひとつが失敗した場合、もう片方が100%を取得します。どちらも失敗すると、すべてのレポートは https://nel3.example.com/report
に送信されます。
エラー状況の収集とレポートでの NEL
と Report-to
ヘッダーの連携方法について、おわかりいただけたかと思います。次に、NEL
ヘッダーがポリシーを定義し、Report-to
ヘッダーがそのレポートがどこに送信されるかを定義します。
それぞれのレポートは JSON オブジェクトです。配信は、1件以上のレポートを、HTTP POST
リクエストのボディとしてレポートのエンドポイントに送信することで実行されます。レポートに関する重要な点は、必ずしもリアルタイムでは送信されないということです。クライアントはこれをキューに入れ、バッチとして送信することがあります。サンプルレポートでこの点をもう少し詳しく見てみましょう。
[
{
"age": 666,
"body": {
"elapsed_time": 37,
"method": "GET",
"phase": "connection",
"protocol": "http/1.1",
"referrer": "https://www.example.com/",
"sampling_fraction": 1,
"server_ip": "1.2.3.4",
"status_code": 0,
"type": "tcp.reset"
},
"type": "network-error",
"url": "https://www.example.com/image.png",
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36"
}
]
仕様はこれらすべてのフィールドに関わりますが、そのうちの2つほどに注目します。
すでに述べた通り、NEL は、DNS、接続、アプリケーションエラーを報告します。これはレポートボディ
のフェーズ
で示されます。この例では、接続
エラーです。NEL は多くのエラータイプを定義します。このケースのエラータイプ
は tcp.reset
で、これは TCP 接続はリセットされたものの、https://www.example.com/image.png
の取得を試みていることを意味します。
レポートはキューに入れられてバッチで送信されることがあるため、age
と elapsed_time
、そして少々の計算によりエラーの発生時間を算出できます。レポートの age
は、エラー発生とレポート送信の間の時間です。この例では、レポートはエラー発生から11分以上経過した後に送信されています (もっと長くすることも可能です)。仮に「age」が0
であれば、レポートはエラー検出の直後に送信されることになります。elapsed_time
は、リクエストが開始されてからブラウザがこれをエラーだと判断するまで (もしくは断念するまで) の時間を示します。このケースでは、クライアントが https://www.example.com/image.png
にリクエストを出してから TCP 接続のリセットを確認するまでに37秒かかっています。クロックスキューと、すべてのクライアントのクロックが正しいわけではないという事実を考慮して、レポートには意図的にタイムスタンプを含んでいません。そのため、この2つの数字を鑑みて、レポート到着の時間に関するタイムスタンプをレポートのエンドポイントでログすれば、このエラーが実際にいつクライアント側で発生したかについて、多くの事実を把握できます。
NEL のデプロイに Fastly を活用する
ここまで、NEL の基本と、NEL がクライアント側の障害やエラー状況を可視化する方法について見てきました。ここからは少し難易度を上げて、これらのメカニズムを Fastly のエッジクラウドプラットフォームでの作業にどう活用できるかについて考えていきましょう。
NEL ポリシーとレポートのエンドポイントを確立するためのレスポンスヘッダーの追加は、VCL で非常にシンプルに実行できます。
ただし、これがすべてではありません。ここではビーコンターミネーションと同様、Fastly での NEL レポートの収集とログ向けの、オリジンレスのレポートのエンドポイントを作成できます。コード2行より多少長くはなりますが、それほど複雑なものではありません。
まず、NEL レポートは POST
ボディとしてブラウザから送信されるので、クライアントは POST
リクエストのエンドポイントへの送信が可能かどうか確認するために CORS のプリフライトリクエストを送ります。最初に行うべきなのは、エンドポイントがこれらのプリフライトを適切に処理できるかどうかを確認することです。これは、レスポンスの Access-Control-*
ヘッダーが正しいものであることを意味します。
ここでは、vcl_recv
で /report
へのあらゆるリクエストを終了し、vcl_error
で適切な CORS レスポンスヘッダーのシンセティック 204 (No Content
) を生成します。これにより、204 でレポート配信の POST
リクエストにも対応できるようになるため、プリフライトの OPTIONS
リクエストとレポート自体を処理できるようになります。
最後は、それが POST
ボディ経由で配信された際のログエンドポイント へのレポートのロギングです。レポートのコンテンツには req.postbody
VCL 変数を使用できます。ただし、VCL に記録する内容の管理を行えるため、後の分析のために追加情報をログすることもできます。タイムスタンプ取得がいかに役立つかについてはすでに説明しましたが、他にも多くの有益な VCL 変数 があります。以下は、NEL レポート、取得する場合のタイムスタンプ、vcl_log
のクライアントに関する GeoIP 情報を、GCS バケットのどこにログするかの例です。
このケースでは、「reports_bucket
」が、Fastly のサービスで設定された GCS ログエンドポイント の名前です。それぞれのログ行が単一の JSON オブジェクトとなるよう構築されています。これは、GCS 内の各ファイルが改行処理で区切られた JSON 形式であることを意味し、後の分析のためにエクスポートしやすくなっています (例として GCS を挙げましたが、ご自身で選択したあらゆるログエンドポイントにレポートを送信するよう設定できます)。
ログを直接、Google BigQuery などに記録するのはよりシンプルな方法で、ログ直後からデータを分析できます。ここで問題になるのは、レポートのボディです。それらはバッチで送られるため、単一のリクエストが複数のレポートを送信する場合があるからです。VCL は JSON を処理して各レポートを抽出することができないので、レポート全体を BigQuery に送信すると、ひとつの表のセルに複数のレポートが含まれる可能性があり、クエリの作成がかなり難しくなります。そのため、代わりにボディ全体を GCS に送り、Cloud Functions を使用して各ファイルを処理し、個別のレポートを抽出してから、レポートごとに1行ずつ BigQuery に挿入します。
もしリクエストのボディを読み取り、処理できるのであれば、この最後の2ステップについては考えずに済みます。ちなみに、これは図らずも undefined Compute によってもたらされる多くの可能性のうちのひとつです。ここでは今後のブログ記事の予告としておきますが、今後は Compute を使って JSON ボディのロギングを簡素化、能率化する方法について説明する予定です。
NEL のデプロイから学べること
NEL の導入は現時点ではあまり普及していません。ブラウザによるサポートの浸透度は低、HTTP Archive データ (直近でインターネット上の540万の一意のホスト名からのリクエストを記録) を調査してみると、レスポンスに NEL ヘッダーを含めているホストはわずか1.7%に過ぎません。しかし、Fastly は NEL の非常に高いポテンシャルに大きな期待を寄せていて、Fastly インサイト の一環としてその試行錯誤を続けています。以下に、これまでに得た有益な情報を共有します。
レポート配信は、Chrome の DevTools のリクエストとしては表示されません。そのため、実際にリクエストがクライアントから送信されていることを確認する場合 (もしくはされていない場合の対処としては)、創造性を発揮する必要があります。例えば、この世で一番偉大なトラブルシューティングツールとも言える、Wireshark を使用してみてはいかがでしょうか。
ただし、Chrome では登録された NEL ポリシーとレポートのエンドポイントの両方が
chrome://net-export
で表示されます。このプロセスは、すべてが1か所に集約されていた以前のchrome://net-internals
よりは少々複雑ですが、ひとたび慣れてしまえば、すべての NEL 関連項目が確認できるようになるはずです。単一のオリジンに適用するポリシーが複数になった場合には、より具体的なものが使用されます。そのため、もしあるポリシーが
include_subdomains
のサブドメインを持つhttps://example.com
に、別のポリシーがhttps://www.example.com
に適用されている場合、https://www.example.com
のリソースにエラーがあれば、ブラウザはhttps://www.example.com
のポリシーを使用します。NEL ヘッダーとレポートのエンドポイントをアプリケーション内で統一しておくのは、想定外のことを防ぐのに有効な対策と言えるでしょう。ただし、複数のサブドメインにまたがる柔軟性が必要な場合、適した方法があります。レポートのエンドポイント自体を NEL ポリシーに分類できます。これにより、エラー配信に問題があれば NEL から知らせることができます。
セキュリティ制約のため、NEL には IP アドレスに変更があるオリジンのエラーを処理する特殊な方法があります。これは、ひとつのオリジン (ラウンドロビン DNS などの) に複数の IP アドレスを使用する場合に起こることがあります。また、ユーザーをエッジロケーションに誘導するダイナミックルーティングを使った、大規模なコンテンツ配信ネットワークを使用している場合にも発生する可能性があります。このような状況では、エラーレポートの精度は多少下がります。ここでは詳細の説明は省きますが、もし NEL の調査を検討していてこれが関係すると思われる場合は、仕様のこちらのセクションをご参照ください。
NEL と Reporting API の仕様はどちらも進化しています。NEC のエディターによる直近のドラフトは前回の公式公開版から少し変更されており、もし採用されれば、NEL レポートの一環としてリクエストとレスポンスヘッダーも収集できる機能が追加されます。Reporting API のエディターによる直近のドラフトも、前回の公開版とは異なる部分があります。
Report-to
ヘッダーの名前が変わり、構造化ヘッダーとして再定義されていますが、これは悪くないアイデアです。他にも変更点があるようです。これらのすべてに興味があり、強い関心をもってこの記事をお読みいただいているようであれば、引き続きこれらの仕様の展開にご注目ください。エラーをモニタリングしているのと同じ場所へのエラーの報告には留意してください。これは、Reporting API のフェイルオーバーメカニズムが役立つ可能性のあるケースです。
概念として、私は NEL のポテンシャルに強く惹かれています。これは、クライアント視点でのアプリケーションとネットワークの健全性に関する高い可視性を提供する方法です。また、Fastly のエッジ機能を使用したアプリケーション構築にも、大きな期待を寄せています。オリジンレスのレポートのエンドポイントはその理想的な例と言えます。これらのメカニズムの有益性を感じ、色々と試している方は、ぜひ Fastly にお知らせください。また、発見や学びをブログ記事やプレゼンテーションで共有してくださるのも大歓迎です。