Ich habe hunger

あふりかエンジニア、アフリカ向けのB2BのSaaSを開発する

Node.js + expressで色々ログを自前でDBに残そうと思った時の話

タイトルの通りなんですがログを自前で残そうという仕事が発生しました。
具体的には、どんどんデータ操作は業務に沿ってされるべきなんだけども、やべー時(悪意を持った人間が何かをしたとき)とかに「誰が、いつ、どういうデータに対して、どういう操作をしたか」という、まぁよく言うアクセスログをDBに貯めましょう、という話になりました。

で、とりあえずテーブル定義。

CREATE TABLE `log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `user_id` bigint(20) DEFAULT NULL,
  `url` text NOT NULL,
  `method` varchar(255) DEFAULT NULL,
  `request_body` longtext,
  `request_query` longtext,
  `referer` text,
  `session_id` text NOT NULL,
  `user_agent` text NOT NULL,
  PRIMARY KEY (`id`,`created_at`),
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;


main.jsの中で呼び出して、ミドルウェアとして登録(この表現合ってるのか?)

var originalLogger = require('./lib/logger');
// 中略
app.use(originalLogger);


ほんでもって、Logger自体の中身なんですが基本的には req.headers の中に含まれてるんですが req.get で呼び出せる模様。
ログインシステムがある時は、ログインしているユーザーのuser_idも取っとくと便利です。
あとは、POSTとかPUTで投げられるパラメーターはreq.body, GETのパラメーターはreq.queryに入ってるのでそれぞれきちんと分ける(特に今回のシステムだとGETパラメーターによって振る舞いを変える部分がある)。
bodyやqueryは連想配列になってるのでそれらはJSON.stringifyでDBに入れられる形に変えておく。

module.exports = function(req, res, next) {
    var user_id = req.session.user_id || null;
    var url = req.url;
    var method = req.method;
    var tmp = [req.body];
    var request_body = {};
    for (variable in req.body) {
        if (variable == 'password'){
            continue;
        }
        request_body[variable] = req.body[variable];
    }
    request_body = JSON.stringify(request_body) || null;
    var request_query = JSON.stringify(req.query) || null;
    var referer = req.get('referer') || null;
    var session_id = req.sessionID;
    var userAgent = req.get('User-Agent');

    // この辺で何か必要なデータを抽出するとか中処理

    var query = 'INSERT INTO log (created_at, user_id, url, method, request_body, request_query, referer, session_id, user_agent) VALUES (NOW(), ?, ?, ?, ?, ?, ?, ?)';
    var connection = dbConnection.Connection();
    connection.query(query, [user_id, url, method, request_body, request_query, referer, session_id, userAgent], function (err, result) {
        next();
    });
};


あとは、ログインしたりする時にreq.bodyの中にはパスワードが平文で入っているので、それを削除する。forで回しているのは request_body = req.bodyみたいにすると参照渡しになってしまって、連想配列のpasswordをkeyにしたのをDELETEすると元のやつも消えてログインできないのでこういう形に。concatとか使うという話もあったけど、まぁ一旦この形でGOしました。後は、特定の記事とか会社とかそういうったよく出てくる大事な単位というのがサービスに存在しているのなら、そのidなり識別子を抽出してDBに突っ込んでおくというのも良いよね。