My External Storage

Nov 23, 2021 - 4 minute read - Comments - aws

AWS Athenaでドットが含まれたJSONキーをパースする

ググっても答えがほとんど見つからなかったのでメモしておく。

TL;DR

  • AWS Athenaではjson_extractを使ってJSON文字列を構造化してパースできる
  • ネストしたJSONのキーはjson_extract(body, '$.log.request.method')のようにアクセスする
  • span.idのようなキーはjson_extract_scalar(log, '$["span.id"]')と書けばアクセスできる
SELECT
json_extract(log, '$.http.request.method') as req_method,
json_extract(log, '$.http.request.path') as req_path,
json_extract(log, '$.http.response.status') as rsp_status,
json_extract_scalar(log, '$["trace.id"]') as trace_id,
json_extract_scalar(log, '$["span.id"]') as span_id,
FROM app_logs
LIMIT 10;

AWS Athenaではjson_extractを使ってJSON文字列を構造化してパースできる

アプリケーションログのJSONをS3に大量に保存している。 普段はNew Relicでログを見ているのだが、New Relic上には1か月分のログしかないため昔のログデータについてはAthenaを経由してS3上でのログを確認したかった。

今回確認したいログは次のようなJSON形式のログだ。

{
  "container_name": "my_app",
  "ecs_cluster": "prd-cluster",
  "ecs_task_arn": "arn:aws:ecs:ap-northeast-1:3...",
  "log": "{\"level\":\"info\",\"http\":{\"request\":{\"hostname\":\"example.com\",\"method\":\"GET\",\"path\":\"/users\"},\"response\":{\"status\":200}},\"trace.id\":\"9a....\",\"span.id\":\"h34....\"}"
}

log部分の文字列は次のようなJSON文字列になる。


{
  "level": "info",
  "http": {
    "request": {
      "hostname": "example.com",
      "method": "GET",
      "path": "/users"
    },
    "response": {
      "status": 200
    }
  },
  "trace.id": "9a....",
  "span.id": "h34...."
}

このJSONの中身を使ってAthenaでクエリを書きたかった。

ネストしたJSONのキーはjson_extract(body, '$.http.request.method')のようにアクセスする

まずAthenaでS3上のログを検索するにはテーブル定義が必要だ。

今回のアプリケーションログは次のようなテーブルを使って検索する。

テーブル定義

CREATE EXTERNAL TABLE IF NOT EXISTS app_logs (
    container_name string,
    ecs_cluster string,
    ecs_task_arn string,
    log string
)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
WITH SERDEPROPERTIES (
	'ignore.malformed.json' = 'true'
LOCATION 's3://our-application-logs/ecs/my_app/';

logキーの中身は動的に変わるしJSON文字列として保存されている。 もっとlogキー部分を構造化しようとしてもよいのだが、今回は検索クエリのほうでその中身をパースして利用する。 上記のようなテーブル定義ではlogフィールドはただの文字列だ。しかし、AWS Athenaでは文字列内のJSONの構造を利用してクエリを書くができる。

logフィールドは有効なJSON文字列なのでネストしたその中身もjson_extractまたはjson_extract_scalarでアクセスすることができる。

SELECT
json_extract(log, '$.http.request.method') as req_method,
json_extract(log, '$.http.request.path') as req_path,
json_extract(log, '$.http.response.status') as rsp_status,
json_extract_scalar(log, '$.trace.id') as trace_id,
FROM app_logs
LIMIT 10;

ただし、span.idなどのキーはドットが入っているため通常のアクセスでは利用できない。 $.trace.idはネストしているJSON構造と判断され、値が取得できない。

span.idのようなキーはjson_extract_scalar(log, '$["span.id"]')と書けばアクセスできる

このようなドットを含むJSONキーは'$["span.id"]'と書けばAthenaがパースしてくれる。

SELECT
json_extract_scalar(log, '$["trace.id"]') as trace_id,
json_extract_scalar(log, '$["span.id"]') as span_id,
FROM app_logs
LIMIT 10;

上記と違い[""]で囲めば「trace オブジェクトの id キー」ではなく、「trace.idキー」と判断してくれる。

終わりに

span.idなどはNew Relic Oneが自動付与するキー名であり自分たちでキー名を変更できる要素ではなかった。 span.idなどはSentryでも記録しているIDだったのでどうしても検索キーとして使いたい背景があった。 だいぶ調べるのに時間がかかったが、目的のログ解析はできそうなのでよかった。

余談

ちなみに最近知ったのだがAWSリソースのドキュメントは全部無料のKindleになっている。Auroraとかハイライトつけて読めば理解できるかな?(ただしすごいボリューム)

参考

関連記事