CloudWatchのロググループに手動でテスト用ログを投入したい。

## はじめに / やりたいこと

[前回の続き](https://qiita.com/akiko-pusu/items/257072e0f1bb8ccd0329)で、AWSを使っての作業メモになります。
いくつか挙げていたやりたいことの中の、こちらを試します。

- ログをよしなに見れるようにする
 - ログで何かのパターンを検出したらアラームを生成する
 - 必要に応じて通知設定をする 

ログ周りの設定なのですが、基本はアプリケーションが出すログをチェックするので、メトリクスやアラームの設定が妥当なのかどうかは、タイミングが悪いとなかなか確認できません。
アプリケーション作成側にお願いするには、ちょっと手間がかかります。

### 手動でテスト用ログを突っ込みたい

ある程度はコンソールで出来ますが、やはりログ生成（ベント送信）のところが悩みどころ。

- CloudWatchにロググループを作成する (コンソールで作成可能)
- ロググループ宛にログストリームを作成する (コンソールで作成可能)
- ログストリームに対してイベントを流す（ログを送る）
  - これはコンソールからはできない...(キャプチャ参照) 
  - ここはアプリケーションにエージェントを仕込むか送信する処理を組み込む
- メトリクスを作成する (コンソールで作成可能)
- アラームを作成する (コンソールで作成可能)

**ログストリームはつくれてもイベントが送れない...**

もしかしたら画面からでもイベントを登録できるかもしれませんが...。

<img width="450" alt="logstream-console.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27976/2a1387a6-786b-e7b1-9ce1-402c3b95ccca.png">


AWSの操作は、実際はAPIを介して色々な操作をしますので、正しいお作法でデータを投げれば、アプリケーションに代わってダミーのログを投入することができるはず...。（わざわざログを送信するアプリケーションを作るのは辛い）

aws cliを眺めていたら、どうやら ``aws logs put-log-events`` が使えることが判明。
下記の記事にもとても詳しく書かれていて、非常に助かりました！

- [[JAWS-UG CLI] CloudWatch Logs:#2 ログストリームの作成](https://qiita.com/tcsh/items/8650180e0e2c15647c3c#_reference-620340e498840e858ab0) (by @tcsh さま)

これでなんとか試せそうです。

また、イベントの送信以外にコンソールから出来る作業も、できれば何をしたか記録がわかりやすいように**Terraform**でやってみたい。
ということで、今回は以下を使うことにしました。

- Terraform
- aws cli (とshell script)

## 必要になるリソース

ここでのおさらいですが、「表題のことを実行する」だけなら、**EC2インスタンスやコンテナ、VPC、サブネットなどのアプリケーションの実稼働環境は必要ありません**。

登場するものは、以下のみです。

- AWSをCLIもしくはSDKで操作するためのAPIキー
- CloudWatchを操作できる権限

## Terraformを使ってロググループ＆メトリクスを作る

Terraformの設定自体は省略します。
また、今回はCloudWatchの機能を使うだけなので、特に他のリソース（EC2やS3といったもの)も必要ありません。

### ロググループの作成

```Terraform:sample.tf

# TestLogGroupというロググループを定義
resource "aws_cloudwatch_log_group" "test-log-group" {
  name              = "TestLogGroup"
  retention_in_days = 0
  tags              = {}
}
```

実行は以下。(ターゲットを指定して1つずつやっていきます)

```bash
% terraform plan --target=aws_cloudwatch_log_group.test-log-group
Terraform will perform the following actions:

  # aws_cloudwatch_log_group.test-log-group will be created
  + resource "aws_cloudwatch_log_group" "test-log-group" {
      + arn               = (known after apply)
      + id                = (known after apply)
      + name              = "TestLogGroup"
      + retention_in_days = 0
    }

Plan: 1 to add, 0 to change, 0 to destroy.

% terraform apply --target=aws_cloudwatch_log_group.test-log-group

....

aws_cloudwatch_log_group.test-log-group: Creating...
aws_cloudwatch_log_group.test-log-group: Creation complete after 0s [id=TestLogGroup]
```

ということで、空っぽのロググループが出来ました。この段階では、ログストリームはありませんね。

<img width="450" alt="test-log-group.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27976/4fd03562-9468-21ea-ef71-ca565bdcf2c1.png">

### メトリクスの作成

こちらも続けてメトリクスを設定します。対象のロググループがわかればいいのですが、そこはTerraformが解決してくれるので、設定は以下の通り。

> log_group_name = "${aws_cloudwatch_log_group.test-log-group.name}"

ここでどのロググループへのフィルタかを設定しています。
（直接、AWSのコンソールからarnを参照して、その値を入れても大丈夫）

```Terraform:metric.tf

# ERRORというパターンを拾います
resource "aws_cloudwatch_log_metric_filter" "error_test-log-group" {
  name           = "ErrorCount_TestLogGroup"
  pattern        = "ERROR"
  log_group_name = "${aws_cloudwatch_log_group.test-log-group.name}"

  metric_transformation {
    name      = "ErrorCount_TestLogGroup"
    namespace = "Logs"
    value     = "1"
  }
}
```

こちらも、target指定でplan & applyします。

```bash
% terraform apply --target=aws_cloudwatch_log_metric_filter.error_test-log-group
aws_cloudwatch_log_group.test-log-group: Refreshing state... [id=TestLogGroup]

...

aws_cloudwatch_log_metric_filter.error_test-log-group: Creating...
aws_cloudwatch_log_metric_filter.error_test-log-group: Creation complete after 0s [id=ErrorCount_TestLogGroup]
```

こんな感じで、ロググループにフィルタが1つ設定されました。（ここからコンソール側でも調整できます）

<img width="450" alt="metric-filter.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27976/e119caa2-d914-0a7d-cee1-9b0bacb7b11a.png">

## aws cliを利用してログストリームを作成しログを投入

さて、メトリクスフィルタを設定しても、ここからログを実際に流さないとなにも表示がされません。ここから先は、aws cliを使っていきます。

[参考にさせていただいた記事](https://qiita.com/tcsh/items/8650180e0e2c15647c3c#_reference-620340e498840e858ab0) の通りで、以下の流れになります。

- ログストリームを作る
- 1回目はこのログストリームに対して、まず初回のイベントを登録
- 同じログストリームにイベントをつなげる場合は、2回目はログストリームのuploadSequenceTokenを指定して送信する

参考: [文字列をCloudWatchLogsにPUTしてみた](https://dev.classmethod.jp/cloud/aws/put-cloudwatchlogs/) (by DevelopersIO)

わたしの場合は、こんな感じにしています。

- スクリプトを実行するたびに、別のログストリームを作成する
- そのログストリームに対して11個分のイベントを送信する
- 2回目以降のイベントはERRORを含むものと含まないものを偶数奇数で分ける

```bash:log-stream-push.sh
TIME_EPOCH="`date +%s`000"
STREAM_DATETIME="`date +%Y%m%d%H%M%S`"

LOG_GROUP_NAME="TestLogGroup"
LOG_STREAM_NAME="test-stream-${STREAM_DATETIME}"

# イベントメッセージを生成する関数
function generate_msg() {
  LOG_TIMESTAMP=`date +%Y-%m-%dT%H:%M:%S%z`

  # サンプルのログメッセージを生成（適度に場合わけ）
  if [ $1 -eq 0 ]; then
    LOG_MSG="[INFO]${LOG_TIMESTAMP}:  :   Rendered layouts/_common.html.erb (2.5ms)"
  else
    LOG_MSG="[ERROR]${LOG_TIMESTAMP}:  :   Something error occurred: xxxxx"
  fi

  # shellの関数はreturnだと数値しか返せないので、echoで送って、
  # MSG=`generate_msg 引数` で受ける
  echo ${LOG_MSG}
}

# 1回目はテスト用のstreamを作成
LOG_MSG=`generate_msg 0`
LOG_EVENTS="timestamp=${TIME_EPOCH},message=\"${LOG_MSG}\""

aws logs create-log-stream \
        --log-group-name ${LOG_GROUP_NAME} \
        --log-stream-name ${LOG_STREAM_NAME}


aws logs put-log-events \
        --log-group-name ${LOG_GROUP_NAME} \
        --log-stream-name ${LOG_STREAM_NAME} \
        --log-events "${LOG_EVENTS}"

# 2回目以降はstreamのトークンを取得して実行

for i in {1..10} ; do
  MSG_PATTERRN=$(( $i & 1 ))

  LOG_MSG=`generate_msg $MSG_PATTERRN`

  # 出来上がったログストリームから、uploadSequenceTokenを取得
  LOG_PUT_TOKEN=`aws logs describe-log-streams --log-group-name ${LOG_GROUP_NAME} \
        --log-stream-name-prefix ${LOG_STREAM_NAME} \
        --query 'logStreams[].uploadSequenceToken' --output text` \
        && echo ${LOG_PUT_TOKEN}
  LOG_EVENTS="timestamp=${TIME_EPOCH},message=\"${LOG_MSG}\""

  aws logs put-log-events \
        --log-group-name ${LOG_GROUP_NAME} \
        --log-stream-name ${LOG_STREAM_NAME} \
        --log-events "${LOG_EVENTS}" \
        --sequence-token ${LOG_PUT_TOKEN}
done
```

実行してみます。確認用にuploadSequenceTokenを出力しています。

```bash
% sh log-stream-push.sh
{
    "nextSequenceToken": "49594xxxxxxxxxxx082"
}
{
    "nextSequenceToken": "49594xxxxxxxxxxx634"
}
....
```

aws cliからの結果はダンマリですが、コンソール側を確認すると、ちゃんとログが登録されています。（タイムスタンプ改良の余地が...)

<img width="450" alt="test-log-stream-result.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27976/49057170-7717-9cdd-f73e-d95d41ac8fe6.png">

### ログストリーム投入後のメトリクス

イベント（ログ）が投入される様になると、メトリクス側にも反映されてきます。
何回かスクリプトを回して、5回エラーが出たり、10回エラーが出ていることが分かります。

<img width="450" alt="start-metrics.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27976/3e3a8dca-9e28-74a3-4250-a431c0fbca22.png">


## Terraformでアラームを設定してみる

手動でのベント登録でもなんとかなりそう！
では、上記で設定したメトリクスをつかって、アラーム設定をしてみます。
こちらもTerraformで設定します。

- テスト用なので、period (間隔)を60秒に
- 合計で6回以上検出されたらアラームを出す
  - 手元で1分につきスクリプトを1回だけ実行のときはアラームが出ません
  - 手元で1分につきスクリプトを2回以上実行すると、アラームが出るはず
- Emailなどでアクションを起こす場合はalarm_actionsに対象のトピックを登録
  - 今回は空っぽにします 

```Terraform:alarm.tf

# 上記で設定したメトリクスをつかって、アラーム設定
resource "aws_cloudwatch_metric_alarm" "alarm_error_test-log-group" {
    actions_enabled           = true
    alarm_actions             = [ ]
    alarm_description         = "ERRORを検出してアラート"
    alarm_name                = "Alerm_ErrorCount_TestLogGroup"
    comparison_operator       = "GreaterThanOrEqualToThreshold"
    datapoints_to_alarm       = 1
    dimensions                = {}
    evaluation_periods        = 1
    insufficient_data_actions = []
    metric_name               = "ErrorCount_TestLogGroup"
    namespace                 = "Logs"
    ok_actions                = []
    period                    = 60
    statistic                 = "Sum"
    tags                      = {}
    threshold                 = 6
    treat_missing_data        = "notBreaching"
}
```

こちらもapplyしてみます。

```bash
 terraform apply --target=aws_cloudwatch_metric_alarm.alarm_error_test-log-group
```

## コンソールでの画面

数回試してみました。
以下は、3回出現の場合はOKに。その後9回出現してアラームが発生した例です。

3回なので閾値には該当しません。

<img width="450" alt="alarm-close.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27976/bc62aa9d-9c53-d532-25a6-26c9a2b91545.png">

その後、9回検出したので、アラームが1件上がって来ました。

<img width="450" alt="alarm-again.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27976/a1800d6e-b542-8b09-42e9-01ee73680cc6.png">

こんな感じでうまくいったようです。

あとは通知先のアクションに、トピックを紐づければよさそうです。

## まとめ

以上、ちょっとした作り込みでしたが、なんとかスクリプトからログを生成してテスト用のアラームを発生させるところまで出来ました。
アラーム自体が適切に設定されていれば、あとは実際のアプリケーションのロググループに対して同じ様にしていけば良さそうです。
また、アプリケーションがなくても手動でログを流してアラアーム発生させて、Lambdaのテストをするといったこともやりやすそうです。

あらためて、インフラの設定をしつつも、「API叩いてるんだなあ...」というのを感じた作業です。