willsoto/nestjs-prometheus

Injecting into middleware

Closed this issue ยท 4 comments

I am trying to inject some metrics into a middleware from my AppModule.

consumer.apply(RequestMetricsMiddleware).forRoutes('*');

And this is how my middleware is defined:

import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';
import { InjectMetric } from '@willsoto/nestjs-prometheus';
import { Counter, Histogram } from 'prom-client';

@Injectable()
export class RequestMetricsMiddleware implements NestMiddleware {
  constructor(
    @InjectMetric('node_request_operations_total') private operationsCounter: Counter<string>,
    @InjectMetric('node_request_duration_seconds') private requestDurationHistogram: Histogram<string>,
  ) {}
  use(req: Request, _: Response, next: NextFunction): void {
    var startTime = performance.now();

    req.on('close', () => {
      var endTime = performance.now();
      const totalMilliseconds = endTime - startTime;
      const name = req?.body?.query?.split('{')?.[0]?.toString().trim() || req.url;
      this.requestDurationHistogram.observe(totalMilliseconds / 1000); // convert to seconds

      Logger.debug(`Request to ${name} took ${totalMilliseconds} milliseconds`);
    });

    this.operationsCounter.inc();

    next();
  }
}

My Prometheus Module look like so:

import { makeCounterProvider, makeGaugeProvider, makeHistogramProvider, PrometheusModule } from '@willsoto/nestjs-prometheus';
import { PrometheusController } from '../PrometheusController';
import { PrometheusStatsService } from './PrometheusStats.service';

@Module({
  imports: [
    PrometheusModule.register({
      controller: PrometheusController,
    }),
  ],
  providers: [
    PrometheusStatsService,
    makeCounterProvider({
      name: 'node_request_operations_total',
      help: 'The total number of processed requests',
    }),
    makeHistogramProvider({
      name: 'node_request_duration_seconds',
      help: 'Histogram for the request duration in seconds',
      buckets: [0, 1, 2, 5, 6, 10],
    }),
  ],
  exports: [PrometheusStatsService],
})
export class PrometheusStatsModule {}

Which I import in my AppModule under the imports array.

I am getting the error:

This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason:

Error: Nest can't resolve dependencies of the class RequestMetricsMiddleware {
    constructor(operationsCounter, requestDurationHistogram) {
        this.operationsCounter = operationsCounter;
        this.requestDurationHistogram = requestDurationHistogram;
    }
    use(req, _, next) {
        var startTime = performance.now();
        req.on('close', () => {
            var _a, _b, _c, _d;
            var endTime = performance.now();
            const totalMilliseconds = endTime - startTime;
            const name = ((_d = (_c = (_b = (_a = req === null || req === void 0 ? void 0 : req.body) === null || _a === void 0 ? void 0 : _a.query) === null || _b === void 0 ? void 0 : _b.split('{')) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.toString().trim()) || req.url;
            this.requestDurationHistogram.observe(totalMilliseconds / 1000);
            common_1.Logger.debug(`Request to ${name} took ${totalMilliseconds} milliseconds`);
        });
        this.operationsCounter.inc();
        next();
    }
} (?, PROM_METRIC_NODE_REQUEST_DURATION_SECONDS). Please make sure that the argument PROM_METRIC_NODE_REQUEST_OPERATIONS_TOTAL at index [0] is available in the AppModule context.

Potential solutions:
- If PROM_METRIC_NODE_REQUEST_OPERATIONS_TOTAL is a provider, is it part of the current AppModule?
- If PROM_METRIC_NODE_REQUEST_OPERATIONS_TOTAL is exported from a separate @Module, is that module imported within AppModule?
  @Module({
    imports: [ /* the Module containing PROM_METRIC_NODE_REQUEST_OPERATIONS_TOTAL */ ]
  })

    at Injector.lookupComponentInParentModules (/Users/mf/Projects/a/backend/node_modules/@nestjs/core/injector/injector.js:231:19)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at Injector.resolveComponentInstance (/Users/mf/Projects/a/backend/node_modules/@nestjs/core/injector/injector.js:184:33)
    at resolveParam (/Users/mf/Projects/a/backend/node_modules/@nestjs/core/injector/injector.js:106:38)
    at async Promise.all (index 0)
    at Injector.resolveConstructorParams (/Users/mf/Projects/a/backend/node_modules/@nestjs/core/injector/injector.js:121:27)
    at Injector.loadInstance (/Users/mf/Projects/a/backend/node_modules/@nestjs/core/injector/injector.js:52:9)
    at Injector.loadMiddleware (/Users/mf/Projects/a/backend/node_modules/@nestjs/core/injector/injector.js:61:9)
    at MiddlewareResolver.resolveMiddlewareInstance (/Users/mf/Projects/a/backend/node_modules/@nestjs/core/middleware/resolver.js:16:9)
    at async Promise.all (index 0)

Update

Temporarily solved the issue by adding the makeCounterProvider and makeHistogramProvider to exports as well, although not very pretty/DRY.

Your solution makes total sense and is expected. In order for the any injectable to be visible to other modules, they must be exported unless they all live within that module. The Nest.js docs explain these concepts

I see @willsoto I just didn't really like repeating the code twice, so perhaps there was another solution ๐Ÿ˜Š

You can always make them a variable if you want. Nothing that says they have to be declared inline in the providers array.

I think it will be better if we update DOCS as this package is specific to NESTJS.
I shall raise PR @willsoto for your last comment too