CloudWatch logsの引数で指定したロググループ(前日のみ)のログをlocalにdownloadするシェルを作成してみました

前提

・AWS CLIがインストールされて、初期設定が完了できていること
・実行OS:RedhatLinux 8.2
・jqがインストールされていること

概要

あるPJで毎日、前日のログをローカルに取得して、確認する要望があって、検証してみました。

ハマったこと

ログがあるのに取得できない。

原因:get-log-eventsのデフォルト制限

1MBまたは10,000のログイベントを超えると、追加のオプションが必要です。

参考[1]から翻訳した内容:
デフォルトでは、「get-log-events」は 1MB の応答サイズに収まるできるだけ多くのログイベントを返します (最大10,000のログイベント)。後続の呼び出しでトークンの 1 つを指定することで、追加のログ イベントを取得できます。

解決方法

EXT_TOKEN数分でループすることで解決できました。
補足:–start-from-headオプションの必要になるので、追加しています。

参考[1]から抜粋:
ーーーーーーーーーーーーーーーー
–start-from-head | –no-start-from-head (boolean)

If the value is true, the earliest log events are returned first. If the value is false, the latest log events are returned first. The default value is false.

If you are using nextToken in this operation, you must specify true for startFromHead .
ーーーーーーーーーーーーーーーー

コードから抜粋:

            NEXT_TOKEN=$(cat "${RESPONSE_FILE_NAME}" | jq -r '.nextForwardToken')
            RET_CODE=${?}
            if [ ${RET_CODE} -ge 1 ]; then
                echo "info:ロググループ["${LOGGROUP}"]の対象ログストリームのNEXT_TOKEN取得に失敗しました。"
                END_FUNC 9
            else
                echo "info:ロググループ["${LOGGROUP}"]の対象ログストリームのNEXT_TOKENを取得しました。"
                echo "NEXT_TOKEN:" $NEXT_TOKEN
            fi

            # NEXT_TOKEN数分でループ
            while [ -n "${NEXT_TOKEN}" ]; do
                cat "${RESPONSE_FILE_NAME}" | jq -r '.events[] | [(.timestamp/1000+32400 | strftime("%Y-%m-%d %H:%M:%S")), .message] |@tsv' | sed 's/\\n$//' >> $OUT_FILE
                aws logs get-log-events --log-group-name "${LOGGROUP}" --log-stream-name "${LOGSTREAM}" --start-from-head --next-token "${NEXT_TOKEN}" > "${RESPONSE_FILE_NAME}"
                NEXT_TOKEN=$(cat "${RESPONSE_FILE_NAME}" | jq -r '.nextForwardToken')
                RET_CODE=${?}
                if [ ${RET_CODE} -ge 1 ]; then
                    echo "info:ロググループ["${LOGGROUP}"]の対象ログストリームのNEXT_TOKEN取得に失敗しました。"
                    END_FUNC 9
                else
                    echo "info:ロググループ["${LOGGROUP}"]の対象ログストリームのNEXT_TOKENを取得しました。"
                    echo "NEXT_TOKEN:" $NEXT_TOKEN
                fi

                if [[ $(cat "${RESPONSE_FILE_NAME}" | jq -e '.events == []') == "true" ]]; then
                    break
                fi

            done

日付が跨る箇所のログ数が正しく取得できていない。

原因:ロググループのタイムスタンプとEC2のOS上にあるログファイルの時間が少しずれていて、ロググループのタイムスタンプでgrepしたため。

解決方法

EC2のOS上にあるログファイルの時間でgrepすることで解決しました。

修正前のgrepキー:
「2021-05-29」
正しく取得できませんでした。

修正後grepキー:
「May 29」
正しく取得しました。

シェルの内容

ファイル名:DownloadCloudwatchlogsByOneDayAgo.sh

#!/bin/bash
############################################################################
## 事前準備 
#    実行ユーザ:root
#     引数例:ロググループの名前
#          messages
############################################################################

#環境変数
NORMAL_END=0
ABNORMAL_END=9
############################################################################
# END_FUNC
# exit コードによる後処理
# ${1} = exit コード
############################################################################
END_FUNC()
{
    case ${1} in
        ${NORMAL_END})
        ;;
        ${ABNORMAL_END})
        ;;
    esac
    exit ${1}
}

