Next.jsのアプリケーションにおいて、App Router
と Pages Router
の両方で共通の prom-client
メトリクス(例: Counter
)を共有しようとすると、いくつかの課題に直面します。特に、global
オブジェクトにメトリクスレジストリを登録しようとした際に問題が発生します。
発生した問題
T3 StackのようなNext.jsプロジェクトで、ログのメトリクス化のために prom-client
の Counter
を Logger
クラスで共有することを検討しました。具体的には、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-client
の register
インスタンスを 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 Router
と Pages Router
のAPIルートは、Next.jsのビルドおよび実行環境において、それぞれ異なるJavaScriptコンテキストで動作するようです。そのため、たとえ register
インスタンスを共有しようとしても、実際には別々の prom-client
レジストリインスタンスが生成されてしまい、メトリクスを真に共有することができませんでした。
これは、Next.jsの App Router
と Pages Router
のAPIルートが、異なるNode.jsプロセスやV8コンテキストで実行される可能性があるためと考えられます。global
オブジェクトは、そのコンテキスト内でのみ有効なため、異なるコンテキスト間では共有されません。
この問題は、Next.jsのGitHub Discussionsでも言及されており、現時点(2023年11月)では明確な解決策は提示されていません。
結論
Next.jsの App Router
と Pages Router
のAPIルート間で prom-client
のメトリクスを完全に共有することは、現在のところ困難です。それぞれのルーターで独立したメトリクス収集を行うか、Prometheus Pushgatewayのような外部サービスを利用してメトリクスを集約するなどの代替手段を検討する必要があります。