ExpressのアクセスログをMongoLabに吐き出してみる

# はじめに
この分野は浦島太郎なのでツッコミ歓迎。

## 背景
一昔前は自分のウェブサイトは、友人たちと共有していたレンタルサーバの上に載せていたのですが、利用していたサーバの移行手続きの際に管理していたドメインほぼ全てをHerokuへ移動させる事にしました。この際、特にログとか考えていなかったのでLogplex（$ heroku logsで表示されるアレ）以外にログがありません。つまり最新1500行のみ。たいしてアクセスがあるわけじゃないんですが、ログがないのも寂しいので、なんか考える事にしました。

## 最近の流行り
間にLogstashなりFluentdなりを挟んで、最後はお好みのデータベースに流し込み、必要に応じて監視や解析って感じなんですかね？良さ気な解析ツールはだいたいバックエンドと蜜に連携してて課金に誘導されていく印象でした。バックエンドをさらにMongoDB、ElasticSearchの2段に分けてそれぞれに得意な処理を、みたいな例もみかけました。

まぁ、趣味サイトなのでログの量も大したことはないので、お金払ってTDさんやBigQueryとかはあり得ないどころか、VM借りてFluentdやElasticSearchを常時動かしとくのも躊躇するレベル。本当はKibanaとか試してみたかったのですが、やっぱり無理かなー。

趣味サイトはフロントエンド側のサーバすら統一されてなくてExpress/Node.js、Sinatra/Ruby、http/goなどなどサイトを立ち上げた時期に応じて手当たり次第試してる感じなので、間にFluentdみたいなのが入るのは魅力的なんですけどね。無料枠でやってるといつかサービスの都合でバックエンドも変えなきゃいけないかもしれないし。

# とりあえずMongoDB
## 結局全部諦める
で、色々と夢が膨らんだところで全てリセット。まぁ、Herokuに閉じて無料枠で遊ぶのなら、MongoDBに突っ込むのが無難ですかね。あまり先の事までは考えていないですが。

Fluentdみたいな中間層を置く余裕がないので、せめて臨機応変に使えるMongoDBを中間層に置くかな、と。そもそもHerokuでサポートしてるData StoreのAdd-onsで無料枠があるとなると、あまり選択肢がないんですよね。MongoDBならMongoLabを使えば496MBまでは無料枠でいけるし、MongoDBのCapped Collectionを使えば最大サイズを指定しておけば溢れたら古いデータから自動で消してくれるのでメンテナンスフリーでいけそう。

## MongoLabを追加
```shell-session
toyoshim@tss:~/workspace/Heroku (master) $ heroku addons:add mongolab:sandbox
Adding mongolab:sandbox on chime... done, v11 (free)
Welcome to MongoLab.  Your new subscription is being created and will be available shortly.  Please consult the MongoLab Add-on Admin UI to check on its progress.
Use `heroku addons:docs mongolab` to view documentation.
```

