CloudWatch Logs InsightsのURLを生成する

# はじめに
CloudWatch Logs Insightsのコンソールでログを検索すると、その内容がURLに反映されます。例えば次のようなURLです。

`
https://ap-northeast-1.console.aws.amazon.com/cloudwatch/home?region=ap-northeast-1#logsV2:logs-insights$3FqueryDetail$3D$257E$2528end$257E0$257Estart$257E-3600$257EtimeType$257E$2527RELATIVE$257Eunit$257E$2527seconds$257EeditorString$257E$2527fields$252A20$252A40timestamp$252A2C$252A20$252A40message$252A20$252A7C$252A20limit$252A2020$257Esource$257E$2528$257E$2527log_group_1$257E$2527log_group_2$2529$2529
`

このURLを動的に生成したかったのですが、URLの生成ルールが複雑だったため、解析しました。

# 具体例
例として、`log_group_1`と`log_group_2`というlog groupに対して`fields @timestamp, @message | limit 20`というクエリを実行する場合を考えます。対象のログの期間は現在から1時間前までとします。

URLは複数のステップを経て生成されます。

## STEP1
まず、ログを絞り込むための内容をCloudWatch Logs Insightsのkey-valueの形式に対応させます。上の例だと次のようになります。

```
end: 0
start: -3600
timeType: "RELATIVE"
unit: "seconds"
editorString: "fields @timestamp, @message | limit 20"
source: ["log_group_1", "log_group_2"]
```

次の点に注意が必要です。

- 原則velueは文字列型になる
- `end`と`start`は数値型として扱われる
- 対象のロググループ`source`は配列として扱われる(ロググループは複数指定できるため)

## STEP2
次に、各valueを次の規則に従って変換します。

### 文字列
URLエンコードした後、`%`を`*`に置換し、最後に先頭に`'`を付けます。

例えば

```
fields @timestamp, @message | limit 20
```

の場合、URLエンコードすると

```
fields%20%40timestamp%2C%20%40message%20%7C%20limit%2020
```

になります。`%`を`*`に置換すると

```
fields*20*40timestamp*2C*20*40message*20*7C*20limit*2020
```

になります。最後に先頭に`'`を付けると

```
'fields*20*40timestamp*2C*20*40message*20*7C*20limit*2020
```

となります。

### 数値
数値型の場合は特に変換はせず、そのままにします。

### 配列
配列の場合は少し面倒です。ロググループ名`["log_group_1", "log_group_2"]`の例で考えます。

まず、配列の各要素を、その要素ごとの規則に従って変換します。
今回の例では各要素は文字列なので、文字列の規則で変換します。特殊文字を含まないので先頭に`'`が付くだけです。

```
['log_group_1, 'log_group_2]
```

各要素ごとの規則に従って変換したら、次に各要素の先頭に`~`を付けます。

```
[~'log_group_1, ~'log_group_2]
```

次に、全ての要素を繋げて一つの文字列にします。

```
~'log_group_1~'log_group_2
```

最後に、`()`で囲めば完了です。

```
(~'log_group_1~'log_group_2)
```


以上の変換を行うと、例の各valueは次のようになります。

```
end: 0
start: -3600
timeType: 'RELATIVE
unit: 'seconds
editorString: 'fields*20*40timestamp*2C*20*40message*20*7C*20limit*2020
source: (~'log_group_1~'log_group_2)
```

## STEP3
各パラメータの変換ができたら、それらを結合します。
まず、各key-valueを`key~value`の形にします。

```
end~0
start~-3600
timeType~'RELATIVE
unit~'seconds
editorString~'fields*20*40timestamp*2C*20*40message*20*7C*20limit*2020
source~(~'log_group_1~'log_group_2)
```

次にそれらを`~`で連結し、一つの文字列にします。

```
end~0~start~-3600~timeType~'RELATIVE~unit~'seconds~editorString~'fields*20*40timestamp*2C*20*40message*20*7C*20limit*2020~source~(~'log_group_1~'log_group_2)
```

