CloudWatchLogのログストリームをaws logsコマンドで削除する

## はじめに
ある時刻より前のログストリームを削除する作業が発生する予定です。対象のログストリームは約1000個ほどあり、手作業で削除するのは困難なため、自動化を検討しています。自動化といっても、基本的にはコマンドを実行してログストリームを削除するというシンプルな作業です。

具体的な例として、次の図で新しいログストリームが上に表示されているとします。ここで、上から4番目を含むログストリーム以降（古いものすべて）を削除することが目的です。なお、UI上では古いものを上に表示することも可能ですが、今回の記事では新しいログが上に表示される前提で話を進めます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131136/20edbaf6-ef98-66d3-6a08-631791160ed6.png)

## システム時刻のツール
時刻を指定するにはシステム時刻が必要です。このツールを使用することで、ミリ秒とUTCで時刻を表示できるため、AWSのUIで表示される時刻と一致します。

https://tools.m-bsys.com/dev_tools/unix_time.php

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131136/e14bea49-4c59-8a17-6eb7-4e551b22afe5.png)

## aws logコマンドを軽く勉強
### ロググループすべて表示
```bash
aws logs describe-log-groups --limit 50
```

```json
{
    "logGroups": [
        {
            "logGroupName": "/aws/codebuild/CodeBuildSample",
            "creationTime": 1643445457639,
            "metricFilterCount": 0,
            "arn": "arn:aws:logs:ap-northeast-1:XXXXXXXX:log-group:/aws/codebuild/CodeBuildSample:*",
            "storedBytes": 200220,
            "logGroupClass": "STANDARD",
            "logGroupArn": "arn:aws:logs:ap-northeast-1:XXXXXXXX:log-group:/aws/codebuild/CodeBuildSample"
        },
        {
            "logGroupName": "/aws/codebuild/create_test_httpserver_image",
            "creationTime": 1644115723974,
            "metricFilterCount": 0,
            "arn": "arn:aws:logs:ap-northeast-1:XXXXXXXX:log-group:/aws/codebuild/create_test_httpserver_image:*",
            "storedBytes": 432863,
            "logGroupClass": "STANDARD",
            "logGroupArn": "arn:aws:logs:ap-northeast-1:XXXXXXXX:log-group:/aws/codebuild/create_test_httpserver_image"
        },
...
}
```

- creationTime: ログストリームが作成された時刻を示します。これはログイベントの発生時刻とは関係ありません。
- firstEventTimestamp: そのログストリーム内で記録された最初のログイベントのタイムスタンプです。
- lastEventTimestamp: そのログストリーム内で記録された最後のログイベントのタイムスタンプです。
- lastIngestionTime: ログイベントが CloudWatch Logs に取り込まれた時刻を示します。これはログイベントの発生時刻（timestamp）よりも遅れることがあり、--start-time や --end-time とは関係ありません。
### 指定したロググループを表示

```json
aws logs describe-log-groups --log-group-name-prefix "/aws/codebuild/CodeBuildSample"
{
    "logGroups": [
        {
            "logGroupName": "/aws/codebuild/CodeBuildSample",
            "creationTime": 1643445457639,
            "metricFilterCount": 0,
            "arn": "arn:aws:logs:ap-northeast-1:XXXXXXXX:log-group:/aws/codebuild/CodeBuildSample:*",
            "storedBytes": 200220,
            "logGroupClass": "STANDARD",
            "logGroupArn": "arn:aws:logs:ap-northeast-1:XXXXXXXX:log-group:/aws/codebuild/CodeBuildSample"
        }
    ]
}
```

### 指定したロググループのログストリームを表示
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131136/20edbaf6-ef98-66d3-6a08-631791160ed6.png)
※この画像の３番目と４番目のログストリーム名だけ表示ということはできなかった。ログストリームの中すべて表示された感じ

このコマンドでは、LastEventTime（最後のイベント時刻）に基づいてログストリームがソートされ、新しいものから順に表示されます。--descending を使わなければ、デフォルトで昇順（古いものが先）で表示されます。


