NestJS course Β· Module 12: Containers and CI/CD
Observability - metrics with Prometheus
In this lesson5
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. @willsoto/nestjs-prometheus collects and exposes application metrics in the Prometheus format - and along the way turns metrics into ordinary providers, injected like anything else.
Registering metrics
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.
Use in a service
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}@InjectMetric('name') works like the @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. inc() raises the counter by one; that is all a Counter can do. startTimer() returns a function you call once the work is finished - only then does the histogram record the measured time:
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.
The RED method - what to measure first
You could add hundreds of metrics. RED is the acronym naming the three you always start with, ordered by importance:
- Rate - requests per second. It tells you whether there is any traffic at all.
- Errors - the share of requests ending in failure. It tells you whether the traffic is being served.
- Duration - response time, usually as p95 and p99. It tells you whether it is being served well.
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.
What /metrics returns
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 # HELP carry the metric's description, and those with # TYPE its type. Then come the measurements themselves: the name, the labels in braces, the value.
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.
Summary
The Empire's eyes are watching, and metrics are ordinary providers:
@willsoto/nestjs-prometheuscollects and exposes metrics in the Prometheus format - it does not handle WebSockets, test endpoints or generate documentation,PrometheusModule.register()exposes the/metricsendpoint for you,- you register metrics as providers:
makeCounterProvider,makeGaugeProvider,makeHistogramProvider, - registration order:
makeCounterProvider({,name:,help:,}), - a Counter can only rise (request count); a Gauge rises and falls; a Histogram measures a distribution across buckets; a Summary computes quantiles,
@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,- the RED method by importance: Rate (requests per second) β Errors (share of failures) β Duration (p95, p99),
- give time as percentiles, not an average - an average hides every twentieth user waiting five seconds,
/metricsreturns 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.
Code for this lesson: src/prometheus-metrics.ts
1// Observability - Metrics with Prometheus
2console.log("=== PROMETHEUS METRICS ===\n");
3
4// Prometheus metric types
5interface MetricType {
6 name: string;
7 type: 'counter' | 'gauge' | 'histogram' | 'summary';
8 description: string;
9 example: string;
10}
11
12const metricTypes: MetricType[] = [
13 {
14 name: 'Counter',
15 type: 'counter',
16 description: 'Only grows (e.g. number of requests)',
17 example: 'http_requests_total{method="GET",status="200"} 1547',
18 },
19 {
20 name: 'Gauge',
21 type: 'gauge',
22 description: 'Grows and shrinks (e.g. active connections)',
23 example: 'active_connections 42',
24 },
25 {
26 name: 'Histogram',
27 type: 'histogram',
28 description: 'Distribution of values in buckets (e.g. response times)',
29 example: 'http_request_duration_seconds_bucket{le="0.1"} 1400',
30 },
31 {
32 name: 'Summary',
33 type: 'summary',
34 description: 'Quantiles (p50, p95, p99)',
35 example: 'request_duration_summary{quantile="0.95"} 0.25',
36 },
37];
38
39console.log("Prometheus metric types:\n");
40metricTypes.forEach((m, i) => {
41 console.log(`${i + 1}. ${m.name} (${m.type})`);
42 console.log(` Description: ${m.description}`);
43 console.log(` Example: ${m.example}\n`);
44});
45
46// RED Method
47console.log("=== RED METHOD ===\n");
48const red = {
49 Rate: 'Number of requests per second',
50 Errors: 'Percent of requests with errors',
51 Duration: 'Response time (p95, p99)',
52};
53
54Object.entries(red).forEach(([k, v]) => {
55 console.log(` ${k}: ${v}`);
56});
57
58console.log("\n=== ENDPOINT /metrics ===\n");
59console.log("# HELP http_requests_total Total number of requests");
60console.log("# TYPE http_requests_total counter");
61console.log('http_requests_total{method="GET",status="200"} 1547');
62Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What is the @willsoto/nestjs-prometheus package used for?
2. What characterizes a Counter metric type in Prometheus?
These are 2 of 3 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Code editor
Define three metrics: http_requests_total (Counter with labelNames: method, route, status), active_connections (Gauge), http_request_duration (Histogram)
- Vertical ordering
Arrange the elements of the RED method from most important to least frequently checked:
- Click in order
Arrange the elements of Counter metric registration in PrometheusModule:
- Code editor
Inject metrics via @InjectMetric, use requestsCounter.inc() and requestDuration.startTimer() / timer()