#シェルの引数チェック
if [ $# -ne 1 ]; then
    echo "usage:" "`basename $0` <aws cloudwatchlogsのロググループの名前>"
    END_FUNC ${ABNORMAL_END}
fi

SYS_DATE_CURRENT=$(TZ=UTC-9 date -d '0 days ago' '+%Y-%m-%d')
SYS_DATE_CURRENT_YYYYMMDD_HHMMSS_3N=$(TZ=UTC-9 date +%Y%m%d_%H%M%S_%3N)

SYS_DATE_ONE_DAYS_AGO=$(TZ=UTC-9 date -d '1 days ago' '+%Y-%m-%d')

SYS_DATE_TWO_DAYS_AGO=$(TZ=UTC-9 date -d '2 days ago' '+%Y-%m-%d')

GREP_DATE=$(env LANG=en_US.UTF-8 TZ=UTC-9 date -d '1 days ago' '+%b %d')

LOG_STREAMS_DATE_FROM=$(echo "${SYS_DATE_TWO_DAYS_AGO}" '23:59:00')
LOG_STREAMS_DATE_TO=$(echo "${SYS_DATE_CURRENT}" '00:00:01')

LOG_GROUP_NAME="${1}"

OUT_LOG_GROUP_FILENAME="${1}"_ALL.log
OUT_LOG_GROUP_FILENAME_BY_DATE="${1}"_ONE_DAY_"${SYS_DATE_ONE_DAYS_AGO}".log

# CloudwatchLogsのロググループを存在しているかをチェックする(messagesで始まるロググループをチェック)
LOG_GROUP_NAME_NUM=$(aws logs describe-log-groups --log-group-name-prefix messages | jq '.logGroups[] | .logGroupName' -r | grep -e "${LOG_GROUP_NAME}" | wc -l)
if [ "${LOG_GROUP_NAME_NUM}" -ge 1 ]; then
    echo "info:ロググループ["${LOG_GROUP_NAME}"]が存在しているので、後続処理を行う。"
else
    echo "info:ロググループ["${LOG_GROUP_NAME}"]が存在していないため、終了します。"
    END_FUNC 9
fi

############################################################################
# DOWNLOAD_CLOUDWATCHLOGS
# CLOUDWATCHLOGSをローカルにダウンロードする
# ${1} = ロググループの名前
# ${2} = 開始時間(YYYY-MM-DD hh:mm:ss)
# ${3} = 終了時間(YYYY-MM-DD hh:mm:ss)
# ${4} = 出力先のログファイル名
############################################################################
DOWNLOAD_CLOUDWATCHLOGS()
{
    if [ $# -ne 4 ]; then
        echo "usage:" "`basename $0` <ロググループの名前> <開始時間(YYYY-MM-DD hh:mm:ss)> <終了時間(YYYY-MM-DD hh:mm:ss)> <出力先のログファイル名>"
        END_FUNC 9
    fi

    # 引数
    LOGGROUP="${1}"
    DT_FROM="${2}"
    DT_TO="${3}"
    OUT_FILE="${4}"

    # Unixtimeに変換
    UT_FROM=`date -d "${DT_FROM}" +%s`000
    UT_TO=`date -d "${DT_TO}" +%s`000

    # jqのselect条件
    COND="(($UT_FROM <= .firstEventTimestamp) and (.firstEventTimestamp <= $UT_TO))"
    COND="$COND or (($UT_FROM <= .lastEventTimestamp) and (.lastEventTimestamp <= $UT_TO))"
    COND="$COND or ((.firstEventTimestamp <= $UT_FROM) and ($UT_TO <= .lastEventTimestamp))"

    echo "COND:" $COND

    RET_CODE=${?}
    if [ ${RET_CODE} -ge 1 ]; then
        END_FUNC 9
    fi

    TEMP_OUT_LINES=$(aws logs describe-log-streams --log-group-name $LOGGROUP --order-by LastEventTime --no-descending | jq -r ".logStreams[] | select($COND) | .logStreamName" | wc -l)
    if [ ${TEMP_OUT_LINES} -ge 1 ]; then
        echo "info:ロググループ["${LOG_GROUP_NAME}"]の対象ログストリームが存在しているので、後続処理を行う。"

        # LOGSTREAM数分でループ
        aws logs describe-log-streams --log-group-name $LOGGROUP --order-by LastEventTime --no-descending | jq -r ".logStreams[] | select($COND) | .logStreamName" | while read LOGSTREAM; do
            echo "LOGSTREAM:" $LOGSTREAM
            RESPONSE_FILE_NAME="RESPONSE"_$SYS_DATE_CURRENT_YYYYMMDD_HHMMSS_3N.log

            aws logs get-log-events --log-group-name $LOGGROUP --log-stream-name $LOGSTREAM --start-from-head > "${RESPONSE_FILE_NAME}"

            NEXT_TOKEN=$(cat "${RESPONSE_FILE_NAME}" | jq -r '.nextForwardToken')
            RET_CODE=${?}
            if [ ${RET_CODE} -ge 1 ]; then
                echo "info:ロググループ["${LOGGROUP}"]の対象ログストリームのNEXT_TOKEN取得に失敗しました。"
                END_FUNC 9
            else
                echo "info:ロググループ["${LOGGROUP}"]の対象ログストリームのNEXT_TOKENを取得しました。"
                echo "NEXT_TOKEN:" $NEXT_TOKEN
            fi

            # NEXT_TOKEN数分でループ
            while [ -n "${NEXT_TOKEN}" ]; do
                cat "${RESPONSE_FILE_NAME}" | jq -r '.events[] | [(.timestamp/1000+32400 | strftime("%Y-%m-%d %H:%M:%S")), .message] |@tsv' | sed 's/\\n$//' >> $OUT_FILE
                aws logs get-log-events --log-group-name "${LOGGROUP}" --log-stream-name "${LOGSTREAM}" --start-from-head --next-token "${NEXT_TOKEN}" > "${RESPONSE_FILE_NAME}"
                NEXT_TOKEN=$(cat "${RESPONSE_FILE_NAME}" | jq -r '.nextForwardToken')
                RET_CODE=${?}
                if [ ${RET_CODE} -ge 1 ]; then
                    echo "info:ロググループ["${LOGGROUP}"]の対象ログストリームのNEXT_TOKEN取得に失敗しました。"
                    END_FUNC 9
                else
                    echo "info:ロググループ["${LOGGROUP}"]の対象ログストリームのNEXT_TOKENを取得しました。"
                    echo "NEXT_TOKEN:" $NEXT_TOKEN
                fi

                if [[ $(cat "${RESPONSE_FILE_NAME}" | jq -e '.events == []') == "true" ]]; then
                    break
                fi

            done

        done

    else
        echo "info:ロググループ["${LOGGROUP}"]の対象ログストリームが存在していないため、終了します。"
        END_FUNC 9
    fi

}

############################################################################
# ログ取得処理(期間中全量)
############################################################################
echo "LOG_STREAMS_DATE_FROM:" $LOG_STREAMS_DATE_FROM
echo "LOG_STREAMS_DATE_TO:" $LOG_STREAMS_DATE_TO
echo "LOG_FILE_DATE:" $SYS_DATE_ONE_DAYS_AGO
echo "GREP_DATE:" $GREP_DATE

DOWNLOAD_CLOUDWATCHLOGS "${LOG_GROUP_NAME}" "${LOG_STREAMS_DATE_FROM}" "${LOG_STREAMS_DATE_TO}" "${OUT_LOG_GROUP_FILENAME}"

############################################################################
# 対象日付でGrep
############################################################################
cat "${OUT_LOG_GROUP_FILENAME}" | grep -e "${GREP_DATE}" > "${OUT_LOG_GROUP_FILENAME_BY_DATE}"

############################################################################
# 終了処理:戻り値を返却
############################################################################
if [ "${RET_CODE}" == "9" ]; then
    END_FUNC "${RET_CODE}"
elif [ "${RET_CODE}" == "0" ]; then
    END_FUNC "${RET_CODE}"
fi

実行結果

ロググループ「messages」のログを取得してみます。

sh DownloadCloudwatchlogs.sh messages
info:ロググループ[messages]が存在しているので、後続処理を行う。
LOG_STREAMS_DATE_FROM: 2021-05-28 23:59:00
LOG_STREAMS_DATE_TO: 2021-05-30 00:00:01
LOG_FILE_DATE: 2021-05-29
GREP_DATE: May 29
COND: ((1622246340000 <= .firstEventTimestamp) and (.firstEventTimestamp <= 1622332801000)) or ((1622246340000 <= .lastEventTimestamp) and (.lastEventTimestamp <= 1622332801000)) or ((.firstEventTimestamp <= 1622246340000) and (1622332801000 <= .lastEventTimestamp))
info:ロググループ[messages]の対象ログストリームが存在しているので、後続処理を行う。
LOGSTREAM: i-064c12df5219b073a
info:ロググループ[messages]の対象ログストリームのNEXT_TOKENを取得しました。
NEXT_TOKEN: f/36180115725620556155173784387743061284168486914523070494
info:ロググループ[messages]の対象ログストリームのNEXT_TOKENを取得しました。
NEXT_TOKEN: f/36180115725620556155173784387743061284168486914523070494

出力ファイル名:messages_ONE_DAY_2021-05-29.log

ファイルの内容を割愛しますが、「May 29」でgrepした結果を取得しています。

参考

[1]
https://awscli.amazonaws.com/v2/documentation/api/latest/reference/logs/get-log-events.html

Last modified: 2021-05-30

Author