ツー

日常の記録

無限にたまったCloudWatch Logsの空のlogstreamを全部削除する

CloudWatch Logsのログ自体は、保持期間を指定できるのでデータは消えてくれるけど、データの入れ物であるlogstreamだけは空のものが延々と残り続けるので削除する。

コード

こういう時はさくっとできるserverless frameworkで。

service: lambda-delete-empty-logstream
frameworkVersion: '3'
provider:
  name: aws
  runtime: nodejs14.x
  stage: dev
  region: ap-northeast-1
  iam:
    role:
      statements:
        - Effect: "Allow"
          Action:
            - "logs:DescribeLogGroups"
            - "logs:DescribeLogStreams"
            - "logs:DeleteLogStream"
          Resource: "*"

functions:
  main:
    handler: handler.main
    description: Delete old and empty logstream
    timeout: 300
    events:
      - schedule: rate(1 day)

resources:
  Description: Delete old and empty logstream
'use strict';

const AWS = require('aws-sdk');
const Logs = new AWS.CloudWatchLogs();

// rate limit is 5 calls/sec. so sleep 200sec
// https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/cloudwatch_limits_cwl.html
const sleep = () => new Promise((resolve) => setTimeout(resolve, 200));

module.exports.main = async (event) => {
    const now = Date.now();
    const threshold = 7 * 60 * 60 * 24 * 1000;

    let cnt = 0;
    let groups;
    groups = await Logs.describeLogGroups({}).promise();

    while (1) {
        for (const log of groups.logGroups) {
            let streams;
            streams = await Logs.describeLogStreams({ logGroupName: log.logGroupName }).promise();

            while (1) {
                for (const stream of streams.logStreams) {
                    if (stream.storedBytes === 0 && (now - stream.lastEventTimestamp) > threshold) {
                        console.log("DELETE", ++cnt, log.logGroupName, stream.logStreamName);
                        await Logs.deleteLogStream({ logGroupName: log.logGroupName, logStreamName: stream.logStreamName }).promise();
                        await sleep();
                    }
                }

                if (streams.nextToken) {
                    console.log("FETCH stream");
                    streams = await Logs.describeLogStreams({ logGroupName: log.logGroupName, nextToken: streams.nextToken }).promise();
                    await sleep();
                } else {
                    break;
                }
            }
        }

        if (groups.nextToken) {
            console.log("FETCH group");
            groups = await Logs.describeLogGroups({ nextToken }).promise();
        } else {
            break;
        }
    }

    return "OK";
};

実行結果

ドキュメントを確認したところ、logstream一覧や削除のAPIは秒間5回が限度らしい

なので1callごとに200msのsleepを入れるようにした。

lambdaの実行時間上限値の300秒に設定して実行してみたら、logstreamを1000個くらい消したあとタイムアウトして終了する。だーーっと流して一斉に消してもいいけれど、急いでるものでもないし一日に一回だけ流す設定にしてみた。いつか消せるものは全部消えてくれるでしょう。たぶんまた増えるだろうし、一日一回のバッチ処理で徐々に消せばいいかなという感じ。

作った後にEventBridgeでログを消えた瞬間にlogstreamを消すやつ作れそうじゃね?とも思ったが、暇があったらまた調べようと思う。