255.255.255.255 - - [01/Apr/2021:11:28:40 +0900] "GET /api/hoge/fuga HTTP/1.1" 200 246 "https://example.com/hoge" "Mozilla/5.0 (Example User Agent)" "255.255.255.255"
255.255.255.255 - - [01/Apr/2021:11:28:41 +0900] "GET /api/foo?bar=xxx&piyo=zzz HTTP/1.1" 200 84 "https://example.com/hoge" "Mozilla/5.0 (Example User Agent)" "255.255.255.255"
こんな感じの nginx のアクセスログがあって、CloudWatchLogsに転送されてるときに、
パス毎のアクセス数を集計したいことがあります。
これをCloudWatch Logs Insightsで分析しようとすると、
fields @timestamp, @message
| parse '* - - [* *] "* * *" * * "*" "*" "*' as ip1, dateTime, timeZone, method, uri, httpVer, status, bytes, referral, userAgent, ip2
| stats COUNT() BY uri
みたいな書き方が多いと思うんですが、そうすると、
uri COUNT()
/api/hoge/fuga 30
/api/foo?bar=xxx&piyo=xxx 2
/api/foo?bar=xxx&piyo=yyy 4
/api/foo?bar=yyy&piyo=zzz 1
...
みたいな感じで、クエリパラメータが違うと別パス扱いになるのでなんか違う感じになります。
ほしい結果としては以下みたいな感じで、 /api/foo
は1つにまとめたい感じ。
uri COUNT()
/api/hoge/fuga 30
/api/foo 16
で、CloudWatch Logs Insightsのクエリだけでどうすればいいかというと、
parse
のときに、正規表現を使うようにします。
ドキュメントを見ると以下のように書いてあって、正規表現を利用したパターンマッチが使えます。
この単一のログ行を例として使用します。
25 May 2019 10:24:39,474 [ERROR] {foo=2, bar=data} The error was: DataIntegrityException
次の 2 つの parse 式は、それぞれ以下のことを行います。エフェメラルフィールド level、config、および exception が作成されます。level の値は ERROR、config の値は {foo=2, bar=data}、exception の値は DataIntegrityException です。最初の例は glob 式を使用し、2 番目の式は正規表現を使用します。
parse @message "[*] * The error was: *" as level, config, exception
parse @message /\[(?<level>\S+)\]\s+(?<config>\{.*\})\s+The error was: (?<exception>\S+)/
次の例では、正規表現を使用して、ログフィールド @message から、エフェメラルフィールド user2、method2、および latency2 を抽出し、method2 と user2 の一意的な組み合わせごとに平均レイテンシーを返します。
parse @message /user=(?<user2>.*?), method:(?<method2>.*?), latency := (?<latency2>.*?)/ | stats avg(latency2) by method2, user2
というわけで、正規表現でパースすることで、パスとクエリをそれぞれ取得することができます。
fields @timestamp, @message
| parse @message /[0-9.]+ - - \[\S+ \S+\] \"(?<method>[A-Z]+) (?<path>[^?]+)\??(?<query>\S+)? \S+" (?<status>[0-9]+) \S+ "\S+" "[^"]+" "\S+"/
| stats COUNT() BY path
これで、期待する結果が得られます。
path COUNT()
/api/hoge/fuga 30
/api/foo 16
正規表現の場合の書式は、 (?<エイリアス>パターン)
みたいな感じで名前付きの値を取得できるので、
(?<path>[^?]+)\??(?<query>\S+)?
っていう感じで、パスと、クエリ文字列があればクエリを取得する感じにできます。
通常の正規表現として考えると、([^?]+)\??(\S+)?
になり、 URIの中の ?
の前と後でそれぞれマッチングするようにしてるだけです。
split() 関数みたいなので分割できるとか、 正規表現で置換できるとかあればいいんですが、あんまり複雑な関数は用意されてないので、将来的にそのへんが整備されたらもっと楽にできるようになるんじゃないかなと思ってます。