The application works. No exception has been thrown, the logs are silent, the health check shines green. And yet users write that "the site drags". You check - responses arrive after eight hundred milliseconds instead of a hundred. Since when? Nobody knows. Logs record events, and this is a trend: something that grew over weeks and is invisible in any single entry.
Rome set signal towers on its borders. They did not report single events - they measured traffic: how many riders passed, how long the crossing took, how many sentries failed to return. Only from those numbers could you see something going wrong before a gate fell. That is what metrics are.
The standard for collecting metrics is Prometheus - a system that collects and stores numbers describing your application. It offers four kinds of measurement, from simplest to most elaborate:
For HTTP request duration the right choice is a Histogram. A Counter would say only how many requests there were, a Gauge how long the last one took. A Histogram shows the shape: that nine out of ten requests finish under 200 ms while every tenth exceeds a second. That shape reveals a problem an average would hide.
You create a metric once, describing it with three fields:
1import { Counter } from 'prom-client';
2
3@Injectable()
4export class PrometheusService {
5 private readonly httpRequestCounter = new Counter({
6 name: 'http_requests_total',
7 help: 'Total HTTP requests',
8 labelNames: ['method', 'route', 'status'],
9 });
10
11 recordRequest(method: string, route: string, status: number) {
12 this.httpRequestCounter.inc({ method, route, status: String(status) });
13 }
14}The order of the fields is conventional but always the same:
is the metric's identifier, name
a human-readable description, help
the list of dimensions you will be able to slice the data by.labelNames
The labels are the interesting part. Thanks to them one counter answers many questions: how many POST requests there were, how many hit
/legions, how many ended with a 500. Without labels you would need a separate counter for every combination.Beware of one trap: a label with many possible values multiplies the number of data series. Putting a user id in a label creates as many series as you have users - and will bring Prometheus down. Labels must have a finite, small set of values, @name.
Before writing your own metrics, it is worth switching on the built-in ones:
1import { collectDefaultMetrics, register } from 'prom-client';
2
3collectDefaultMetrics();collectDefaultMetrics() collects the default system metrics - processor and memory usage and the Node.js event loop lag. That last one is especially valuable: a growing event loop lag means something is blocking the thread and the application is falling behind, though no endpoint has failed yet.Note what this function does not do: it sends nothing, draws no charts and resets no counters. It only starts collecting.
Prometheus does not accept data pushed by an application - it comes for it itself. Your job is to expose it at an agreed address:
1@Controller()
2export class MetricsController {
3 @Get('/metrics')
4 async getMetrics(@Res() res: Response) {
5 res.set('Content-Type', register.contentType);
6 res.send(await register.metrics());
7 }
8}register is the registry of every defined metric, and register.metrics() returns them in the text format Prometheus understands. The Content-Type header must say plain text, not JSON - hence register.contentType, which sets the right value for you.This model is called scraping: every dozen seconds or so Prometheus queries that address and records what it found. The application need not know who is watching it, or whether anyone is.
The whole road from nothing to a chart looks like this:
prom-client package./metrics endpoint.The division of labour between the last two often gets blurred. Prometheus collects and stores, Grafana draws and alerts. That separation lets you replace one without the other - and means the application knows neither.
The signal towers stand and the traffic is measured:
name, help, labelNames,collectDefaultMetrics() collects system metrics - CPU, memory, event loop lag; it sends nothing,/metrics endpoint with Content-Type set to plain text via register.contentType,prom-client, define metrics, the /metrics endpoint, configure the scraper, visualise in Grafana,In the next lesson we will drop from trends down to a single bug - you will meet debugging techniques for when you know something is wrong but not where. For now remember: a log says what happened once; a metric says what keeps happening - and it is the one that warns you before a gate falls.