Technical Challenges of Sharing prom-client Metrics Between Next.js App Router and Pages Router

Exploring the technical challenges of sharing prom-client metrics between Next.js App Router and Pages Router, caused by separate JavaScript contexts.

When trying to share common prom-client metrics (e.g., Counter) across both the App Router and Pages Router in a Next.js application, several challenges arise. Problems particularly occur when attempting to register a metrics registry on the global object.

The Problem

In a Next.js project like a T3 Stack, I considered sharing a prom-client Counter through a Logger class for log metrics. Specifically, I tried registering prom-client’s default registry on the global object to use a single metrics instance across the entire application.

However, with this approach, a build error occurred when trying to register a Counter to the global registry from a Pages Router API route.

import { Counter } from 'prom-client';

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

  private constructor() {
    // Problems occur when trying to initialize Counters and register them with the default registry here
    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's default registry is managed globally, but
    // in the Next.js environment, App Router and Pages Router have different contexts,
    // so sharing the global object may not work as expected.
  }

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

Workaround and Remaining Issues

To work around this problem, I tried having the Logger class hold its own prom-client register instance.

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

class Logger {
  private static instance: Logger;
  private errorCounter: Counter<string>;
  private warnCounter: Counter<string>;
  private registerInstance: typeof register; // Hold the register instance

  private constructor() {
    this.registerInstance = register; // Get the default registry instance

    this.errorCounter = new Counter({
      name: 'errors_total',
      help: 'Total number of errors',
      registers: [this.registerInstance], // Register with this registry
    });

    this.warnCounter = new Counter({
      name: 'warnings_total',
      help: 'Total number of warnings',
      registers: [this.registerInstance], // Register with this registry
    });
  }

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

  // Method to register metrics (if needed)
  public registerMetric(metric: Counter<string>) {
    this.registerInstance.registerMetric(metric);
  }
  // ... logging methods ...
}

export default Logger;

This approach resolved the build error, but the fundamental problem was not solved. The App Router and Pages Router API routes appear to run in different JavaScript contexts within Next.js’s build and runtime environment. Therefore, even when trying to share a register instance, separate prom-client registry instances are actually created, making it impossible to truly share metrics.

This is likely because Next.js’s App Router and Pages Router API routes may execute in different Node.js processes or V8 contexts. The global object is only valid within its context and is not shared across different contexts.

This issue has also been mentioned in Next.js GitHub Discussions, and no clear solution has been provided as of November 2023.

Conclusion

Fully sharing prom-client metrics between Next.js’s App Router and Pages Router API routes is currently difficult. Alternative approaches need to be considered, such as performing independent metrics collection in each router or using an external service like Prometheus Pushgateway to aggregate metrics.