Lambdaリアルタイムログ監視、ログレベル毎に通知先をSNSで一括管理してSlackやメールで通知したい

# やりたいこと

- CloudWatch Alarmの通知先
- AWS Lambdaのログ監視の通知先

を通知レベルに応じてSNSでソースコード変更なしに柔軟に一括管理できるようにしたく実現方式を検討したメモ。

例えば、以下のように通知レベル毎に通知先を定義する。

|通知レベル<br>(SNSトピック)|通知先|定義|
|:---:|:---|:---|
|INFO|・Slack #log_info|システムログ。調査時の効率化目的で普段は見ないログ。|
|WARN|・Slack #log_warn|異常事態の予兆を知らせる通知。障害の未然防止に繋がるアクション検討するため。|
|ERROR|・Slack #log_error<br>・メール サービス運用者のML|異常事態が明らかな重大なエラー。|

検討した結論はこんな感じ。
<img width="600" alt="2017-11-28 0.48.18.png" src="https://qiita-image-store.s3.amazonaws.com/0/51734/63a77e20-2972-5537-7ba6-8f4f7c59b470.png">

Lambdaの実行ログはサブスクリプションフィルタでフィルターをかけて、各SNSへ通知するLambdaを噛ませて、SNSからSlack通知はLambdaのBlueprint「cloudwatch-alarm-to-slack」をCloudWatch経由以外の通知にも対応できるよう修正して対応した。

#ログ監視対象のLambda

```index.js
'use strict';

exports.handler = (event, context, callback) => {

    // INFO通知したい時
    console.log('SNS-INFO Subject-INFO Message-INFO');
    // WARN通知したい時
    console.log('SNS-WARN Subject-WARN Message-WARN');
    // ERROR通知したい時
    console.log('SNS-ERROR Subject-ERROR Message-ERROR');

    callback(null);
};
```
以下のログパターンに合致した場合に通知できるようにした。
>`SNS-<LEVEL> <SUBJECT> <MESSAGE>`

#CloudWatchLogsのサブスクリプションフィルタでSNS通知まで

##サブスクリプションフィルタの設定方法

ログ監視対象のLambdaに対してCloudWatchLogsから対象ロググループに対してLambdaストリーミングを設定する。ログの形式は「AWS Lambda」を選択してサブスクリプションフィルターとして以下を登録。

```
[timestamp=*Z, request_id="*-*", level="SNS-*", subject, message]
```

[フィルタとパターンの構文](http://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html)を参考にSNS通知時の件名(`subject`)とメッセージ本文(`message`)をLambdaで扱える変数として自動抽出してくれる機能を使った。


##SNSへ通知するLambda

```index.js
'use strict';

const zlib = require('zlib');
const aws = require('aws-sdk');
const sns = new aws.SNS({apiVersion: '2010-03-31'});

function publishToSnsTopic(topicArn, subject, message) {
    sns.publish({
        TopicArn: topicArn,
        Subject: subject,
        Message: message
    }, function (err, data) {
        if (err) { console.log('[Error] Failed sns published: ' + data);}
    });
}

exports.handler = (event, context, callback) => {
    const base64Logs = new Buffer(event['awslogs']['data'], 'base64');
    zlib.gunzip(base64Logs, function(err, binary) {
        var data = JSON.parse(binary.toString('utf8'));

        if(!Array.isArray(data.logEvents)) { return; }
        data.logEvents.forEach(function(d) {
           if (d.extractedFields === undefined ) { return; }
           if (d.extractedFields.level === undefined ) { return; }

           var topicArn;
           switch (d.extractedFields.level) {
               case "SNS-INFO":
                   topicArn = process.env.SNS_INFO_TOPIC_ARN;
                   break;
               case "SNS-WARN":
                   topicArn = process.env.SNS_WARN_TOPIC_ARN;
                   break;
               case "SNS-ERROR":
                   topicArn = process.env.SNS_ERROR_TOPIC_ARN;
                   break;
               default:
                   console.error('[ERROR] Level is not matched: ' + data.message);
                   return;
           }
           publishToSnsTopic(topicArn, d.extractedFields.subject, d.extractedFields.message);
        });
    });
};
```
ソースコードの変更なくSNSを変更できるようLambdaの環境変数でSNSのARNを管理できるようにした。
Lambdaのロールには`sns:Publish`のポリシー許可が必要。

#SNSからSlack通知するLambda

Blueprintの「cloudwatch-alarm-to-slack」を以下のdiffのように修正。
CloudWatch経由じゃないとJSONパースエラーになるので、CloudWatchLogs経由も対応できるように修正した。
KMSとかの設定は[こちら](https://dev.classmethod.jp/cloud/aws/lambda-blueprint-slack-cloudwatch/)の記事を参考にした。

```diff
@@ -84,18 +84,23 @@ function postMessage(message, callback) {
 }
 
 function processEvent(event, callback) {
-    const message = JSON.parse(event.Records[0].Sns.Message);
-
-    const alarmName = message.AlarmName;
-    //var oldState = message.OldStateValue;
-    const newState = message.NewStateValue;
-    const reason = message.NewStateReason;
+    const messageStr = event.Records[0].Sns.Message;
+    const subject = event.Records[0].Sns.Subject;
 
     const slackMessage = {
         channel: slackChannel,
-        text: `${alarmName} state is now ${newState}: ${reason}`,
+        text: subject + "\n" + messageStr
     };
 
+    try {
+        const message = JSON.parse(messageStr);
+        const alarmName = message.AlarmName;
+        //var oldState = message.OldStateValue;
+        const newState = message.NewStateValue;
+        const reason = message.NewStateReason;
+        slackMessage.text = `${alarmName} state is now ${newState}: ${reason}`;
+    } catch (e) {}
```

これで完了。

##Lambdaのログ監視のSlack通知

<img width="300" alt="CloudWatchLogsError1.png" src="https://qiita-image-store.s3.amazonaws.com/0/51734/a3ebca7a-515a-e292-9298-e328dc13a400.png">

##CloudWatch AlertのSlack通知

<img width="800" alt="CloudWatchError2.png" src="https://qiita-image-store.s3.amazonaws.com/0/51734/c2ae6266-6d18-da9c-d5b4-5675ed22d084.png">



#参考

*　[cloudwatchlogs -> lambda -> SNSを試してみた](https://qiita.com/sadayuki-matsuno/items/83b70ee97755e534764f)
*　[AWS Lambda で CloudWatch Logs のログ本文をSlack通知(2)](http://htnosm.hatenablog.com/entry/2016/05/06/090000)