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 の API は ECMAScript の 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 プールの仕組みを用意する必要も含めて、後日また検証する。