We use cookies to enhance your experience on the site
CodeWorlds

Observability - metrics with Prometheus

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:

  1. Rate - requests per second. It tells you whether there is any traffic at all.
  2. Errors - the share of requests ending in failure. It tells you whether the traffic is being served.
  3. 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, @name.

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"} 43

The 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-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,
  • 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,
  • /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.

Go to CodeWorlds