```json
aws logs describe-log-streams --log-group-name "/aws/codebuild/CodeBuildSample" --order-by "LastEventTime" --descending
{
    "logStreams": [
        {
            "logStreamName": "f87502e6-02e9-4a9f-983f-d24da10708da",
            "creationTime": 1643448259298,
            "firstEventTimestamp": 1643448288439,
            "lastEventTimestamp": 1643448401294, # 画面に表示されている時刻 2022-01-29 09:26:41 (UTC)
            "lastIngestionTime": 1643448401326,
            "uploadSequenceToken": "49617516286203631843433840421382680637257958507614832370",
            "arn": "arn:aws:logs:ap-northeast-1:XXXXXXXX:log-group:/aws/codebuild/CodeBuildSample:log-stream:f87502e6-02e9-4a9f-983f-d24da10708da",
            "storedBytes": 0
        },
        {
            "logStreamName": "bdf4d5bc-12ef-458a-ade3-2d799421c813",
            "creationTime": 1643447502830,
            "firstEventTimestamp": 1643447528546,
            "lastEventTimestamp": 1643447650998, # 画面に表示されている時刻 2022-01-29 09:14:10 (UTC)
            "lastIngestionTime": 1643447651009,
            "uploadSequenceToken": "49610437510739277453900163238473018145882494041166184850",
            "arn": "arn:aws:logs:ap-northeast-1:XXXXXXXX:log-group:/aws/codebuild/CodeBuildSample:log-stream:bdf4d5bc-12ef-458a-ade3-2d799421c813",
            "storedBytes": 0
        },
...
}
```

### ロググループ内のフィルタ条件を使用してログイベントを検索する

特定のキーワードでフィルタリングしたログイベントを検索することもできます
このコマンドでは、「ERROR」というキーワードを含むログイベントを表示します。

```json
PS C:\Users\tsuyoshi> aws logs filter-log-events --log-group-name "/aws/codebuild/CodeBuildSample" --filter-pattern "ERROR"
{
    "events": [
        {
            "logStreamName": "d460e76b-fc45-485f-9201-fc448cde34c0",
            "timestamp": 1643445486223,
            "message": "[Container] 2022/01/29 08:38:02 Phase context status code: YAML_FILE_ERROR Message: Unknown runtime version named 'openjdk8' of java. This build image has the following versions: corretto11, corretto8\n",        
            "ingestionTime": 1643445486242,
            "eventId": "36650059035934392615983147944961464701732891068622438409"
        },
        {
            "logStreamName": "21998f6f-e7ad-4104-b7e0-8fbb157fb6c2",
            "timestamp": 1643446864546,
            "message": "[Container] 2022/01/29 09:01:00 Phase context status code: YAML_FILE_ERROR Message: Unknown runtime version named 'openjdk11' of java. This build image has the following versions: corretto11, corretto8\n",       
            "ingestionTime": 1643446864576,
            "eventId": "36650089773564416890307229922200602156940188018581045257"
        },
...
}
```

## CloudWatchLogのログストリームをaws logsコマンドで削除する
ここからが本題である

### ステップ１
下記の図でいうと上から４番目を含むログ以降を全部消すことが目的です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131136/20edbaf6-ef98-66d3-6a08-631791160ed6.png)


下に行くほど古いログを示しています。上から4番目のログは 2022-01-29 09:01:04 で、システム時刻（UTC）に変換すると 1643446864000 になります。ここで注意すべき点は、ログストリームの最終イベント時刻は、そのログストリーム内の最後のログイベントの時刻を示しているということです。つまり、2022-01-29 09:01:04 より新しいイベントも存在する可能性があります。さらに言うと、3番目の最終イベント時刻以降にもログイベントが存在しています。

では、4番目以降のログストリームをどのようにコマンドで出力するかというと、1つ前の3番目の最終イベント時刻以降を指定すれば表示されるはずです。以下にコマンドを記載します。1643447436000 は、上から3番目の最終イベント時刻のシステムタイムスタンプです。