HerokuのDashboardに行くとプロジェクトのResourcesタブにMongoLabが追加されてる。手元のmongo shellが2.x系の場合はmongo shellからログインできないので、なんらかの方法で3.0系のmongo shellを用意する（参考：[mongolabでmongo shellからログインできない時の対処法](http://qiita.com/toyoshim/items/2b20e9acdeecf2a680fe)）。

あるいは、Web UIで全て済ませてしまうのも手ですね。ここではコマンドラインのメモを残します。496MBだけど、途中で何かしたくなった時のために200MBくらいで作りました。保存期間が半分になったくらいでは大勢に影響はないはず。

```shell-session
root@d2760283f209:/# mongo ********.mongolab.com:*****/heroku_******** -u <dbuser> -p <dbpassword>
MongoDB shell version: 3.0.8
connecting to: ********.mongolab.com:*****/heroku_********
Welcome to the MongoDB shell.
For interactive help, type "help".
For more comprehensive documentation, see
	http://docs.mongodb.org/
Questions? Try the support group
	http://groups.google.com/group/mongodb-user
rs-********:PRIMARY> db.createCollection("log", { capped: true, size: 200000000 })
{ "ok" : 1 }
}
```

# Expressへの組み込み
## JSON形式への変換
[morgan](https://github.com/expressjs/morgan)を使っていたので、こいつを拡張してMongoDBに吐き出すことにしました。今から思えば1から書いたほうが良かった気もしますが。

morganにはログを自前のフォーマットにする方法が2つあります。1つ目は[use custom token formats](https://github.com/expressjs/morgan#use-custom-token-formats)として紹介されている方法です。

```js
app.use(morgan('{"method": ":method"}'));
```

みたいにフォーマットを引数で渡して上げれば

```json
{"method": "GET"}
```

こんな感じに整形されます。

もう1つの方法はmorgan.format()を使う方法で、

```js
morgan.format('json', '{"method": ":method"}');
app.use(morgan('json'));
```

こんな感じで事前に任意の名前（ここではjson）でフォーマットを登録して、後から名前で指定してあげます。どちらでも結果は同じなのでお好みで。

またフォーマットも書式で指定する方法以外に関数を渡す方法もあるようです。実際、書式で指定した場合には内部的にcompile()が走り、生成された関数がフォーマッタとして登録されるようです。

## MongoDBへの出力

```js
app.use(morgan('json', {stream: dbstream}));
```

みたいな形でstream.Writableを継承したinstanceを渡してあげると、各ログの出力が1回のwriteで渡ってくるようになります。エラー処理とか省いてざっくり書くと、

```js

function MongoDBStream() {
  stream.Writable.call(this);
  this._db = null;
  this._collection = null;
  this.morgan = morgan('json', {stream: this});
}
util.inherits(MongoDBStream, stream.Writable);

MongoDBStream.prototype._write = function(chunk, encoding, cb) {
  this._collection.insertOne(JSON.parse(chunk)).then(() => {
    cb(null);
  }, (e, db) => {
    cb(e);
  });
};

MongoDBStream.prototype.connect = function(url, collection) {
  return new Promise((resolve, reject) => {
    mongodb.MongoClient.connect(url, (err, db) => {
      if (err)
        return reject(err);
      this._db = db;
      this._collection = db.collection(collection);
      resolve();
    });
  });
};

var dbstream = new MongoDBStream();
dbstream.connect('mongodb://....', 'log').then(() => {
  app.use(dbstream.morgan);
  // ...
});
```

こんな感じで動きます。_writeの中でJSON.parse()してるのがイマイチ。フォーマッタを関数で渡して、フォーマッタが呼ばれた段階でオブジェクトを構築してDBにストアしちゃうのも手かもしれません。まぁ、性能気にしない用途なので。ユルフワ感を大切にしました。

# npmモジュール化

せっかくなので、というか自分自身がたくさんサーバがある関係でnpmモジュールにしちゃいました。[momolog](https://github.com/toyoshim/momolog)って名前で登録してあります。morgan使ったMongoDB向けのロガーだからmomolog。PromiseとかES6の()=>{}構文を使ってるので、nodeは0.*系は全滅です。自分は4.2.1で作業してました。これ使えば数行の修正でMongoDBにログが吐き出せます。

# Herokuでの実際
![heroku.png](https://qiita-image-store.s3.amazonaws.com/0/12843/5068728e-c4b7-11df-1095-b73b99ca66c5.png)

ダッシュボードからMongoLabを追加。検索ボックスでMongoとか入れれば2つくらいにまで絞れますので、あとはポチッと。で、ここにできたMongoLabのアイコンをクリックすると以下のコンソールが開きます。

![mongolab.png](https://qiita-image-store.s3.amazonaws.com/0/12843/fac4e01a-1030-2068-fab6-00705d790f72.png)

色々と見えちゃってますが、パスワード見えてないので良しとしましょう。Add collectionから作る場合にはCappedにチェックを入れます。MongoDBなので事前にCollectionを作らなくても最初のログ書き込み時に自動生成されるのですが、その場合にはCappedにはならないので事前に作ったほうが良いです。

で、ざっくりと以下のような修正を追加。expressは4系、nodeも4系を推奨。

```json:package.json
{
  ...
  "dependencies": {
    ...
    "momolog": "0.1.0"
  },
  ...
```

```js:app.js
var express = require('express')
var momolog = require('momolog')

var app = express();
var log = momolog();
// 環境変数にユーザ名・パスワード込みのURIが自動登録されているので
// それをそのまま渡すだけ。第2引数は先ほど作ったCollection名。
log.connect(process.env.MONGOLAB_URI, 'log').then(() => {
  // 以下のuse()はconnect前でも大丈夫。早いほうが安心。
  app.use(log.morgan());

  // 他の設定はここより後で。
  // morgan登録前にハンドラを登録するとログ対象から外れます。

  app.listen(process.env.PORT, () => {
    // Started.
  });
});
```

あら簡単。

# まとめ
というわけで、すでに世の中に五万とありそうなツールを作ってみました（一応、それなり探してはみたんですが）。今後はRubyとGo向けに同じ吐き出しツール書いて、ログが集まった頃にちょっとした解析ページ作るか有りモノを流用するかしたいです。サブインデックスとか後から考える。フォーマットがまずかったら後で変換する（そのための200MB！）。