AWS Lambda でパフォーマンス監視

サイトのパフォーマンスを監視するために、定期的にスクレイピングをしようと思い、
サーバーを立てるのも面倒なのでAWSのLambdaでしのごうとしたら意外と大変だったのでメモ

# 要約

* Nodejsが古い(v0.10.36)
* seleniumは使わないで、child_processから直接phantomjs(最新: v2.1.1)起動
* performance API には頼らない
* 金があるならNewRelic

# Nodejsが古い

最初はSeleniumでやればすぐにできるだろうと考えていたのですが、
Lambdaで使用できるNode.jsは0.10.36と古く、Seleniumではサポート対象外です。

selenium-webdriverのバージョンを2.44.0ぐらいにすると動かせますが、phantomjsとの相性が悪いようで、
seleniumからphantomjsを実行しようとすると、サーバーとつながらない旨のエラーをはいてきます。
おそらくphantomjsのバージョンがあっていないのだと思いますが、どのバージョンが正しいか見つけるのが面倒だったので、
直接phantomjsを起動しました。

# Phantomjsでperformance API

phantomjsでも、performance APIはサポートされていますが、
`performance.timing`がないため、情報が片手落ちです。

そのため、`onResourceRequested`を使用して、開始時のリクエストなど、必要な情報を取得します。
また、`mark()`もないため、 `new Date().getTime()`で代用します。

# Lambdaにアップロードする

これをまとめると、

Phantomjsを起動するLmbda用関数を作成します。
以下、抜粋なので、これだけでは動きませんが雰囲気はわかると思います。

```js:index.js
exports.handler = function(event, context) {
  var childProcess = require('child_process');

  function callPhantom(callback) {
    var childArgs = ["script.js"]

    process.env['LD_WARN'] = 'true';
    console.log("calling...");
    childProcess.exec("./phantomjs ./script.js", function(err, stdout) {
      console.log("stdout", stdout);
      console.log("err", err);
      context.done(err, stdout)
    });
  }

  console.log("start");
  callPhantom(function() {
    console.log("finish");
    context.done();
  });
};
```

phantomjsに渡すファイルでは、
開始時間など`performance.timing`でとれるような情報を取得しつつ
phantomjs内で実行された情報も取得します。

```js:script.js
phantom.onError = function(msg, trace) {
  console.log("error", msg);
  var result = {"msg": msg};
  console.error(JSON.stringify(result));
  phantom.exit(1);
};

page.onResourceRequested = function(requestData, networkRequest) {
  //stop GA access not to affect user
  if(/.*google-analytics.*/.test(requestData.url)) {
    networkRequest.abort();
    return;
  }
};

resTime = {};
reqTime = {};
realAccessUrl= "http://www.example.com";

page.onResourceReceived = function(response) {
  if (response.stage === "start") {
    //ほしい情報もろもろを取得する
    if(response.url == realAccessUrl) {
      resTime['page_request_start'] = response.time.getTime();
      reqTime[response.id] = "page_response_end";
    }
  } else if(response.stage == "end") {
    if (response.id in reqTime) {
      resTime[reqTime[response.id]] = response.time.getTime();
    }
  }
};

function onPageReady() {
  var result = page.evaluate(function() {
    return JSON.stringify(PerformanceMonitor.getResult());
  });

  var times = JSON.parse(result);
  times["page_request_start"] = resTime["page_request_start"];

  var elapseTimes = {};
  var keys = Object.keys(times);
  var startTime = resTime['page_request_start'];
  for(var i = 0; i < keys.length; i++) {
    var key = keys[i];
    elapseTimes[key] = times[key] - startTime;
  }

  var phantomResult = {
    "elapseTimes": elapseTimes
  };

  console.log(JSON.stringify(phantomResult));
  phantom.exit();
}
```

パフォーマンス計測用に、サイトに埋め込むjsファイルです。
performance.mark()がないので、Date.getTime()を使用し、
Cookieで計測するかどうかを決定しています。

```js:performanmce.js
document.PerformanceMonitor = new function() {
  var performanceResults = {};

  this.mark = function(name) {
    if(isMonitorable == false) return;

    var markName = markPrefix + name;
    //performance.mark(markName);
    if (performanceResults[markName] == undefined || performanceResults[markName] == null) {
      performanceResults[markName] = new Date().getTime();
    }
  };

  this.getResult = function() {
    if(isMonitorable == false) return {};

    return performanceResults;
  };

  var isMonitorable = false;

  initIsMonitorable();
  function initIsMonitorable() {
    isMonitorable = /monitor_enable=true[;\s]?/.test(document.cookie);
  }
}()
```

npmモジュールは以下

```json:package.json
 "devDependencies": {
    "phantomjs": "2.1.3",
    "phantom-url": "1.0.0"
  }
```

# Uploadする

上をまとめてzipにしてアップロードします。
ただ、30MB近くになるので、ブラウザからアップロードしようとすると偶に失敗します。
ただ、境界に近かったのか、

npmをseleniumからphantomjsに変えたところ失敗する確率が大きく減りました。

# New Relicを使う

簡単だと思って気軽にやってみたら、意外と面倒だったのでNew Relic のSynthesisを使うと良いかもしれません。
簡単に試したところ、seleniumも、performance APIも動いたので、手軽だと思います