fluct+API Gateway+Lambda+DynamoDBでJavaScriptのエラーログ収集

## **概要**

SPAなど、JavaScriptの比重が大きいアプリケーションにて、クライアントででたエラーログをサーバーに保存しておき、問題発生時に、解析の助けとしたい。



## **いつエラーを送信するか**

実装パターンを調べたところ、以下の2つの組み合わせが採用事例としては多いよう。

* window.onerror
* 特定のリクエスト時

`特定のリクエスト時`とは、特にエラーを検知したい関数でエラーログを設置しておく方法である。<a href="https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onerror" target="_blank">window.onerror</a>でエラーを検知する際は、確実に送信できる内容が、エラーメッセージ、ファイル名(URL)、行番号のみで、それ以外はブラウザによって異なる。

また、<a href="https://github.com/stacktracejs/stacktrace.js" target="_blank">stacktrace.js</a>を使うと、いい感じにエラーを表示できるようだった。具体的には以下の様な感じで、`functionName`が、エラーがおこるまでに辿ってきた関数、`fileName` `lineNumber` が該当箇所という具合だ。

```javascript
{
  stack: [
    {functionName: 'fn', fileName: 'file.js', lineNumber: 32, columnNumber: 1},
    {functionName: 'fn2', fileName: 'file.js', lineNumber: 543, columnNumber: 32},
    {functionName: 'fn3', fileName: 'file.js', lineNumber: 8, columnNumber: 1}
  ]
}
```

**結果、今回は導入として`window.onerror`で検知することにした。**



## **ログの保存**

SPAなどにも実装したいことから、APIでログを保存したい。サーバーサイドの実装はRailsを使用することが多いため、以下を検討した。

* Rails API
* Node.js API
* API Gateway+Lambda

RailsとNode.jsを使う場合、通常のAPIを作成する。同じAPIをつくるなら、(既に環境が構築されている)Railsでつくったほうが早く実装できそうというのと、APIサーバーを2つ作るコストもあった。

その点Lambdaだと、ログサーバーだけであれば、APIをRailsやNode.jsでつくるよりもコスト低く実装可能だと考えた。また、保存先をかえるだけで、様々なサービスに応用可能で、例えば、サーバーサイドの環境がない静的なサイトへの組み込みもできる。

**よって、API Gateway+Lambdaを採用することにした。**



## **ログの保存先**

ログの保存先では、実装コスト面から以下を検討した。as a serviceの使用も検討したが、Fluentdなどの導入も検討していたため、現状では、ログを保存するまでを目的とすることにした。

* DynamoDB
* MongoDB

DynamoDBは、<a href="https://aws.amazon.com/jp/sdk-for-node-js/" target="_blank">Node.js AWS SDK</a>で比較的簡単に操作ができそうであった。しかし、DynamoDBは基本的にkey-value型であり、検索方法に制限があるときがあるようだった。

その点、MongoDBにそのデメリットはない。しかし、Lambdaとの相性の良さや、実装コスト、スケーラビリティ、また、同じNoSQLならDynamoDBつかってみたい!!**ということで、DynamoDBを採用することにした。**




## **fluctでLambdaのコード管理**

Lamda関数を書いてゆこう、と思ったが、DynamoDBを扱うのにいくつかのNode Modulesが必要だったため、手元でコードを作成し、それをzip化する必要があった。

**そんなのいちいち手動でやってらんないよ、と思ったので、<a href="https://github.com/fluct/fluct" target="_blank">fluct</a>というツールを使うことにした。**これは、手元のコードを、Amazon API GatewayとAmazon Lambda上に簡単にデプロイしてくれるというツールである。

基本的にはReadme通りやれば問題なかったが、いくつか注意点を書いておく。

### **AWS credentialsの設定**

AWS credentialsを設定するために、専用のユーザーをIAMで作成し、それを読み込んだ。ポリシーは`AmazonAPIGatewayAdministrator`と`AWSLambdaFullAccess`の2つをアタッチした。

