ELBのAccess Logを解析して特定の条件でLambda経由でSNS通知

### 前書き
Amazon Loadbalancer(ELB)を使っていると、『ELBのAccess logを解析して特定の条件(例えばStatus code != 200)が存在すればAlarmを飛ばす』と言う事をやりたい事が多いと思います。  

Status CodeであればCloudWatchの`HTTPCode_Backend_5XX`等のMetricsを使えば簡単に出来ます([ELBのCloud Watch Metricはここ参照](http://docs.aws.amazon.com/ja_jp/ElasticLoadBalancing/latest/DeveloperGuide/elb-cloudwatch-metrics.html))。このMetricsの条件に当てはまった時に、email等のAlarmを飛ばす様にAWS SNSを設定すれば良いです。が、これでAlarm通知は受け取れるんですが、Requestの詳細(URLとかRequest元のIPとか)を確認しようと思ったときに、S3に書き出されているELBの生Access logをみるしか有りません。これ、手作業でやろうとすると結構苦痛です。何故かLog fileがタブ区切りでもカンマ区切りでも無く、半角スペース区切りですし、、、。  

ELBがS3にLogを書き出したタイミングで、AWS Lambdaを起動させて、Lambdaのnode.js内でAccess logを解析して、必要な情報を切り出してEmailの本文に含めれば楽ジャン、と思い、その設定手順やLambdaのscriptをチラ裏しておきます。

### 設定でのハマり箇所
* ELBのLogがS3に吐きだされるAWS Regionと同じRegionに、Lambda, SNS Topicを作成してください
* LambdaのFunctionを作成すると、Lambda functionを動かす為のIAM Roleを新規で作る様に進められます(既存の物を使い廻す事も可能)。このIAM roleに対して、`AmazonSNSFullAccess`をAttach Policyするのを忘れずに。これによりLambda FunctionからSNS通知がさらに発火出来る様になります
* Lambda functionを作成後、`Add event source`で、監視するS3 bucketを設定して下さい
* SNSの`Topic`と`Subscriptions`を**Lambdaと同じAWS region**に作成し、Topicを発火させるとemail通知が行くように事前設定しておいて下さい。また TopicのARNをメモしておいてください

### Debugでのハマり箇所
* まず、Lambdaの編集画面で、左ペインの`Sample Event`に`S3 Put`を指定して、`Invoke`をクリックすると、`Error getting object HappyFace.jpg from bucket sourcebucket...`というエラーがでます。`"key": "HappyFace.jpg"`と`"name": "sourcebucket"`を先ほど`Add event source`で設定したS3 bucket名とそのbucketに実際にあるlog fileに書き換えて、再度`Invoke`をクリックすると、このlog fileがS3 bucketに追加されたという想定でScriptが動作してくれます
* Lambdaの編集画面で`Invoke`させて動かない場合は、CloudWatch -> LogsにFullのDebug logが吐き出されます

### Lambdaのcode
* TopicArnは自分の物に置き換えてください
* 処理の流れは以下の通りです
 * S3 Bucketに変更があったら、1行づつ取り出して、`checkString`に掛ける
 * `checkString`は[ELB Access Logの仕様](http://docs.aws.amazon.com/ElasticLoadBalancing/latest/DeveloperGuide/access-log-collection.html)を元に、半角スペース区切りで9番目の値が`backend_status_code`、区切りの2番目が`URL`、、、と必要な要素を取り出す
 * Status Codeが200以外の場合は、その行を`snsParams.Message`に追記していき、最後に`sns.publish`で結果をSNS発火させる


```js
var aws = require('aws-sdk');
var s3 = new aws.S3({apiVersion: '2006-03-01'});
var sns = new aws.SNS({apiVersion: '2010-03-31'});
var snsParams = {
  Message: '',
  Subject: 'ELB log check alarm',
  TopicArn: 'arn:aws:sns:us-east-1:xxxxxxxx:yyyyyyyyyy'
};

exports.handler = function(event, context) {
  var bucket = event.Records[0].s3.bucket.name;
  var key = event.Records[0].s3.object.key;
  s3.getObject({Bucket: bucket, Key: key}, function(err, data) {
    if (err) {
      console.log("Error getting object " + key + " from bucket " + bucket +
        ". Make sure they exist and your bucket is in the same region as this function.");
      context.fail('Error', "Error getting file: " + err);
    }

    var contentBody = data.Body.toString(data.ContentEncoding);
    snsParams.Message += 'Upload filename:' + key.substr(key.lastIndexOf('/') + 1) + '\n';
    snsParams.Message += 'Data\tStatusCode\tSource IP\tURL\tUserAgent\n';

    contentBody.split('\n').forEach(function (line) {
      checkString(line);
    });

    sns.publish(snsParams, function() {
      context.succeed("success");
    });
  });
};

function checkString(line) {
  if (line !== '') {
    var status_code = line.split(' ')[8];
    var source_ip = line.split(' ')[2].split(':')[0];
    if (Number(status_code) != 200) {
      snsParams.Message += line.split(' ')[0].split('.')[0] + '\t' + status_code + '\t' + source_ip+ '\t' 
       + line.split('"')[1] + '\t' + line.split('"')[3] + '\n' ;
    }
  }
}

```