Elasticsearchで日付周りをPainlessを使ってうまい具合にハンドリングする

Elasticsearchは内部的にUTCで時間を管理しています。そのためその日付情報を使ってユーザーとのタッチポイントとなる箇所、例えばアラートメールなどに表示する時刻もUTCになってしまって、日本時間がわかりにくいということがたまにあります。たまにとは言っても年に数回は日付のフォーマットについて調べている気がするので、ここでまとめておきます。

基本的にはZonedDateTimeでパースして、あとはうまいこと表示します。

# 検索時にRuntime Field

検索結果に対してダイナミックにRuntime Filedを生成する方式。一番現実的かつ効率的。これが使えるケースはこれで良いでしょう。以下の例ではtimestamp_jpフィールドをruntime_mappingsとして作成しています。

Painless的にはdocオブジェクトから取得して、処理結果はemitする、というのがこのコンテキストでのポイントです。

```
GET myindex/_search
{
  "runtime_mappings": {
    "timestamp_jp": {
      "type": "keyword",
      "script": {
        "source": """
        ZonedDateTime zdt = ZonedDateTime.parse(doc['@timestamp'].value.toString());
        zdt = zdt.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
        String formattedString = zdt.format(formatter);
        emit(formattedString);
        """
      }
    }
  },
  "_source": {"excludes": "*"}, 
  "fields": [
    "@timestamp", "timestamp_jp"
  ]
}
```

レスポンス
```json
{
  "hits": {
    "hits": [
      {
        "fields": {
          "@timestamp": [
            "2023-11-10T07:04:15.000Z"
          ],
          "timestamp_jp": [
            "2023/11/10 16:04:15"
          ]
        }
      },
      {
        "fields": {
          "@timestamp": [
            "2023-11-10T07:04:23.000Z"
          ],
          "timestamp_jp": [
            "2023/11/10 16:04:23"
          ]
        }
      }
      ...
    ]
  }
}
```

# Ingest Pipelineで日本時間をフィールドに持たせてしまう

日付を表示する際に適切なゾーンに変換して表示できれば一番美しいですが、必ずしもどこでもそれができるわけではありません。例えばSecurity Alertなどでこれをやるのは現状難しいです。そこでもうインデックスに表示用のデータを文字列方で作ってしまう方法がこちらです。

Ingest Pipeline内で日付データをパース、フォーマットして新しいフィールドを作成します。以下はパイプラインのシミュレーションのみ実施しています。

Painless的にはctxオブジェクトに対してフィールド操作する、というのがこのコンテキストでのポイントです。

```
POST _ingest/pipeline/_simulate
{
  "pipeline": {
    "processors": [
      {
        "script": {
          "lang": "painless",
          "source": """
            def timestamp = ctx['@timestamp'].toString();
            ZonedDateTime zdt = ZonedDateTime.parse(timestamp);
            zdt = zdt.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
            String formattedString = zdt.format(formatter);
            ctx['timestamp_jp'] = formattedString;
          """
        }
      }
    ]
  },
  "docs": [
    {
      "_source": {
        "@timestamp": "2023-12-05T02:34:06.000Z"
      }
    }
  ]
}
```

以下のような結果になります。(関係ないフィールドは消してます)

```json
{
  "docs": [
    {
      "doc": {
        "_source": {
          "timestamp_jp": "2023/12/05 11:34:06",
          "@timestamp": "2023-12-05T02:34:06.000Z"
        }
      }
    }
  ]
}
```

# WatcherのActionで処理する場合

Elasticsearch界隈では根強い人気のWatcherですが、本気で取り組むといつも大変な作業になる印象がありますね。。とはいえ日付の変換処理は当然他と同じです。search inputからのデータは`ctx.payload.hits.hits.0._source`などから取得します。ここで0は検索結果配列のインデックス（番号）です。

```json
{
  "trigger": { "schedule": { "interval": "30m" } },
  "input": {
    "search": {
      "request": {
        "body": {
          "size": 1,
          "query": { "match_all": {} }
        },
        "indices": [ "*" ]
      }
    }
  },
  "condition": { "always": {} },
  "transform": {
    "script" : """
    def timestamp = ctx.payload.hits.hits.0._source['@timestamp'];
    ZonedDateTime zdt = ZonedDateTime.parse(timestamp);
    zdt = zdt.withZoneSameInstant(ZoneId.of('Asia/Tokyo'));
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern('yyyy/MM/dd HH:mm:ss');
    String formattedString = zdt.format(formatter);
    return [
      'timestamp': timestamp,
      'timestamp_jp' : formattedString
    ];
    """
  },
  "actions": {
    "my-logging-action": {
      "logging": {
        "text": "@timestamp: {{ctx.payload.timestamp}}, timestamp_jp: {{ctx.payload.timestamp_jp}}"
      }
    }
  }
}
```

このWatcherでは以下のような文字列がログに出力されます。

```
@timestamp: 2023-12-05T06:32:29.143Z, timestamp_jp: 2023/12/05 15:32:29
```

# まとめ

というわけで様々なコンテキストで日付フォーマットを変換する方法を紹介しました。Painlessでの変換について書いているので当然ながら変換処理内容は変わらないのですが、元になるデータへのアクセス方法が違うのでいつも迷子になります。今後は自分でもこの記事を見直すようにします。