CloudWatchLogsとLambdaを使ったエラーログ監視をしてみる

# はじめに
エラーログ。。簡単にキーワードでひっかけてSlack通知させたいなぁ。。
そんなお悩みありませんか？
AWS上で運用されているかた必見！
かーんたんに（そしてわりと安く）ログ監視できちゃう方法をお披露目したいと思います！

# 構成
![ScreenShot_2020-12-12_05.41.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139745/3ac1fc6e-e8d7-a131-79e8-7097b369291b.png)

# やり方
1. CloudWatchAgentをEC2に導入する
1. Lambdaの処理を書く
1. ロググループにサブスクリプションフィルターを設定する

## 1. CloudWatchAgentをEC2に導入する
ポリシーが必要になるので、以下を参考にアタッチしてください。
https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/create-iam-roles-for-cloudwatch-agent.html#create-iam-roles-for-cloudwatch-agent-roles

インストールはAmazonLinux2を利用してる場合以下で一発導入可能。

```shell
$ sudo yum install amazon-cloudwatch-agent
```
その他のサポート対象OSをお使いの場合は[こちら](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/download-cloudwatch-agent-commandline.html)を参考に導入してみてください！
サポート対象OSについては以下をご確認ください。
https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/Install-CloudWatch-Agent.html

導入が済んだら、設定ファイルを書いて起動してみましょう！
起動方法はOSによって異なるので、OSに合わせた起動方法で起動してください。
設定例

```json
{
  "agent": {
    "run_as_user": "root"
  },
  "logs": {
    "logs_collected": {
      "files": {
        "collect_list": [
          {
            "file_path": "/var/log/hoge_service/fuga.log",
            "log_group_name": "/aws/ec2/hoge_service/fuga.log",
            "log_stream_name": "{instance_id}"
          }
        ]
      }
    }
  }
}
```
log_stream_nameは`instance_id`を使うと手っ取り早くEC2毎にログストリームをわけてくれてオススメ設定です。
複数サーバーで使う場合は同じストリームにしないようにした方が良いでしょうね。
（理由は。。試せばわかりますw）
サーバー毎に監視したいエラーメッセージなどが異なる場合はlog_group_nameをサーバー毎にわける必要があります。
例： サーバーAでは**HogeError**を監視する必要があって、サーバーBでは**FugaError**を監視する必要がある場合は、サーバーAとサーバーBでlog_group_nameを分ける必要がある。

CloudWatchAgentはこの他にも色々な事ができて便利なので、気になる方は試してみてください。

## 2. Lambdaの処理を書く
CloudWatchLogsからトリガーされてSlack通知をするちょうど良いサンプルがPythonであったので、Pythonにしてます。
Lambdaがサポートしているお好きな言語で書くと良いと思います。
尚、Slackへの通知はIncoming webhookを想定して書いてます。
Incoming webhookの使い方は各自お調べください。
ログストリーム名をEC2のインスタンスIDとしているのでAWSコンソール上のEC2詳細画面へのリンクを生成して通知メッセージに入れてます。（オススメ！！）
またロググループへの遷移もできるようにしているので、前後のログ状況が通知後すぐに確認できます。（オススメ！！）

```python
import base64
import requests, json
import urllib
import zlib
import datetime
import os
import boto3
import pprint
from botocore.exceptions import ClientError

print('Loading function')


def lambda_handler(event, context):
    data = zlib.decompress(base64.b64decode(event['awslogs']['data']), 16+zlib.MAX_WBITS)
    data_json = json.loads(data)
    WEB_HOOK_URL = "https://hooks.slack.com/services/hoge/fuga/XXXXXXX"
    WIKI_LINK_TITLE = "<https://hoge.fuga/wiki/HogeError|WIKI>"

    for log in data_json["logEvents"]:
        log_json = json.loads(json.dumps(log, ensure_ascii=False))
        #print(log_json)

        LOG_GROUP_LINK_TITLE = "<https://ap-northeast-1.console.aws.amazon.com/cloudwatch/home?region=ap-northeast-1#logsV2:log-groups/log-group/%s/log-events/%s|%s>" % (urllib.parse.quote_plus(data_json['logGroup']),data_json['logStream'],data_json['logGroup'])
        INSTANCE_ID_LINK_TITLE = "<https://ap-northeast-1.console.aws.amazon.com/ec2/v2/home?region=ap-northeast-1#InstanceDetails:instanceId={0}|{0}>".format(data_json['logStream'])

        print("LOG_GROUP : %s" % data_json['logGroup'])
        print("STREAM_NAME : %s" % data_json['logStream'])
    
        try:
        
            message = u':no_entry:HOGEエラーが発生しました。\nサーバーの状態を確認して%sを参考に対応してください。\nロググループ名：%s\nインスタンスID：%s\nエラーメッセージ：\n```\n%s\n```' % (WIKI_LINK_TITLE,LOG_GROUP_LINK_TITLE,INSTANCE_ID_LINK_TITLE,log_json['message'])

            requests.post(WEB_HOOK_URL, data = json.dumps({
                'channel': u'#XXXXXXX',
                'text': message,  #通知内容
                'username': u'Alart From AWS lambda'  #ユーザー名
            }))
    
        except Exception as e:
            print(e)

    print('End function')

```
**requests**ライブラリが外部パッケージとなっているため、pipなどでダウンロードしてzipで固めてuploadする必要があります。
以下Mac上で対応した例です。
最後にlambda_function.pyを忘れずに入れてディレクトリごとzipで固めてuploadすれば幸せになれます。

