You met Prometheus in the monitoring module: four metric types, scraping, and a
/metrics endpoint written by hand in a controller. It worked - but every new metric meant remembering the registry, and every module meant injecting the service holding them.NestJS has a ready answer.
collects and exposes application metrics in the Prometheus format - and along the way turns metrics into ordinary providers, injected like anything else.@willsoto/nestjs-prometheus
You declare metrics in a module, through the package's helper functions:
1@Module({
2 imports: [PrometheusModule.register()],
3 providers: [
4 makeCounterProvider({
5 name: 'http_requests_total',
6 help: 'Total number of HTTP requests',
7 labelNames: ['method', 'route', 'status'],
8 }),
9 makeGaugeProvider({
10 name: 'active_connections',
11 help: 'Number of active connections',
12 }),
13 makeHistogramProvider({
14 name: 'http_request_duration_seconds',
15 help: 'HTTP request handling time',
16 labelNames: ['method', 'route'],
17 }),
18 ],
19})
20export class MetricsModule {}The order inside
makeCounterProvider is fixed: makeCounterProvider({, then name: 'http_requests_total',, then help: '...',, and finally }).PrometheusModule.register() exposes the /metrics endpoint - you no longer write it yourself. The three helper functions match the three metric types you know: a Counter only rises (request count), a Gauge rises and falls (active connections), a Histogram measures a distribution across buckets (response times). The fourth type, Summary, computes quantiles inside the application.You inject a registered metric like any provider - except you name it:
1@Injectable()
2export class MetricsService {
3 constructor(
4 @InjectMetric('http_requests_total')
5 private readonly requestsCounter: Counter<string>,
6
7 @InjectMetric('http_request_duration_seconds')
8 private readonly requestDuration: Histogram<string>,
9 ) {}
10
11 recordRequest(method: string, route: string, status: number) {
12 this.requestsCounter.inc({ method, route, status: String(status) });
13 }
14
15 startTimer(method: string, route: string) {
16 return this.requestDuration.startTimer({ method, route });
17 }
18}
works like the @InjectMetric('name')
@Inject with a token you met with useValue providers - a metric is not a class, so NestJS cannot match it by type.Two methods are worth telling apart.
raises the counter by one; that is all a Counter can do. inc()
returns a function you call once the work is finished - only then does the histogram record the measured time:startTimer()
1const timer = this.metricsService.startTimer('GET', '/legions');
2
3await this.legionService.findAll();
4
5timer();This pattern - take a function, do the work, call the function - is handier than subtracting timestamps by hand, and it takes care of the unit: Prometheus histograms count in seconds, not milliseconds.
You could add hundreds of metrics. RED is the acronym naming the three you always start with, ordered by importance:
The order is not arbitrary and is worth remembering: Rate dropping to zero means nobody can connect - an immediate alarm. Rising Errors is a failure users can see. Rising Duration is a problem still developing.
Note why time is given as percentiles rather than an average. An average of 200 ms sounds fine even when every twentieth user waits five seconds - p95 says outright: "95% of requests finished below this value". That is a number describing the experience rather than blurring it, @name.
The endpoint the package exposes hands data back in the Prometheus text format - not JSON, not XML, not CSV:
1# HELP http_requests_total Total number of HTTP requests
2# TYPE http_requests_total counter
3http_requests_total{method="GET",route="/legions",status="200"} 1027
4http_requests_total{method="POST",route="/legions",status="201"} 43The shape is simple and worth being able to read. Lines beginning with
carry the metric's description, and those with # HELP
its type. Then come the measurements themselves: the name, the labels in braces, the value.# TYPE
This format looks poor next to JSON, but that is the point: Prometheus queries thousands of applications every dozen seconds, so parsing has to be cheap. When you open
/metrics in a browser and see a wall of text - that means it works.The Empire's eyes are watching, and metrics are ordinary providers:
@willsoto/nestjs-prometheus collects and exposes metrics in the Prometheus format - it does not handle WebSockets, test endpoints or generate documentation,PrometheusModule.register() exposes the /metrics endpoint for you,makeCounterProvider, makeGaugeProvider, makeHistogramProvider,makeCounterProvider({, name:, help:, }),@InjectMetric('name') injects a metric by name, because it is not a class and a type will not do,inc() raises the counter; startTimer() returns a function you call after the work - histograms count in seconds,/metrics returns the Prometheus text format: # HELP, # TYPE, then the name with labels and a value - not JSON, XML or CSV.In the next lesson we will go a step beyond numbers - you will meet distributed tracing, which shows a single request's road through many services. For now remember: metrics tell you how many and how fast; RED names the three numbers to check first.