Next.jsのApp RouterとPages Router間におけるprom-clientメトリクス共有の技術的課題

Next.jsのアプリケーションにおいて、App RouterPages Router の両方で共通の prom-client メトリクス(例: Counter)を共有しようとすると、いくつかの課題に直面します。特に、global オブジェクトにメトリクスレジストリを登録しようとした際に問題が発生します。

発生した問題

T3 StackのようなNext.jsプロジェクトで、ログのメトリクス化のために prom-clientCounterLogger クラスで共有することを検討しました。具体的には、prom-client のデフォルトレジストリを global オブジェクトに登録し、アプリケーション全体で単一のメトリクスインスタンスを使用しようとしました。

しかし、このアプローチでは、Pages Router のAPIルートから global のレジストリに Counter を登録しようとすると、ビルドエラーが発生しました。

import { Counter } from 'prom-client';

class Logger {
  private static instance: Logger;
  private errorCounter: Counter<string>;
  private warnCounter: Counter<string>;

  private constructor() {
    // ここでCounterを初期化し、デフォルトレジストリに登録しようとすると問題が発生
    this.errorCounter = new Counter({
      name: 'errors_total',
      help: 'Total number of errors',
    });

    this.warnCounter = new Counter({
      name: 'warnings_total',
      help: 'Total number of warnings',
    });
    // prom-clientのデフォルトレジストリはグローバルに管理されるが、
    // Next.jsの環境ではApp RouterとPages Routerで異なるコンテキストを持つため、
    // グローバルオブジェクトの共有が期待通りにいかない場合がある。
  }

  public static getInstance(): Logger {
    if (!Logger.instance) {
      Logger.instance = new Logger();
    }
    return Logger.instance;
  }
  // ... logging methods ...
}

回避策と残る課題

この問題を回避するために、prom-clientregister インスタンスを Logger クラスに持たせる方法を試しました。

import { Counter, register } from 'prom-client';

class Logger {
  private static instance: Logger;
  private errorCounter: Counter<string>;
  private warnCounter: Counter<string>;
  private registerInstance: typeof register; // registerインスタンスを保持

  private constructor() {
    this.registerInstance = register; // デフォルトレジストリのインスタンスを取得

    this.errorCounter = new Counter({
      name: 'errors_total',
      help: 'Total number of errors',
      registers: [this.registerInstance], // このレジストリに登録
    });

    this.warnCounter = new Counter({
      name: 'warnings_total',
      help: 'Total number of warnings',
      registers: [this.registerInstance], // このレジストリに登録
    });
  }

  public static getInstance(): Logger {
    if (!Logger.instance) {
      Logger.instance = new Logger();
    }
    return Logger.instance;
  }

  // メトリクスを登録するメソッド (必要であれば)
  public registerMetric(metric: Counter<string>) {
    this.registerInstance.registerMetric(metric);
  }
  // ... logging methods ...
}

export default Logger;

この方法ではビルドエラーは解消されましたが、根本的な問題は解決されませんでした。App RouterPages Router のAPIルートは、Next.jsのビルドおよび実行環境において、それぞれ異なるJavaScriptコンテキストで動作するようです。そのため、たとえ register インスタンスを共有しようとしても、実際には別々の prom-client レジストリインスタンスが生成されてしまい、メトリクスを真に共有することができませんでした。

これは、Next.jsの App RouterPages Router のAPIルートが、異なるNode.jsプロセスやV8コンテキストで実行される可能性があるためと考えられます。global オブジェクトは、そのコンテキスト内でのみ有効なため、異なるコンテキスト間では共有されません。

この問題は、Next.jsのGitHub Discussionsでも言及されており、現時点(2023年11月)では明確な解決策は提示されていません。

結論

Next.jsの App RouterPages Router のAPIルート間で prom-client のメトリクスを完全に共有することは、現在のところ困難です。それぞれのルーターで独立したメトリクス収集を行うか、Prometheus Pushgatewayのような外部サービスを利用してメトリクスを集約するなどの代替手段を検討する必要があります。