```shell
$ mkdir for_lambda
$ cd for_lambda/
$ pip3.7 install requests -t .
Collecting requests
  Downloading https://files.pythonhosted.org/packages/39/fc/f91eac5a39a65f75a7adb58eac7fa78871ea9872283fb9c44e6545998134/requests-2.25.0-py2.py3-none-any.whl (61kB)
     |████████████████████████████████| 61kB 7.1MB/s 
Collecting chardet<4,>=3.0.2
  Using cached https://files.pythonhosted.org/packages/bc/a9/01ffebfb562e4274b6487b4bb1ddec7ca55ec7510b22e4c51f14098443b8/chardet-3.0.4-py2.py3-none-any.whl
Collecting urllib3<1.27,>=1.21.1
  Downloading https://files.pythonhosted.org/packages/f5/71/45d36a8df68f3ebb098d6861b2c017f3d094538c0fb98fa61d4dc43e69b9/urllib3-1.26.2-py2.py3-none-any.whl (136kB)
     |████████████████████████████████| 143kB 9.9MB/s 
Collecting certifi>=2017.4.17
  Downloading https://files.pythonhosted.org/packages/5e/a0/5f06e1e1d463903cf0c0eebeb751791119ed7a4b3737fdc9a77f1cdfb51f/certifi-2020.12.5-py2.py3-none-any.whl (147kB)
     |████████████████████████████████| 153kB 10.2MB/s 
Collecting idna<3,>=2.5
  Using cached https://files.pythonhosted.org/packages/a2/38/928ddce2273eaa564f6f50de919327bf3a00f091b5baba8dfa9460f3a8a8/idna-2.10-py2.py3-none-any.whl
Installing collected packages: chardet, urllib3, certifi, idna, requests
Successfully installed certifi-2020.12.5 chardet-3.0.4 idna-2.10 requests-2.25.0 urllib3-1.26.2
$ ll
total 0
drwxr-xr-x   3 hoge_fuga  1189740132    96 12 12 06:58 bin
drwxr-xr-x   7 hoge_fuga  1189740132   224 12 12 06:58 certifi
drwxr-xr-x   8 hoge_fuga  1189740132   256 12 12 06:58 certifi-2020.12.5.dist-info
drwxr-xr-x  43 hoge_fuga  1189740132  1376 12 12 06:58 chardet
drwxr-xr-x  10 hoge_fuga  1189740132   320 12 12 06:58 chardet-3.0.4.dist-info
drwxr-xr-x  11 hoge_fuga  1189740132   352 12 12 06:58 idna
drwxr-xr-x   8 hoge_fuga  1189740132   256 12 12 06:58 idna-2.10.dist-info
drwxr-xr-x  21 hoge_fuga  1189740132   672 12 12 06:58 requests
drwxr-xr-x   8 hoge_fuga  1189740132   256 12 12 06:58 requests-2.25.0.dist-info
drwxr-xr-x  17 hoge_fuga  1189740132   544 12 12 06:58 urllib3
drwxr-xr-x   8 hoge_fuga  1189740132   256 12 12 06:58 urllib3-1.26.2.dist-info
$ touch lambda_function.py 
```


## 3. ロググループにサブスクリプションフィルターを設定する
CloudWatchAgentの設定がうまくいってログが転送され出すと、CloudWatchLogsに設定ファイルに設定したロググループ名でロググループが追加されていると思います。
今すぐAWSコンソールを開いて確認してみましょう！
必要に応じてログの保持期間などを変更しておきましょう。
ロググループの一覧からでも詳細からでも構いませんので、**アクション**から**Lambda サブスクリプションフィルターを作成**を選択します。
以下を参考に設定してみましょう。
![サブスクリプションフィルター設定例.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139745/bb15284b-5dea-83ef-3f19-18c39bce3990.png)
一つのロググループに対して最大で二つのサブスクリプションフィルターが作成できます。（サポートにて確認済み）
また、面倒ですが、一度登録したサブスクリプションフィルターはLambda側でのみ削除できます。
AWS CLIを使えばこの限りではないと思います。（そこまでは調べてないです。。）
うまく連携できるとLambda側で以下のように確認できます。

![連携確認.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139745/2ff17d59-2d7c-0ed4-848a-e3f433ec45f0.png)
ここまでできれば、Slack通知されるはずです！
できてなかったら、Lambdaの実行ログなどを確認してトラブルシューティング頑張ってください！

# おわりに
比較的コーディング少なめでログの監視ができるものです。
ElasticSearch版のサブスクリプションフィルターもあるようなので、より高度なログ解析のできるようです。
機会があれば、チャレンジしてみたいものです^^