Azure App ServiceでWeb/APIを運用していて、クライアントが送信するリクエストヘッダから情報を得たい、と思ったことはないだろうか? PHPを前提として、リクエストヘッダをログに記録して集計する手順を紹介する。
対象:Azure App Service
Azureの「App Service」でWebサイト/APIサーバを運用していて、クライアントデバイスからアクセスされたときのHTTPリクエストヘッダから詳細な情報を得たい、と思ったことはないだろうか?
例えば、クライアントからHTTPSで接続される際にTLSのバージョンが知りたいということはないだろうか? こうしたプロトコルのバージョンなどの情報は、App Serviceのデフォルトでは記録されないことが多く、その実態は把握しにくい。
しかし、中にはリクエストヘッダに記載されているものもある。そのヘッダをログに記録して集計できれば、頻度などを確認できる。
そこで本Tech TIPSでは、Webサーバの構築/運用担当者を対象として、Azure App Serviceがアクセスされたときのリクエストヘッダを記録して、その頻度を集計する手順を紹介する。
対象はPHPスタックを選択しているサイトとする。情報の抽出にはAzureのログ分析サービス「Log Analytics」を用いる。Log Analyticsの構築や基本的な使い方、またリクエストヘッダの基本仕様の説明については割愛させていただく。
PHPには、「getallheaders」という全リクエストヘッダを取得する関数がある。これを使うと、リクエストヘッダをJSON形式でエラーログに記録できる。
Webサイト上でよくクライアントからアクセスされるPHPファイルを選び、そこに下記のコードを追加する。これで、クライアントから対象のPHPファイルがリクエストされるたびに、そのリクエストヘッダがエラーログに記録されるようになる。
<?php
<前略>
// ---------- 以下を挿入 ----------
// リクエストヘッダをエラーログに記録する関数
function logRequestHeaders(array $headerKeys = []) {
// 全リクエストヘッダを取得
$allReqHeaders = (array)getallheaders();
if (empty($headerKeys)) {
$reqHeaders = $allReqHeaders; // 未指定時は全ヘッダを格納
} else {
$reqHeaders = array_intersect_key(
$allReqHeaders, // 全ヘッダ
array_flip($headerKeys) // 抽出するヘッダ名一覧
);
}
// 該当するヘッダが1つもない場合に返すJSONで初期化
$resultsJSON = '{"RequestHeaders":{}}';
if (!empty($reqHeaders)) {
try {
$resultsJSON = json_encode([ // 該当ヘッダをJSONに変換
'RequestHeaders' => $reqHeaders,
], JSON_THROW_ON_ERROR);
} catch (JsonException $ex) {
// 必要なら、JSONエンコード失敗時の処理をここに挿入
}
}
error_log($resultsJSON); // PHPのエラーログにJSONを記録
}
// ---------- 挿入終わり ----------
<PHPの実行が始まるところ>
// ---------- 以下を挿入 ----------
logRequestHeaders([]); // 最初: 全リクエストヘッダをエラーログに記録
//logRequestHeaders(['X-Forwarded-Tlsversion']); // 後: 負荷軽減のため、特定ヘッダのみ記録
// ---------- 挿入終わり ----------
<後略>
リクエストヘッダを記録する関数「logRequestHeaders」で、特定のヘッダのみ記録できるようにしているのは、全ヘッダを記録する負荷が意外と重く、メインの処理が遅延することがあったためだ。
そこで最初は全ヘッダを記録して、その中から必要なヘッダを決めた後、logRequestHeadersにその名前の一覧を配列で渡すことで、記録するヘッダを絞り込んだ。これにより負荷は軽減され、目立った遅延を抑えることができた。
さて、App Serviceの場合、PHPのエラーログはAzure Monitorの「AppServiceConsoleLogs」というログに、エラーレベル「Error」で記録される。その「ResultDescription」キーに、前述したリクエストヘッダがJSON形式で記録されているはずだ。以下はその例である。
NOTICE: PHP message: {"RequestHeaders":{"X-Client-Port":"15780","X-Client-Ip":"VVV.WWW.YYY.ZZZ",<中略>,"Accept":"*\/*","Content-Length":"","Content-Type":""}}
このログの先頭の「NOTICE: PHP message:」を除去しつつJSONのフォーマットを整えたのが以下のリストだ。
{
"RequestHeaders": {
"X-Client-Port": "15780",
"X-Client-Ip": "VVV.WWW.YYY.ZZZ",
"X-Waws-Unencoded-Url": "\/path\/to\/api.php?key1=value1&key2=value2",
"X-Original-Url": "\/path\/to\/api.php?key1=value1&key2=value2",
"X-Forwarded-For": "VVV.WWW.YYY.ZZZ:15780",
"X-Forwarded-Tlsversion": "1.3",
"X-Arr-Ssl": "2048|256|CN=FujiSSL Public Validation Authority - G3, O=\"SECOM Trust Systems CO.,LTD.\", C=JP|CN=www.example.jp",
"X-Appservice-Proto": "https",
"X-Forwarded-Proto": "https",
"Was-Default-Hostname": "app-fwin2ksample-dev-je-044.azurewebsites.net",
"X-Site-Deployment-Id": "app-fwin2ksample-dev-je-044",
"Disguised-Host": "www.example.jp",
"Client-Ip": "VVV.WWW.YYY.ZZZ:15780",
"X-Arr-Log-Id": "e48d7767-fe75-48a9-b661-5de412e5924d",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Site": "same-origin",
"Referer": "https:\/\/www.example.jp\/?id=company1",
"Max-Forwards": "10",
"Accept-Language": "ja",
"Accept-Encoding": "gzip, deflate, br",
"User-Agent": "Mozilla\/5.0 (iPhone; CPU iPhone OS 18_3_1 like Mac OS X) AppleWebKit\/605.1.15 (KHTML, like Gecko) GSA\/358.1.731895952 Mobile\/15E148 Safari\/604.1",
"Host": "www.example.jp",
"Accept": "*\/*",
"Content-Length": "",
"Content-Type": ""
}
}
「X-」から始まる名前のリクエストヘッダは、主にApp Service内蔵のロードバランサーが付加したものだ。ここからクライアントデバイスとの接続に関する情報が幾つか読み取れる。
例えば、「X-Forwarded-Tlsversion」ヘッダには、クライアントとのHTTPS接続におけるTLS(SSL)のバージョンが格納されている。また「X-Arr-Ssl」ヘッダには、TLSに用いられたサーバ証明書の公開鍵長や署名アルゴリズムの鍵長、発行元、発行先がそれぞれ格納されているように読み取れる。
前述のログはAzure Monitorで記録されているため、その分析サービスであるLog Analyticsで抽出できる。ここでは、クライアントがHTTPSで接続する際のTLSのバージョンを例に、その方法を紹介する。Log Analyticsはあらかじめセットアップ済みとする(セットアップ方法については割愛させていただく)。
以下のKustoのクエリを実行すると、TLSのバージョンごとのリクエスト数が出力される。
AppServiceConsoleLogs
// リクエストヘッダのJSON出力のログだけに絞り込む
| where ResultDescription startswith "NOTICE: PHP message: {\"RequestHeaders\":"
// 先頭の「NOTICE: ~」=21文字を除いたJSONのみを取得
| extend reqHeaderJSON = substring(ResultDescription, 21)
// JSONからTLSバージョンを抽出し、浮動小数点に変換
| extend TLSVersion = extract_json("$.RequestHeaders['X-Forwarded-Tlsversion']", reqHeaderJSON, typeof(real))
// 集計
| summarize RequestCount = count() by TLSVersion
KustoでJSONをパースするには、上記リストにある「extract_json」の他、「parse_json」も利用できる。ただ、後者は全てのキーをパースする分、クエリが重くなる傾向がある。1つのキーのみ抽出するなら、これらの関数を利用するより、文字列のまま正規表現で抽出した方が、それほどクエリを複雑化させることなくクエリの負荷を軽減できるかもしれない。
TLSバージョン以外を集計したい場合は、前述のPHPコードと上記クエリにある「X-Forwarded-Tlsversion」の部分を該当のリクエストヘッダ名に書き換えつつ、必要に応じて「extract_json」の第3パラメーターを「typeof(string)」などの型に変更すればよい。
■関連リンク
Copyright© Digital Advantage Corp. All Rights Reserved.
Windows Server Insider 記事ランキング