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

  • 23
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

概要

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で作成し、それを読み込んだ。ポリシーはAmazonAPIGatewayAdministratorAWSLambdaFullAccessの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はそれを大いに助けてくれる。

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

参考

この投稿は AWS Advent Calendar 20155日目の記事です。