```bash
aws logs describe-log-streams \
    --log-group-name "/aws/codebuild/CodeBuildSample" \
    --order-by LastEventTime \
    --descending \
    --query "logStreams[?to_string(lastEventTimestamp) < '1643447436000']"
```

### ステップ２
ステップ1では、条件に合うログストリームのログイベントが表示されますが、最終的なゴールはログストリームの削除です。削除に必要なのはログストリーム名であり、ログストリーム内のログイベントは不要です。今回は、条件に合うログストリーム名を取得し、そのログストリームを削除するスクリプトを作成しました。

#### スクリプト
指定した時刻より古いログを削除するスクリプトです。使用方法はスクリプト内のコメントを参照すれば理解できると思います。

```bash:log_stream_manager.sh
#!/bin/bash

# このシェルスクリプトは、指定された AWS CloudWatch Logs のロググループ内で、
# 指定された時刻より"古い"（指定時刻"未満"）ログストリームをリスト表示または削除します。
#
# 使用方法:
#   --action=[list|delete] : 'list' で削除対象のログストリーム名を表示し、'delete' でログストリームを削除します。
#   --log_group_name=LOG_GROUP_NAME : 対象のロググループ名を指定します。
#   --last_event_timestamp=TIMESTAMP (UNIXタイムスタンプのミリ秒) : 指定された時刻"未満"のログストリームを対象とします。
#       - 指定したタイムスタンプ自体を含まないログストリームが対象です。
#
# 例:
#   bash log_stream_manager.sh --action=list --log_group_name="/aws/codebuild/CodeBuildSample" --last_event_timestamp=1643448401000
#   bash log_stream_manager.sh --action=delete --log_group_name="/aws/codebuild/CodeBuildSample" --last_event_timestamp=1643448401000
#
# 注意:
#   - last_event_timestamp は UNIX タイムスタンプ (ミリ秒) で指定し、UTC で表示されます。
#   - 削除操作を行う場合は、実行に十分注意してください。

# 初期化
action=""
log_group_name=""
last_event_timestamp=""

# 引数を解析する
while [ $# -gt 0 ]; do
  case "$1" in
    --action=*)
      action="${1#*=}"
      ;;
    --log_group_name=*)
      log_group_name="${1#*=}"
      ;;
    --last_event_timestamp=*)
      last_event_timestamp="${1#*=}"
      ;;
    *)
      echo "Invalid argument: $1"
      exit 1
      ;;
  esac
  shift
done

# 引数の確認
if [ -z "$action" ] || [ -z "$log_group_name" ] || [ -z "$last_event_timestamp" ]; then
  echo "Usage: $0 --action=[list|delete] --log_group_name=LOG_GROUP_NAME --last_event_timestamp=TIMESTAMP (UNIXミリ秒, UTC)"
  echo "Example:"
  echo "$0 --action=list --log_group_name=\"/aws/codebuild/CodeBuildSample\" --last_event_timestamp=1643448401000"
  echo "$0 --action=delete --log_group_name=\"/aws/codebuild/CodeBuildSample\" --last_event_timestamp=1643448401000"
  exit 1
fi

# ログファイル名を生成
log_file="log_stream_manager.log.$(date +"%Y%m%d-%H%M%S")"

# ログ出力用に tee を使用して標準出力とファイル出力を同時に行う
{
    # 指定した UNIX タイムスタンプを UTC 形式で表示
    converted_utc_time=$(date -u -d @"$(($last_event_timestamp / 1000))" +"%Y-%m-%d %H:%M:%S (UTC)")
    echo "Provided timestamp (UNIX milliseconds): $last_event_timestamp"
    echo "Converted to UTC: $converted_utc_time"

    # クエリ条件を構築
    query="logStreams[?to_string(lastEventTimestamp) < '$last_event_timestamp'].logStreamName"

    # ログストリームを取得する
    log_streams=$(aws logs describe-log-streams \
        --log-group-name "$log_group_name" \
        --order-by LastEventTime \
        --descending \
        --query "$query" \
        --no-paginate)

    # パラメータに応じて処理を分岐
    case "$action" in
        list)
            # 削除対象のログストリーム名を表示する機能
            echo "Listing delete log streams:"
            echo "$log_streams" | jq -r '.[]'
            ;;
        
        delete)
            # ログストリーム名を削除する機能
            for stream in $(echo "$log_streams" | jq -r '.[]'); do
                echo "Deleting log stream: $stream"
                aws logs delete-log-stream \
                    --log-group-name "$log_group_name" \
                    --log-stream-name "$stream"
            done
            ;;
        
        *)
            # 不正なパラメータが指定された場合
            echo "Invalid action. Use 'list' to display log streams or 'delete' to remove them."
            exit 1
            ;;
    esac
} | tee "$log_file"
```

