【Node.js】AsyncLocalStorageでリクエスト単位のコンテキストを保持する方法

【Node.js】AsyncLocalStorageでリクエスト単位のコンテキストを保持する方法 Node.js

Node.jsは非同期で動作するため、リクエストごとに変数のスコープを持つことが難しいとされてきました。これにより、リクエスト単位でログのトレースIDやユーザー情報を扱う際、グローバル変数やクロージャでは意図しない値の共有が発生することがあります。

この問題を解決するために登場したのが、AsyncLocalStorageです。本記事では、AsyncLocalStorageの基本的な使い方から、Expressとの組み合わせによる実践的な活用方法まで詳しく解説します。

AsyncLocalStorageとは

AsyncLocalStorageは、Node.jsのasync_hooksモジュールに含まれているクラスで、非同期コンテキストごとに状態(スコープ)を保持できます。各リクエストにユニークなトレースIDやユーザー情報を割り当てて、ログやデバッグ時に利用するのが代表的なユースケースです。

導入と基本構文

Node.js v13.10.0以降であれば、追加パッケージのインストールなしに利用できます。

const { AsyncLocalStorage } = require('async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();

// 任意のコンテキストをrunで設定
asyncLocalStorage.run({ requestId: 'abc123' }, () => {
  someAsyncOperation().then(() => {
    const store = asyncLocalStorage.getStore();
    console.log('リクエストID:', store.requestId);
  });
});

Expressと組み合わせた実用例

通常のミドルウェアでは、非同期処理を通じてコンテキストが失われることがありますが、AsyncLocalStorageを使えば、リクエストライフサイクル全体で情報を保持できます。

1. 初期化とミドルウェア定義

// context.js
const { AsyncLocalStorage } = require('async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();

module.exports = {
  asyncLocalStorage,
};
// middleware/context.js
const { asyncLocalStorage } = require('../context');
const { v4: uuidv4 } = require('uuid');

function contextMiddleware(req, res, next) {
  const requestId = uuidv4();
  asyncLocalStorage.run({ requestId }, () => {
    next();
  });
}

module.exports = contextMiddleware;

2. ルーティングとログ出力

// server.js
const express = require('express');
const app = express();
const contextMiddleware = require('./middleware/context');
const { asyncLocalStorage } = require('./context');

app.use(contextMiddleware);

app.get('/', (req, res) => {
  const store = asyncLocalStorage.getStore();
  console.log('リクエストID:', store.requestId);
  res.send('Hello, World');
});

app.listen(3000, () => {
  console.log('Server running at http://localhost:3000');
});

ユースケースとメリット

AsyncLocalStorageは、次のような場面で特に有用です:

  • ログのトレース性向上: 各リクエストに一意のIDを持たせて、非同期ログを統合
  • 認証情報のスレッドセーフな保持: ユーザーIDなどの状態をグローバルに使わず参照可能
  • デバッグの効率化: 問題発生時のリクエストスコープを特定しやすい

注意点と落とし穴

AsyncLocalStorageは非常に便利ですが、次のような点に注意が必要です:

  • イベントやsetTimeoutなど、async_hooksにフックできない実装では一部動作が保証されないことがある
  • 古いNode.jsバージョンでは未対応(v12では不完全)
  • 長期間保持されるとメモリリークの原因となる場合がある

まとめ

AsyncLocalStorageは、Node.jsの非同期環境下で「リクエスト単位の状態保持」を可能にする非常に強力なツールです。ExpressなどのWebフレームワークと組み合わせることで、ログの一貫性向上、ユーザー情報の安全な参照、トレース機能の強化など多くのメリットを享受できます。

一見地味な機能に見えますが、スケーラブルなNode.jsアプリケーション設計には欠かせない要素の一つです。ぜひプロジェクトに取り入れて、デバッグ効率や可読性の高いコードベースを構築しましょう。