アプリケーションのログをCloudWatch経由で通知してみた

前回に引き続き社内システムのお話です。

社内システムをAWSのCloudWatchに集約していますが、プロジェクト内でChatWorkを利用しているため、
チャットグループを作成し、通知するということをしてみましたので紹介します。
※ちなみに今回は一部AWSの方にもサポートいただきました

#目次
[1.今回の構成について](#今回の構成について)
[2.AWS Systems Managerのパラメータストアで設定ファイルを準備する](#aws-systems-managerのパラメータストアで設定ファイルを準備する)
[3.AWS Systems ManagerのAmazonCloudWatch-ManageAgentを実行する](#aws-systems-managerのamazoncloudwatch-manageagentを実行する)
[4.Lambda経由でメール及びChatWorkへ通知する](#lambda経由でメール及びchatworkへ通知する)
[5.詰まった点](#詰まった点)
[6.まとめ](#まとめ)
[7.参考URL](#参考url)

## 今回の構成について

- 今回は統合CloudWatchエージェントを利用しログを収集する
- CloudWatchのLog groupsに収集された際に一部の単語が含まれている場合にメールとChatWorkに通知する

## AWS Systems Managerのパラメータストアで設定ファイルを準備する

以下の内容ではパラメータを登録する
※collect_listの中身は見やすくするように一部内容を削っています

```json
{
  "agent": {
   "metrics_collection_interval": 60,
   "run_as_user": "root",
   "debug": false
  },
  "logs": {
   "logs_collected": {
    "files": {
     "collect_list": [{
      "file_path": "/var/log/messages",
      "log_group_name": "Production-/var/log/messages",
      "log_stream_name": "{instance_id}",
      "timestamp_format": "%Y %m %d %H:%M:%S",
      "multi_line_start_pattern": "{timestamp_format}"},
     {"file_path": "/var/log/tomcat/catalina.out",
      "log_group_name": "Production-/var/log/tomcat/catalina.out",
      "log_stream_name": "{instance_id}",
      "timestamp_format": "%Y %m %d %H:%M:%S",
      "multi_line_start_pattern": "{timestamp_format}"},
     {"file_path": "/var/log/nginx/tomcat_access.log",
      "log_group_name": "Production-/var/log/nginx/tomcat_access.log",
      "log_stream_name": "{instance_id}",
      "timestamp_format": "%Y %m %d %H:%M:%S",
      "multi_line_start_pattern": "{timestamp_format}"}
   ]}}
   },
   "metrics": {
    "append_dimensions": {
     "AutoScalingGroupName": "${aws:AutoScalingGroupName}",
     "ImageId": "${aws:ImageId}",
     "InstanceId": "${aws:InstanceId}",
     "InstanceType": "${aws:InstanceType}"},
     "metrics_collected": {
     "cpu": {
     "measurement": [
     "cpu_usage_idle",
     "cpu_usage_iowait",
     "cpu_usage_user",
     "cpu_usage_system"
     ],
     "metrics_collection_interval": 60,
     "totalcpu": false
     },
     "disk": {
     "measurement": [
     "used_percent",
     "inodes_free"
     ],
     "metrics_collection_interval": 60,
     "resources": [
     "*"
     ]
     },
     "diskio": {
     "measurement": [
     "io_time"
     ],
     "metrics_collection_interval": 60,
     "resources": [
     "*"
     ]
     },
     "mem": {
     "measurement": [
     "mem_used_percent"
     ],
     "metrics_collection_interval": 60
     },
     "swap": {
     "measurement": [
     "swap_used_percent"
     ],
     "metrics_collection_interval": 60
     }
     }
   }
 }
 ```

## AWS Systems ManagerのAmazonCloudWatch-ManageAgentを実行する

パラメータの登録ができたので、統合CloudWatchエージェントを利用してCloudWatchにログを収集させます

1. AWS System ManagerのRun Commandでコマンドの実行をする
2. コマンドドキュメントを「AmazonCloudWatch-ManageAgent」で絞りこみ選択する
3. コマンドのパラメータの「Optional Configuration Location」に先程作成したパラメータ名を入力する
4. ターゲットは「インスタンスを手動で選択する」で該当のインスタンスを選択する
5. その他はデフォルトのままです
6. 出力オプションのS3への出力はおまかせです（自分はチェック外しました）
7. 「実行」ボタン押す！
8. 成功するとステータスが「成功」となるのを確認する
9. 少し時間をおいてCloudWatchのロググループにパラメータで指定したファイルが収集されていることを確認します

## Lambda経由でメール及びChatWorkへ通知する

ログの収集ができたので、次はLambda経由でメールとChatWorkのチャットグループへ通知します

1.Lambdaで関数を作成します（今回はPythonを利用）

* ChatWorkのAPIキーやエンドポイント、通知先のチャットグループのIDは環境変数で値を登録済
* ChatWorkへ通知する際に通知内容の文字数が長い場合に通知できないようなので適当な長さで切り詰めてます
* メールはSNSを利用しており事前にトピック作成し環境変数として登録済

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

chatwork_api_key = os.environ['CHATWORK_API_KEY'];
chatwork_endpoint = os.environ['CHATWORK_ENDPOINT'];
chatwork_roomid = os.environ['CHATWORK_ROOMID'];

def lambda_handler(event, context):
    data = zlib.decompress(base64.b64decode(event['awslogs']['data']), 16+zlib.MAX_WBITS)
    data_json = json.loads(data)
    log_json = json.loads(json.dumps(data_json["logEvents"][0], ensure_ascii=False))

    # メール通知
    try:
        print("メール通知開始")
        sns = boto3.client('sns')

        #SNS Publish
        publishResponse = sns.publish(
            TopicArn = os.environ['SNS_TOPIC_ARN'],
            Message = log_json['message'],
            Subject = os.environ['ALARM_SUBJECT']
        )
        print("メール通知完了")

    except Exception as e:
        print("メール通知エラー")
        print(e)
    
    # チャットワーク連携
    try:
        print("チャットワーク連携開始")
        # チャットワーク連携
        subject=os.environ['ALARM_SUBJECT']
        body = log_json['message'][:1500]
        post_message_url = '{}/rooms/{}/messages'.format(chatwork_endpoint, chatwork_roomid)
        headers = { 'X-ChatWorkToken': chatwork_api_key }
        message = '[info][title](devil){}(devil)[/title]{}[/info]'.format(subject, body)
        params = { 'body': message }
        res = requests.post(post_message_url, headers=headers, params=params)
        print(message)
        print("チャットワーク連携終了")
    except Exception as ee:
        # エラー出力
        print("チャットワーク連携エラー")
        print(ee)
```

2.Lambdaの設定からトリガーを追加します
3.トリガーは「CloudWatch Logs」を選択して、ロググループは対象のものを選択、フィルター名は適当につけて、トリガーのフィルターパターンは「ERROR」とし追加ボタン押します

これで下準備は整ったので実際にエラー出して動作確認します。
ここまでやってればおそらく通知されるのですが、今回色々と詰まったこともメモとして残しておきます

## 詰まった点

- CloudWatchのロググループの収集日時と実際のログ日時にズレがある
  - 原因は、CloudWatchエージェントの設定ファイル（パラメータストアの内容）で「timestamp_format」を指定していなかった
  - 「timestamp_format」を指定しない場合はログの送信時刻がログのタイムスタンプとして登録される
- 統合CloudWatchエージェントでログが送信されなくなった
  - 原因は、CloudWatchエージェントの設定ファイル（パラメータストアの内容）「multi_line_start_pattern」が適切に設定されていなかった
  - 「multi_line_start_pattern」は複数行にわたるログを一つのログイベントにまとめるための設定となり、正規表現または「{timestamp_format}」 を指定することを想定されている
  - 当初は、「multi_line_start_pattern」に「{datetime_format}」を設定していたが、廃止が予定されているため「{timestamp_format}」に変更した


## まとめ

今回はCloudWatchで収集されたログをメールまたはChatWorkに通知する内容を紹介しました。
エラーの内容をChatWorkに通知することで、ユーザより早くエラーを検知して対応できることができるかと思います。
社内システムではエラー時のログ内容がイケてないので見直して、調査などの着手ができるようにすると更に便利になるかと思います。

## 参考URL

https://qiita.com/ktsuchi/items/60c0b0e01938af8cb50c

https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/CloudWatch-Agent-Configuration-File-Details.html#CloudWatch-Agent-Configuration-File-Logssection