#### 実行例
まずは削除対象を確認します。アクション、ロググループ、時刻はコマンド引数で渡します。時刻については少し複雑ですが、削除したいログストリームの1つ前（新しいもの）の最終イベント時刻を指定すれば問題ありません。実行後、削除対象が正しいかどうか、確認してください。

```bash
# list
$ bash log_stream_manager.sh --action=list --log_group_name="/aws/codebuild/CodeBuildSample" --last_event_timestamp=1643448401000
Provided timestamp (UNIX milliseconds): 1643448401000
Converted to UTC: 2022-01-29 09:26:41 (UTC)
Listing delete log streams:
bdf4d5bc-12ef-458a-ade3-2d799421c813
02bffef7-436c-46bb-ab3c-903e5b67ac03
```

次に削除します。actionの引数を`delete`にするだけです。
```bash
# delete
$ bash log_stream_manager.sh --action=delete --log_group_name="/aws/codebuild/CodeBuildSample" --last_event_timestamp=1643448401000
Provided timestamp (UNIX milliseconds): 1643448401000
Converted to UTC: 2022-01-29 09:26:41 (UTC)
Deleting log stream: bdf4d5bc-12ef-458a-ade3-2d799421c813
Deleting log stream: 02bffef7-436c-46bb-ab3c-903e5b67ac03
```

このあとUIで削除されていることを確認しましょう。

`log_stream_manager.log.20241005-151718`といったログもカレントディレクトリに出力されます。

うまくいきましたか？


## 注意事項
- aws logコマンドではデフォルトの出力最大数がある。なので削除対象がたくさんある場合は考慮する必要がある。コマンドの実行結果と、画面の表示数が違う場合は気を付けてね。
- ログイベントを削除する方法はない。削除する場合はログストリームを削除することになる。

## TODO
- ここまでくれば、CodeBuildなどにこのスクリプトを組み込めば自動化は簡単ですね。
- 範囲指定のログ削除機能もあった方が良さそうです。複雑になるので、スクリプトは分けた方が良いかもしれません。
- 指定した時刻より新しいログを削除する機能もあった方が良いですね。こちらも複雑になるので、スクリプトは分けた方が良さそうです。
- 今回は時間にマッチするログストリームの削除だが、特定のキーワードにマッチするログストリームの削除もあった方がよさそうである

