概要
SPAなど、JavaScriptの比重が大きいアプリケーションにて、クライアントででたエラーログをサーバーに保存しておき、問題発生時に、解析の助けとしたい。
いつエラーを送信するか
実装パターンを調べたところ、以下の2つの組み合わせが採用事例としては多いよう。
- window.onerror
- 特定のリクエスト時
特定のリクエスト時
とは、特にエラーを検知したい関数でエラーログを設置しておく方法である。window.onerrorでエラーを検知する際は、確実に送信できる内容が、エラーメッセージ、ファイル名(URL)、行番号のみで、それ以外はブラウザによって異なる。
また、stacktrace.jsを使うと、いい感じにエラーを表示できるようだった。具体的には以下の様な感じで、functionName
が、エラーがおこるまでに辿ってきた関数、fileName
lineNumber
が該当箇所という具合だ。
{
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は、Node.js AWS SDKで比較的簡単に操作ができそうであった。しかし、DynamoDBは基本的にkey-value型であり、検索方法に制限があるときがあるようだった。
その点、MongoDBにそのデメリットはない。しかし、Lambdaとの相性の良さや、実装コスト、スケーラビリティ、また、同じNoSQLならDynamoDBつかってみたい!!ということで、DynamoDBを採用することにした。
fluctでLambdaのコード管理
Lamda関数を書いてゆこう、と思ったが、DynamoDBを扱うのにいくつかのNode Modulesが必要だったため、手元でコードを作成し、それをzip化する必要があった。
**そんなのいちいち手動でやってらんないよ、と思ったので、fluctというツールを使うことにした。**これは、手元のコードを、Amazon API GatewayとAmazon Lambda上に簡単にデプロイしてくれるというツールである。
基本的にはReadme通りやれば問題なかったが、いくつか注意点を書いておく。
AWS credentialsの設定
AWS credentialsを設定するために、専用のユーザーをIAMで作成し、それを読み込んだ。ポリシーはAmazonAPIGatewayAdministrator
とAWSLambdaFullAccess
の2つをアタッチした。
roleNameの設定
DynamoDBを操作したいため、作成したroleには、AWSLambdaBasicExecutionRole
とは別に、AmazonDynamoDBFullAccess
もアタッチした。
regionの設定
regionは以下のように設定する必要があった。
"fluct": {
"accountId": "",
"restapiId": "",
"roleName": "lambdaLoggingApp",
"region": "ap-northeast-1"
},
Node Modules
必要なnode_modulesはプロジェクトディレクトリ配下のpackage.jsonに追加する必要があった。その際、各モジュールのpreDependency
も追加する必要がある。
CORS対応
以下の記事に従うこと。
npm run deploy
をすると、該当メソッドのMethod Execution>>Integration Response>>Header Mappings>>Access-Control-Allow-Origin>>Mapping value
がリセットされてしまうので、fluctが対応するまでは手動で修正し、再度deployをし直す必要がある。
Lambda関数の実装
たいしたことはしていないが、以下の様なコードを書いた。
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した。
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モジュールにして、以下のように呼び出せると綺麗かなと思う。
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はそれを大いに助けてくれる。
今後も少しづつ、サーバーレスな構成を試していきたいと思った。何か間違いや改善できそうな点があればご指摘ください。
参考
- 大手Webサービスがクライアント側で発生したJavaScriptのエラーをどう収集しているのか まとめ - Qiita
- JavaScriptのエラーを検知したり、スタックトレースをいいかんじに表示する術
- JS stacktraces. The good, the bad, and the ugly.
- MongoHQとAmazon DynamoDBの比較
- fluctでAPI GatewayやLambdaと仲良くやる - ✘╹◡╹✘
- Querying DynamoDB by date - Stack Overflow
- ここにハマった!DynamoDB
- Node.js からDynamoDBを操作するサンプル - Qiita
- AWS Lambdaを紐解く