Node.js AsyncLocalStorage - requestIdをすべての関数に渡す時代は終わりました

問題

Expressでリクエストごとのロギングを実装する場合、requestIdをすべての関数に引数として渡す必要がありました。

app.get('/users/:id', (req, res) => {
  const requestId = crypto.randomUUID();
  const user = getUser(req.params.id, requestId);
  res.json(user);
});

function getUser(id, requestId) {
  logger.info(`[${requestId}] getUser called`);
  return queryDB(`SELECT * FROM users WHERE id = $1`, [id], requestId);
}

function queryDB(sql, params, requestId) {
  logger.info(`[${requestId}] query: ${sql}`);
  // ...
}

ビジネスロジックと関係のないrequestIdが、すべての関数シグネチャに入り込んでしまいます。

解決方法

AsyncLocalStorageを使えば、非同期コールチェーン全体にコンテキストを暗黙的に伝播できます。Node.js組み込みなので、追加パッケージは不要です。

const { AsyncLocalStorage } = require('node:async_hooks');
const crypto = require('node:crypto');

const asyncLocalStorage = new AsyncLocalStorage();

// Expressミドルウェア
function requestContext(req, res, next) {
  const store = {
    requestId: crypto.randomUUID(),
    method: req.method,
    path: req.path,
  };
  asyncLocalStorage.run(store, next);
}

app.use(requestContext);

これで、どこからでもgetStore()でコンテキストにアクセスできます。

// ロガーユーティリティ
function createLogger(module) {
  return {
    info(msg) {
      const store = asyncLocalStorage.getStore();
      const requestId = store?.requestId ?? 'no-context';
      console.log(JSON.stringify({
        timestamp: new Date().toISOString(),
        requestId,
        module,
        msg,
      }));
    }
  };
}

// サービスレイヤー - requestId引数がなくなりました
const logger = createLogger('user-service');

function getUser(id) {
  logger.info(`getUser called: ${id}`);
  return queryDB(`SELECT * FROM users WHERE id = $1`, [id]);
}

function queryDB(sql, params) {
  logger.info(`query: ${sql}`);
  // ...
}

同じリクエストのログは同一のrequestIdでまとまるため、デバッグ時にgrepひとつで全体の流れが把握できます。

ポイント

  • AsyncLocalStorageはNode.js 16.4以降でstable APIです。追加インストールは不要です
  • run()内で実行されるすべての非同期コードが同じstoreを共有します。setTimeoutPromiseasync/awaitすべて対応しています
  • パフォーマンスオーバーヘッドはほぼありません。Node.js 23+ではV8ネイティブのAsyncContextFrameでさらに高速化されています
  • NestJSはClsModule、Fastifyは@fastify/request-contextで同様のパターンを提供しています
  • ロギング以外にも、認証情報、トランザクションID、マルチテナント識別などに活用できます