これは未検証である
```bash
#!/bin/bash

# このシェルスクリプトは、指定された AWS CloudWatch Logs のロググループ内で、
# 指定された時刻より"古い"（指定時刻"未満"）ログストリームをリスト表示または削除します。
#
# 使用方法:
#   --action=[list|delete] : 'list' で削除対象のログストリーム名を表示し、'delete' でログストリームを削除します。
#   --log_group_name=LOG_GROUP_NAME : 対象のロググループ名を指定します。
#   --last_event_timestamp=TIMESTAMP (UNIXタイムスタンプのミリ秒) : 指定された時刻"未満"のログストリームを対象とします。
#       - 指定したタイムスタンプ自体を含まないログストリームが対象です。
#   --filter_string=FILTER_STRING : 特定の文字列を含むログイベントを持つログストリームをフィルタリングします。
#
# 例:
#   bash log_stream_manager.sh --action=list --log_group_name="/aws/codebuild/CodeBuildSample" --last_event_timestamp=1643448401000 --filter_string="ERROR"
#   bash log_stream_manager.sh --action=delete --log_group_name="/aws/codebuild/CodeBuildSample" --last_event_timestamp=1643448401000 --filter_string="ERROR"
#
# 注意:
#   - last_event_timestamp は UNIX タイムスタンプ (ミリ秒) で指定し、UTC で表示されます。
#   - 削除操作を行う場合は、実行に十分注意してください。

# 初期化
action=""
log_group_name=""
last_event_timestamp=""
filter_string=""

# 引数を解析する
while [ $# -gt 0 ]; do
  case "$1" in
    --action=*)
      action="${1#*=}"
      ;;
    --log_group_name=*)
      log_group_name="${1#*=}"
      ;;
    --last_event_timestamp=*)
      last_event_timestamp="${1#*=}"
      ;;
    --filter_string=*)
      filter_string="${1#*=}"
      ;;
    *)
      echo "Invalid argument: $1"
      exit 1
      ;;
  esac
  shift
done

# 引数の確認
if [ -z "$action" ] || [ -z "$log_group_name" ] || [ -z "$last_event_timestamp" ] || [ -z "$filter_string" ]; then
  echo "Usage: $0 --action=[list|delete] --log_group_name=LOG_GROUP_NAME --last_event_timestamp=TIMESTAMP (UNIXミリ秒, UTC) --filter_string=FILTER_STRING"
  echo "Example:"
  echo "$0 --action=list --log_group_name=\"/aws/codebuild/CodeBuildSample\" --last_event_timestamp=1643448401000 --filter_string=\"ERROR\""
  echo "$0 --action=delete --log_group_name=\"/aws/codebuild/CodeBuildSample\" --last_event_timestamp=1643448401000 --filter_string=\"ERROR\""
  exit 1
fi

# ログファイル名を生成
log_file="log_stream_manager.log.$(date +"%Y%m%d-%H%M%S")"

# ログ出力用に tee を使用して標準出力とファイル出力を同時に行う
{
    # 指定した UNIX タイムスタンプを UTC 形式で表示
    converted_utc_time=$(date -u -d @"$(($last_event_timestamp / 1000))" +"%Y-%m-%d %H:%M:%S (UTC)")
    echo "Provided timestamp (UNIX milliseconds): $last_event_timestamp"
    echo "Converted to UTC: $converted_utc_time"

    # 特定の文字列を含むログイベントを持つログストリームを取得する
    log_streams=$(aws logs filter-log-events \
        --log-group-name "$log_group_name" \
        --start-time 0 \
        --end-time "$last_event_timestamp" \
        --filter-pattern "$filter_string" \
        --query "events[].logStreamName" \
        --no-paginate | jq -r '.[]')

    # パラメータに応じて処理を分岐
    case "$action" in
        list)
            # 削除対象のログストリーム名を表示する機能
            echo "Listing log streams with events containing '$filter_string':"
            echo "$log_streams"
            ;;
        
        delete)
            # ログストリーム名を削除する機能
            for stream in $log_streams; do
                echo "Deleting log stream: $stream"
                aws logs delete-log-stream \
                    --log-group-name "$log_group_name" \
                    --log-stream-name "$stream"
            done
            ;;
        
        *)
            # 不正なパラメータが指定された場合
            echo "Invalid action. Use 'list' to display log streams or 'delete' to remove them."
            exit 1
            ;;
    esac
} | tee "$log_file"
```


## ログインサイト用メモ
クエリを使ったログの表示がされなかった。ログは存在する
```bash
fields @timestamp, @message
| filter @timestamp >= '2022-02-01T00:00:00Z' and @timestamp <= '2022-02-22T23:59:59Z'
| sort @timestamp desc
| limit 25
```

ミリ秒指定すると表示された
```bash
fields @timestamp, @message
| filter tomillis(@timestamp) >= 1643673600000 and tomillis(@timestamp) <= 1645574399000
| sort @timestamp desc
| limit 25
```