Edited at
Node.jsDay 5

Node.js で Log に userId を自動で出力する方法


概要

たとえば、ログ出力した際にログインしている userId を(request 持ちまわることなく)出力したいですよね。

その実現方法を紹介します。


出力処理のイメージ

const logger = require('./logger');

// どのファイルからでもログを出力したい
logger.info('ほげ');



ログイン済みの場合の出力例

2018-11-25T07:23:41.162Z userId=1234 [INFO] ほげ



実現するための実装

これは下記の方法で実現できます。


logger.js

class Logger {

constructor () {
this.requestContext = null;
}

setRequestContext (requestContext) {
// logger を利用しているクラスの単体テストでも影響がないように直接 require はせず、注入する。
this.requestContext = requestContext;
}

info (msg) {
const userId = (this.requestContext && this.requestContext.get('request:userId')) || '-';
console.log(`${new Date().toISOString()} userId=${this.getUserId() || '-'} [INFO] ${msg}`);
}
}

module.exports = new Logger();



express(app.js)の実装

const express = require('express');

const app = express();

// リクエスト単位のグローバル変数を実現するためのクラスを導入
const requestContext = require('request-context');
app.use(requestContext.middleware('request'));

// logger初期化
const logger = require('./logger');
logger.setRequestContext(requestContext);

// これはサンプルなのでミドルウェアでログインしたことにする
app.use(function (req, res, next) {
req.userId = '1234'
requestContext.set('request:userId', req.userId); // 値をセット
next();
});

// ルータ読み込み
app.use('/', require('./router'));

app.listen(3000);



ルータ(router.js)の実装

const express = require('express');

const router = express.Router();
const logger = require('./logger');

router.get('/', function(req, res) {
logger.info('ほげ');
res.send('hello world');
});

module.exports = router;


これで、 requestContext を介して Request 単位のグローバル変数的な値(今回でいうなら userId)を保持できるようになり、ログイン後にログを吐けば、どこであっても userId が出力されることになります。

これで、めでたしめでたしなのですが、1点問題があるのです。


domain に依存している

実はこの実装、 domain に依存しているのですが、この機能はなにを隠そう deprecated (非推奨) なのです。

ただ、単純な deprecated なのではなく、実際には別の手段が見つかるまでの非推奨という特殊な状態なのです。

domain を利用するうえでの前提が公式ドキュメントに記載されています。


domain 公式ドキュメントの冒頭

公式ドキュメントの冒頭の部分を翻訳してみましょう。

This module is pending deprecation. Once a replacement API has been finalized, this module will be fully deprecated. Most end users should not have cause to use this module. Users who absolutely must have the functionality that domains provide may rely on it for the time being but should expect to have to migrate to a different solution in the future.

このモジュールは非推奨が保留されている状態です。代替となる API が最終化されたら、このモジュールは完全に非推奨となります。ほとんどの利用者にとっては、このモジュールを使う理由はないでしょう。domain が提供する機能をどうしても必要とするユーザは、差し当たってこれに頼ったとしても、将来、異なる解決策へと移行する前提であることになります。

Domains provide a way to handle multiple different IO operations as a single group. If any of the event emitters or callbacks registered to a domain emit an 'error' event, or throw an error, then the domain object will be notified, rather than losing the context of the error in the process.on('uncaughtException') handler, or causing the program to exit immediately with an error code.

domain は複数の異なったIO操作を単一のグループとしてハンドルする方法を提供します。domain に登録されている Event Emitter やコールバックのいずれかが 'error' イベントを emit したり、エラーを投げたなら、エラーのコンテキストが失われることなく、エラーコードを伴ってプログラムが即終了するのでもなく、process.on('uncaughtException') ハンドラの中で domain オブジェクトが通知されます。


つまりは

将来変わるのを受け入れたうえで自己責任で使っていく、ということになりますね。

個人的には十分テストしたうえでですが、他に代替手段がない以上、使うしかない気がしてます。


より良い方法をご存知の方がいらっしゃいましたら

私自身、より良い方法があるならそれを使いたいと常々思ってますが、現時点ではあまり有用な方法に出会えていません。

この記事を読まれ方の中により良い方法をご存知の方がいらっしゃいましたら、コメント頂けると大変嬉しいです!

以上、Log に userId を自動で出力する方法の紹介でした!