そうしたら全体を`()`で囲い、その後さらに先頭に`~`を付けます。

```
~(end~0~start~-3600~timeType~'RELATIVE~unit~'seconds~editorString~'fields*20*40timestamp*2C*20*40message*20*7C*20limit*2020~source~(~'log_group_1~'log_group_2))
```

## STEP4
パラメータを一つに結合したら、最後にURLの形に整形します。

まず、全体をURLエンコードします。

```
%7E%28end%7E0%7Estart%7E-3600%7EtimeType%7E%27RELATIVE%7Eunit%7E%27seconds%7EeditorString%7E%27fields%2A20%2A40timestamp%2A2C%2A20%2A40message%2A20%2A7C%2A20limit%2A2020%7Esource%7E%28%7E%27log_group_1%7E%27log_group_2%29%29
```

次に、先頭に`?queryDetail=`を付けます。

```
?queryDetail=%7E%28end%7E0%7Estart%7E-3600%7EtimeType%7E%27RELATIVE%7Eunit%7E%27seconds%7EeditorString%7E%27fields%2A20%2A40timestamp%2A2C%2A20%2A40message%2A20%2A7C%2A20limit%2A2020%7Esource%7E%28%7E%27log_group_1%7E%27log_group_2%29%29
```

もう一度全体をURLエンコードします。

```
%3FqueryDetail%3D%257E%2528end%257E0%257Estart%257E-3600%257EtimeType%257E%2527RELATIVE%257Eunit%257E%2527seconds%257EeditorString%257E%2527fields%252A20%252A40timestamp%252A2C%252A20%252A40message%252A20%252A7C%252A20limit%252A2020%257Esource%257E%2528%257E%2527log_group_1%257E%2527log_group_2%2529%2529
```

`%`を`$`に置換します。

```
$3FqueryDetail$3D$257E$2528end$257E0$257Estart$257E-3600$257EtimeType$257E$2527RELATIVE$257Eunit$257E$2527seconds$257EeditorString$257E$2527fields$252A20$252A40timestamp$252A2C$252A20$252A40message$252A20$252A7C$252A20limit$252A2020$257Esource$257E$2528$257E$2527log_group_1$257E$2527log_group_2$2529$2529
```

最後に先頭に`https://{REGION}.console.aws.amazon.com/cloudwatch/home?region={REGION}#logsV2:logs-insights`を付ければ完成です。REGIONにはAWSのリージョンが入ります。

```
https://ap-northeast-1.console.aws.amazon.com/cloudwatch/home?region=ap-northeast-1#logsV2:logs-insights$3FqueryDetail$3D$257E$2528end$257E0$257Estart$257E-3600$257EtimeType$257E$2527RELATIVE$257Eunit$257E$2527seconds$257EeditorString$257E$2527fields$252A20$252A40timestamp$252A2C$252A20$252A40message$252A20$252A7C$252A20limit$252A2020$257Esource$257E$2528$257E$2527log_group_1$257E$2527log_group_2$2529$2529
```

## 補足
上の例では相対時刻でログの範囲を絞っていますが、絶対時刻で指定する場合は次のようになります。

```
end: "2022-04-01T00:00:00.000Z"
start: "2022-04-30T23:59:59.000Z"
timeType: "ABSOLUTE"
unit: "seconds"
editorString: "fields @timestamp, @message | limit 20"
source: ["log_group_1", "log_group_2"]
```

絶対時刻の場合は`end`と`start`が文字列型になることに注意してください。

# おわりに
以上の手順で、CloudWatch Logs InsightsのURLを生成することができます。
もちろん逆の手順を踏めばURLから各パラメータを得ることも可能です。

エンコードとデコードの例をPythonで作成したので、サンプルコードをGistに上げておきます。
https://gist.github.com/sin9270/9f529d8b96eff1da23caabc374dc7a05