* <a href="https://github.com/fluct/fluct/blob/master/docs/faq.md#how-to-configure-aws-credentials" target="_blank">How to configure AWS credentials?</a>

### **roleNameの設定**

DynamoDBを操作したいため、作成したroleには、`AWSLambdaBasicExecutionRole`とは別に、`AmazonDynamoDBFullAccess`もアタッチした。

### **regionの設定**

regionは以下のように設定する必要があった。

```json
  "fluct": {
    "accountId": "",
    "restapiId": "",
    "roleName": "lambdaLoggingApp",
    "region": "ap-northeast-1"
  },
```

### **Node Modules**

必要なnode_modulesはプロジェクトディレクトリ配下のpackage.jsonに追加する必要があった。その際、各モジュールの`preDependency`も追加する必要がある。

### **CORS対応**

以下の記事に従うこと。

* [API Gateway でメソッドの CORS を有効にする - Amazon API Gateway](http://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/how-to-cors.html)

`npm run deploy`をすると、該当メソッドの`Method Execution>>Integration Response>>Header Mappings>>Access-Control-Allow-Origin>>Mapping value`がリセットされてしまうので、fluctが対応するまでは手動で修正し、再度deployをし直す必要がある。

## **Lambda関数の実装**

たいしたことはしていないが、以下の様なコードを書いた。

```javascript
var Promise = require('bluebird');
var uuid = require('uuid');
var AWS = require('aws-sdk');
var dynamodb = new AWS.DynamoDB({region: 'ap-northeast-1'});

function putItem(query) {
  return new Promise(function(resolve, reject) {
    var params = {
      TableName: query.tableName,
      Item: {
        'Id': {'S': uuid.v1()},
        'Date': {'N': query.date},
        'Stack': {'S': query.stack},
      },
    };
    dynamodb.putItem(params, function(err, data) {
      if (err) return reject(err);
      return resolve(JSON.stringify(data));
    });
  });
}

exports.handler = function(event, context) {
  if (!event || !event.requestParameters) {
    return context.done(null, {
      'status': 404,
      'message': 'Invalid Parameters'
    });
  };

  var query = {
    tableName: event.requestParameters.tableName,
    stack: event.requestParameters.stack,
    date: event.requestParameters.date,
  };

  putItem(query)
    .then(function() {
      context.done(null, {
        'status': 200,
        'message': 'OK'
      });
    })
    .catch(function(err) {
      context.done(null, {
        'status': null,
        'message': err,
      });
    })
};
```

テストは、eventをモックしたLambdaコンソールのテストを利用している。コード量が増えてきたら、手元でできるようにユニットテストを導入する必要があるかもしれない。


## **API詳細**

最終的なAPIは以下のようになった。パラメータ`tableName`を変えるだけで、他のアプリケーションでも使い回しが可能になるようにした。

| 　 | 説明 |
| ------ | ------ |
| 役割 | ログをDynamoDBに保存する。 |
| URL | https://your-api/log |
| メソッド | `POST` |

| リクエストパラメータ | 説明 | 型 | 必要性 |
| ------ | ------ | ------ | ------ |
| tableName | DynamoDBのテーブル名 | 文字列 | 必須 |
| date | 保存時の時刻(YYYYMMDD) | 数値 | 必須 |
| stack | stacktrace.jsで取得したエラーの情報。配列をJSON.stringifyしている。 | 文字列 | 必須 |


## **DynamoDB詳細**

DynamoDBのカラムは以下のように構成した。

| カラム名 | 説明 | 型 | 役割 |
| ------ | ------ | ------ | ------ |
| Id | uuidでつくったハッシュ値 | 文字列 | プライマリキー |
| Date | 保存時の時刻(YYYYMMDD) | 数値 | プライマリキーのソートキー |
| Stack | stacktrace.jsで取得したエラーの情報。配列をJSON.stringifyしている。 | 文字列 | |

データの内容は、`DynamoDBのコンソール>>データを確認したいサービスのテーブルを選択>>フィルターの追加>>属性にDate、値にYYYYMMDDHHmmssで日付を入力`し、数値として範囲を指定して検索する。

こちらに関してはいくつか思うことがあった。

* Lambda⇄DynamoDBの実装コスト低い!
* Dateで絞り込む感じかあ...
* `Stack`はまとめてしまっているが、いずれいい感じにできればと考えている...
* グラフ化とかもやってみたい...



## **クライアントへの組み込み**

クライアントへは以下の様なコードを、utilファイルに組み込み、exportした。

```javascript

import request from 'superagent';
import Promise from 'bluebird';
import StackTrace from 'stacktrace-js';
import moment from 'moment';
import { LOGGING_DB, LOGGING_ENDPOINT } from 'config';

/**
 * エラーログをポストする。
 * @param {Object} query
 * @return {Object}
 */
function _postLog(query) {
  return new Promise((resolve, reject)=> {
    request
      .post(LOGGING_ENDPOINT)
      .send(query)
      .end(function(err, res) {
        if (err) return reject(err);
        return resolve(res.text);
      });
  });
}

/**
 * エラーログ収集のlambda関数を実行する
 */
export function logError() {
  window.onerror = function(msg, file, line, col, error) {
    StackTrace
      .fromError(error)
      .then((stackframes) => {
        const query = {
          tableName: LOGGING_DB,
          stack: JSON.stringify(stackframes),
          date: moment().format('YYYYMMDDHHmmss'),
        };
        return _postLog(query);
      })
      .catch((err) => {
        console.log(err);
      });
  };
}
```

npmモジュールにして、以下のように呼び出せると綺麗かなと思う。

```javascript
import Log from 'error-log-module';

const log = new Log({
  LOGGING_DB,
  LOGGING_ENDPOINT,
});

log.start();
```



## **まとめ**

fluct+API Gateway+Lambda+DynamoDBを使用し、JavaScriptのエラーログ収集の実装をした。API Gateway+Lambdaはこのような用途にぴったりだと思ったし、fluctはそれを大いに助けてくれる。

今後も少しづつ、サーバーレスな構成を試していきたいと思った。何か間違いや改善できそうな点があればご指摘ください。



## **参考**

* <a href="http://qiita.com/grapswiz/items/4e97968f3d3df97a4c76" target="_blank">大手Webサービスがクライアント側で発生したJavaScriptのエラーをどう収集しているのか まとめ - Qiita</a>
* <a href="http://kitak.hatenablog.jp/entry/2014/10/08/093056" target="_blank">JavaScriptのエラーを検知したり、スタックトレースをいいかんじに表示する術</a>
* <a href="https://bugsnag.com/blog/js-stacktraces/" target="_blank">JS stacktraces. The good, the bad, and the ugly. </a>
* <a href="http://devdev.hatenadiary.jp/entry/2014/02/03/224709" target="_blank">MongoHQとAmazon DynamoDBの比較 </a>
* <a href="http://r7kamura.hatenablog.com/entry/2015/08/08/233055" target="_blank">fluctでAPI GatewayやLambdaと仲良くやる - ✘╹◡╹✘</a>
* <a href="http://stackoverflow.com/questions/14836600/querying-dynamodb-by-date" target="_blank">Querying DynamoDB by date - Stack Overflow</a>
* <a href="http://blog.brains-tech.co.jp/entry/2015/09/30/222148" target="_blank">ここにハマった！DynamoDB</a>
* <a href="http://qiita.com/inouet/items/b2246d2419daf1788f63" target="_blank">Node.js からDynamoDBを操作するサンプル - Qiita</a>
* <a href="http://www.slideshare.net/keisuke69/aws-lambda-46129981" target="_blank">AWS Lambdaを紐解く</a>