Node.js で Request Local なコンテキストを Zone.js で作る

Request Local なコンテキストが欲しい理由

例えば、Request 毎に id を生成して、その id をログに書き出す内容に含めたい。こうすれば、障害や不具合の調査の際に、Request 単位で起きた事象の把握が容易になる。

Node.js での難しさ

ログを出力する箇所が express のレイヤーだけであれば、Request オブジェクトにプロパティを生やせば実現できるが、モデルや他のレイヤーも考慮すると難しくなる。

リクエスト毎に実行コンテキストが作られる場合は、グローバル変数で id を扱えばよいが、Node.js は基本的にひとつのスレッドで複数のリクエストを捌く。そのため、グローバル変数で扱うとリクエストの度に id が上書きされるので、複数のリクエストを同時に捌く際、前のリクエストで出力するログに新しいリクエストの id が含まれることになり、意図しない挙動になる。

標準モジュールの domain や、それを使った request-local というモジュールを使うと、やりたいことは実現できるのだが、domain はかなり前のバージョンから pending deprecation になっている。代わりになる API が用意されれば、deprecated になるので、使うのはためらう。

Zone.js

他に実現する方法はないのか調べたところ、Zone.js というライブラリでできそうだった。Zone.js は Angular が内部で使っているライブラリで、非同期処理に跨ったコンテキストを作成することができる。Zone.js の APIECMAScript の Spec として提案されているので、(stage 0 ではあるが)これの策定が進めば、Node.js でも実装されて domain に置き換わるのかな、と勝手に思っている。Zone.js 自体の説明は AngularとZone.jsとテストの話 - Qiita が詳しい。

コードは以下のようになる。middleware でリクエスト毎にコンテキストを作って、後は任意の箇所で Zone.current を通して、コンテキストにアクセスすればよい。実際には Zone の API の変更に備えて、直接扱うのではなく、RequestLocal のような名前の Zone をラップするモジュールを用意するのが適当だろう。

const express = require('express');
const uuidv1 = require('uuid/v1');
require('zone.js');

class Foo {
    constructor() {
        console.log(`[trxId=${Zone.current.get('trxId')}]: create Foo instance`)
    }
}

const app = express();

app.use(function (req, res, next) {
    Zone.current.fork({
        properties: {
            trxId: uuidv1(),
        },
    }).run(next);
});

app.get('/', (req, res) => {
    new Foo();
    res.send('Hello World!');
});

app.listen(3000, () => {
    console.log('listening port on 3000.');
});

Zone.js はライブラリの仕様を実現するために setTimeout 等の非同期処理をおこなう API を上書きしており、アプリケーション全体に影響がある「つよい」造りになっている。意図しない挙動が起きないか、もう少し個人プロダクトで試す予定。

Worker

今後の Node.js で入る Worker を使って、Request 毎に Worker を生成することで解決できないかな、とも考えている。Worker の生成コストだったり、Worker プールの仕組みを用意する必要も含めて、後日また検証する。