API GatewayのカスタムアクセスログをAthenaでクエリ出来るようにするためのログ形式の追加設定
いわさです。
Amazon API Gatewayでは実行ログとアクセスログを出力する機能が備わっています。
アクセスログについてはCloudWatch LogsとKinesis Data Firehoseをターゲットに指定することが出来ます。
今回、API GatewayからアクセスログをKinesis Data Firehoseを使ってS3バケットへ出力し、Athenaでクエリしたところ期待した挙動になりませんでした。
そこで、ログ形式に少し設定を追加するとAthenaでクエリ出来るようになるので紹介したいと思います。
レコードが認識されない
設定としてはKinesisでDelivery streamを作成し、カスタムアクセスログ標準のJSON形式でて設定を行っています。
S3バケット上には2つのファイルが出力されており、各ファイルにはJSON形式のデータが複数含まれていました。
しかし、Athenaでクエリしてみると...
大量のレコードを期待していたのですが2件しか取得されていません。
1ファイルあたり1件のレコードしか取得出来ていないようです。
確認してみると、S3バケットへ出力されたログファイルは以下のような1行の形式になっていました。
データとデータの間が結合されていますね。
{ "requestId":"39cc8eac-98d8-444b-b824-f39ecf78c609", "ip": "203.0.113.224", "caller":"-", "user":"-","requestTime":"05/Apr/2022:23:43:16 +0000", "httpMethod":"POST","resourcePath":"/hogehoge", "status":"200","protocol":"HTTP/1.1", "responseLength":"20" }{ "requestId":"bcb7750b-ef36-4cac-8496-c36938d2f2e4", "ip": "203.0.113.224", "caller":"-", "user":"-","requestTime":"05/Apr/2022:23:43:19 +0000", "httpMethod":"POST","resourcePath":"/hogeerror", "status":"500","protocol":"HTTP/1.1", "responseLength":"7" }{ "requestId":"5283884f-678b-4039-a9fa-2170fa4b1f24", "ip": "203.0.113.224", "caller":"-", "user":"- ...
API GatewayでCSVを指定した場合も確認してみました。
203.0.113.224,-,-,06/Apr/2022:05:24:25 +0000,POST,/hogehoge,HTTP/1.1,200,20,74d34d76-cadf-487e-89d1-5dacbef06028203.0.113.224,-,-,06/Apr/2022:05:24:26 +0000,POST,/hogehoge,HTTP/1.1,200,20,cb7c55b4-2108-4e50-98a5-dbe8f021e6f6203.0.113.224,-,-,06/Apr/2022:05:24:27 +0000,POST,/hogehoge,HTTP/1.1,200,20,6f08d27d-0634-4589-b7a2-ae5754297974203.0.113.224,-,-,06/Apr/2022:05:24:28
こちらも1行で、JSONの場合は}{
がデータの区切りだとまだわかるのですが、CSVだともはやどこが区切りかわからないです。
対処方法
対処方法のひとつとして、Kinesis Delivery Stream の Transform で加工する方法があります。
他の案はどうかなと考えたときに、今回の構成であれば API GatewayからKinesisへ渡す時点でどうにか出来ないものかなと考えたので、フォーマットを変更してみました。
CSV
まずは、CSVで改行コードを入れてみました。(末尾の\n
)
$context.identity.sourceIp,$context.identity.caller,$context.identity.user,$context.requestTime,$context.httpMethod,$context.resourcePath,$context.protocol,$context.status,$context.responseLength,$context.requestId\n
そうすると、結果として期待するログ形式になりました。
203.0.113.224,-,-,06/Apr/2022:05:45:37 +0000,POST,/hogehoge,HTTP/1.1,200,20,2720c742-744d-4309-9a57-2a556fb501f1 203.0.113.224,-,-,06/Apr/2022:05:45:39 +0000,POST,/hogehoge,HTTP/1.1,200,20,633d5df5-22bd-4763-908f-66d608f6ef55 203.0.113.224,-,-,06/Apr/2022:05:45:40 +0000,POST,/hogehoge,HTTP/1.1,200,20,6b743f3c-8937-48f5-8630-774405fb70c5
JSON
AthenaのJSON SerDeライブラリのドキュメントには以下の記載があります。
Athena では、レコードが改行文字で区切られ、JSON データが 1 行に (フォーマットされていない) あることが想定されています。
JSON SerDe ライブラリ - Amazon Athena
データ間が改行で区切られていれば期待する形式になりそうですね。
以下ように末尾に\n
を付与したフォーマットを指定します。
{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "caller":"$context.identity.caller", "user":"$context.identity.user","requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength" }\n
こちらも期待どおり改行されていますね。
{ "requestId":"9cc01e14-412a-4a98-8c73-4f01fa0ad702", "ip": "203.0.113.224", "caller":"-", "user":"-","requestTime":"06/Apr/2022:05:53:00 +0000", "httpMethod":"POST","resourcePath":"/hogeerror", "status":"500","protocol":"HTTP/1.1", "responseLength":"7" } { "requestId":"4d59d228-b60d-4a90-8d48-9f2739f7f882", "ip": "203.0.113.224", "caller":"-", "user":"-","requestTime":"06/Apr/2022:05:53:17 +0000", "httpMethod":"POST","resourcePath":"/hogehoge", "status":"200","protocol":"HTTP/1.1", "responseLength":"20" } { "requestId":"423a658b-3549-40dd-a509-b56e725f446e", "ip": "203.0.113.224", "caller":"-", "user":"-","requestTime":"06/Apr/2022:05:53:20 +0000", "httpMethod":"POST","resourcePath":"/hogeerror", "status":"500","protocol":"HTTP/1.1", "responseLength":"7" }
では、Athenaでクエリしてみましょう。
データが正しく取得出来るようになりました。
さいごに
本日はAPI Gateway -> Kinesis Data Firehose -> S3 -> Athena の流れでカスタムアクセスログを扱う場合のログ形式指定方法を紹介しました。
API Gateway以外(CloudWatch Logsからの転送など)も考えると Transform で行えるようにしておくと色々なケースに対応出来るかなという所感ではあるのですが、API Gatewayのカスタムアクセスログの用途として使う場合に限ると、Lambdaなしで手軽にAthenaでクエリ出来る状態に設定出来るので、覚えておくと使い所があるかもしれません。