AWS CloudWatch Logsのput log eventsとメトリクスフィルターで簡易的な監視

# はじめに
AWS CloudWatch Logsのput-log-eventsでCloudWatch Logsへログを送り、メトリクスフィルターでログの内容を監視します。

CloudWatch Agentでログを送信した方が素直な実装です。

# put-log-events

参考

https://dev.classmethod.jp/articles/put-cloudwatchlogs/

`put-log-events` を行う時のBashスクリプト

```bash
#!/bin/bash -

readonly PROCNAME=${0##*/}
function log() {
  local fname=${BASH_SOURCE[1]##*/}
  echo "$(date '+%Y-%m-%dT%H:%M:%S') ${PROCNAME} (${fname}:${BASH_LINENO[0]}:${FUNCNAME[1]}) $@" 
}

export SERVICE_NAME=$1 # hello_world.service
# CloudWatchLogs設定
export LOG_GROUP_NAME=$2  # /aws/ec2/service-status
export LOG_STREAM_NAME=$3 # status
export AWS_DEFAULT_REGION=ap-northeast-1

function main() {
  log "Start batch program."

  # CloudWatchLogsにPUTするメッセージ
  message=$(systemctl show $SERVICE_NAME | grep "ActiveState=")
  log $message

  # put-log-eventに利用するトークン
  upload_sequence_token=$(aws logs describe-log-streams --log-group-name "$LOG_GROUP_NAME" --query 'logStreams[?logStreamName==`'$LOG_STREAM_NAME'`].[uploadSequenceToken]' --output text)

  # put-log-eventに利用するタイムスタンプ
  time_stamp=$(date +%s%3N)

  # put-log-eventsの実行
  if [ "$upload_sequence_token" != "None" ]
  then
    # トークン有りの場合
    aws logs put-log-events --log-group-name "$LOG_GROUP_NAME" --log-stream-name "$LOG_STREAM_NAME" --log-events timestamp=$time_stamp,message="$message" --sequence-token $upload_sequence_token  --output text
  else
    # トークン無しの場合（初回のput）
    aws logs put-log-events --log-group-name "$LOG_GROUP_NAME" --log-stream-name "$LOG_STREAM_NAME" --log-events timestamp=$time_stamp,message="$message"  --output text
  fi
  log "End batch program."
}

main
```

このスクリプトに

```bash
sudo chmod 755 monitor-by-putting-logs-to-cloudwatch.sh
```

と実行権限を付ける。cronでスクリプトを定期実行したり、送りたいログをbashの引数として与えて実行すると指定のCloudWatch Logsのロググループへログを送れる。

## スクリプトを実行するのに必要なAWSの権限

スクリプト中の `describe-log-streams` と `put-log-events` を実行するために `logs:DescribeLogStreams` と `logs:PutLogEvents` の権限が必要となる。 `logs:PutLogEvents` は他のCloudWatchLogsへログを追加できる権限となるため、気になる場合は適宜リソースを制限すること。

IAMポリシー

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "logs:DescribeLogStreams",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}
```

## put-log-eventに利用するトークン

`put-log-events` は2回目以降のputではトークンが必要となる。そのため、

```
upload_sequence_token=$(aws logs describe-log-streams --log-group-name "$LOG_GROUP_NAME" --query 'logStreams[?logStreamName==`'$LOG_STREAM_NAME'`].[uploadSequenceToken]' --output text)
```

でトークンを取得する必要がある。

## put-log-eventに必要なタイムスタンプ

`put-log-events` の使用時には必ずミリ秒まで含めた13桁のUNIXタイムが必要となる。13桁のUNIXタイムは下記のdateコマンドで取得できる。

```
time_stamp=$(date +%s%3N)
```

# メトリクスフィルターなどのCloudFormation

CloudWatch Alarmの通知先のAWS SNS Topicはすでにあるものとする。
作成しているリソース

- CloudWatch Logs
    - ロググループ
    - ログストリーム
    - メトリクスフィルター
- CloudWatch Alarm
    - アラーム

```yaml
AWSTemplateFormatVersion: 2010-09-09
Description: >-
  alert crontab result, and monitoring crontab

Parameters:
  SNSTopicARN:
    Description: SNS Topic ARN for Notification
    Type: String
  ProjectName:
    Description: Project Name
    Type: String

Resources:
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-loggroup.html
  LogGroup:
    Type: AWS::Logs::LogGroup
    Properties: 
      LogGroupName: !Sub /aws/ec2/${ProjectName}-service-status
      RetentionInDays: 1
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-logstream.html
  LogStream:
    Type: AWS::Logs::LogStream
    Properties: 
      LogGroupName: !Ref  LogGroup
      LogStreamName: "service-status"
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-metricfilter.html
# aws logs describe-metric-filters --output yaml
  MetricFilter:
    Type: AWS::Logs::MetricFilter
    Properties: 
      FilterPattern: "ActiveState=active" ## 適宜書き換え
      LogGroupName: !Ref LogGroup
      MetricTransformations: 
      - DefaultValue: 0.0
        MetricName: !Sub ${ProjectName}-filter-active
        MetricNamespace: !Sub ${ProjectName}-service-status
        MetricValue: 1
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cw-alarm.html
# aws cloudwatch describe-alarms --alarm-names "active-alarm"  --output yaml
  CloudWatchAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties: 
      ActionsEnabled: true
      AlarmActions:
      - Ref: SNSTopicARN
      AlarmDescription: >-
        Monitor ec2 service status log.
      AlarmName: !Sub ${ProjectName}-active-alarm
      ComparisonOperator: LessThanOrEqualToThreshold
      DatapointsToAlarm: 1
      Dimensions: []
      EvaluationPeriods: 1
      InsufficientDataActions: []
      MetricName: !Sub ${ProjectName}-filter-active
      Namespace: !Sub ${ProjectName}-service-status
      Period: 300
      Statistic: Sum
      Threshold: 0.0
      TreatMissingData: breaching
```

## メトリクスフィルターのフィルターパターン

メトリクスフィルターではフィルターパターンによってログを監視可能なメトリクスに変換している。

公式リファレンス

https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html

参考

https://dev.classmethod.jp/articles/cloudwatch-logs-metric-filters-how-to-setup/

下記のフィルターパターンの場合、 `ActiveState=active` を含むログがあるときメトリクスの値を `1` とする。

```yaml
  MetricFilter:
    Type: AWS::Logs::MetricFilter
    Properties: 
      FilterPattern: "ActiveState=active" ## 適宜書き換え
      LogGroupName: !Ref LogGroup
      MetricTransformations: 
      - DefaultValue: 0.0
        MetricName: !Sub ${ProjectName}-filter-active
        MetricNamespace: !Sub ${ProjectName}-service-status
        MetricValue: 1
```

## ログのWatchDog

CloudFormationの下記の項目で `TreatMissingData: breaching` と設定すると、ログが定期的に送信されなくなった時アラートを送ることができる。
メトリクスが無い状態をどう扱うか設定しており、この設定では `Period: 300` (=5分間) メトリクスが無い場合、 `INSUFFICIENT_DATA` としてアラートを送信する。

```yaml
  CloudWatchAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties: 
      Period: 300
      TreatMissingData: breaching
```

公式リファレンス

https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/AlarmThatSendsEmail.html#alarms-and-missing-data