Go back to the car dashboard of lesson 01. The odometer only ever increases: it is a counter, like http_requetes_total. The fuel gauge goes up when you fill up and down as you drive: it is a gauge, like requetes_en_cours. The maintenance logbook that notes "12 trips under 10 km, 30 under 50 km, 3 over 100 km" sorts each trip into a bucket: it is a histogram, like http_duree_requete_seconds. And the black box, which writes one line per event with the exact time, is the API's JSON log. Prometheus receives nothing: it is the one that comes to read the meters every 15 seconds, like an inspector who goes around reading the meters of every car in the fleet.
A Prometheus metric always has a name, possibly labels, and a numeric value. The type says how to read that value over time. Prometheus knows four: the counter, the gauge, the histogram and the summary. The first three are used by the catalog API; the fourth is not, and the reason is instructive. All the lines below are copied from http://localhost:8000/metrics on the course machine; the values will be different on yours.
Counter. A counter only ever increases, or restarts at zero if the service restarts. It counts cumulative events. The API exposes http_requetes_total, with three labels: the response code, the HTTP method and the route:
# HELP http_requetes_total Nombre de requêtes HTTP reçues, par méthode, route normalisée et code de réponse.
# TYPE http_requetes_total counter
http_requetes_total{code="200",methode="GET",route="/cours"} 3247.0
http_requetes_total{code="201",methode="POST",route="/inscriptions"} 855.0
http_requetes_total{code="404",methode="GET",route="/cours/{id}"} 210.0
http_requetes_total{code="500",methode="GET",route="/cours"} 32.0What you never do with a counter: read its raw value to say "there are 3247 requests right now". A counter never goes down; what interests you is its rate of increase, computed with the rate() function (module 2). Documentation: Prometheus — Metric types, Counter.
Gauge. A gauge goes up and down freely: a value at a given instant. The API exposes requetes_en_cours, the number of requests being processed at the precise instant of the reading:
# HELP requetes_en_cours Nombre de requêtes HTTP en cours de traitement à cet instant.
# TYPE requetes_en_cours gauge
requetes_en_cours 1.0One request is in progress at the instant of the reading; the next instant, this number may be 0 or 4. The API also exposes configuration gauges, whose value is a piece of information more than a measurement: api_info{version="1.0.0"} 1.0 (the version is in the label, the value is always 1) and api_panne_taux_erreurs 0.01 (the share of requests the API deliberately fails: 1% in normal operation). Documentation: Prometheus — Metric types, Gauge.
Histogram. A histogram measures the distribution of a value, such as the duration of an HTTP request: how many requests took less than 5 ms, less than 10 ms, less than 25 ms…? The API exposes http_duree_requete_seconds. Unlike a counter or a gauge, a histogram publishes several lines for a single metric: one counter per bucket, plus a total count and a sum. Here are the twelve lines of the /cours route:
# HELP http_duree_requete_seconds Durée de traitement des requêtes HTTP, en secondes, par route normalisée.
# TYPE http_duree_requete_seconds histogram
http_duree_requete_seconds_bucket{le="0.005",route="/cours"} 32.0
http_duree_requete_seconds_bucket{le="0.01",route="/cours"} 74.0
http_duree_requete_seconds_bucket{le="0.025",route="/cours"} 1566.0
http_duree_requete_seconds_bucket{le="0.05",route="/cours"} 3248.0
http_duree_requete_seconds_bucket{le="0.1",route="/cours"} 3276.0
http_duree_requete_seconds_bucket{le="0.25",route="/cours"} 3278.0
http_duree_requete_seconds_bucket{le="0.5",route="/cours"} 3279.0
http_duree_requete_seconds_bucket{le="1.0",route="/cours"} 3279.0
http_duree_requete_seconds_bucket{le="2.0",route="/cours"} 3279.0
http_duree_requete_seconds_bucket{le="+Inf",route="/cours"} 3279.0
http_duree_requete_seconds_count{route="/cours"} 3279.0
http_duree_requete_seconds_sum{route="/cours"} 85.42293146001248Each _bucket{le="…"} (le for less or equal) counts the requests faster than or equal to that threshold, cumulatively: the 32 requests under 5 ms are also counted in the 74 under 10 ms, and in the 3279 of the +Inf bucket (all of them). _count is the total number of observations (3279, equal to the +Inf bucket), _sum the sum of all durations (85.4 seconds in total, that is 26 ms on average per request). This is what makes it possible to rebuild a quantile after the fact, with histogram_quantile() (module 2). Direct reading: 3248 requests out of 3279 took less than 50 ms, that is 99%. Documentation: Prometheus — Metric types, Histogram.
Summary, and why the lab does not use it. A summary also measures a distribution, but it computes its quantiles directly inside the observed program, before publishing them: one {quantile="0.5"} line, one {quantile="0.9"} line, plus _sum and _count. The problem: a quantile computed on the client side cannot be recombined with that of another instance. If the lab ran three copies of the API, you could not average the three quantile="0.9" to obtain the true 90th percentile of the whole. A histogram publishes raw counters per bucket: Prometheus can add them up across instances before computing the quantile. There is therefore no summary line in the /metrics of the lab's API. Documentation: Prometheus — Histograms and summaries.
| Type | What it measures | In the lab | Question it answers |
|---|---|---|---|
| Counter | A cumulative total that only increases | http_requetes_total, inscriptions_total, cours_consultes_total | "How many requests per second?" (with rate) |
| Gauge | An instantaneous value that goes up and down | requetes_en_cours, api_disque_libre_octets, api_info | "How many right now?" |
| Histogram | A distribution, in cumulative buckets | http_duree_requete_seconds | "95% of requests take less than how long?" |
| Summary | A distribution, quantiles computed client-side | none | The same, but without being able to aggregate across instances |
Each unique combination of metric name and labels forms a time series: a sequence of (timestamp, value) pairs that Prometheus stores and queries. http_requetes_total{code="200",methode="GET",route="/cours"} is one series; http_requetes_total{code="201",methode="POST",route="/inscriptions"} is another. On the course machine, http_requetes_total counts 14 series (14 combinations of code, method and route that have appeared since startup). Labels make it possible to filter and group without changing the name: "server errors, only on /inscriptions" reads http_requetes_total{route="/inscriptions",code=~"5.."}. Documentation: Prometheus — Data model.
The cardinality of a metric is the number of distinct series it produces. http_requetes_total{code,methode,route}, with five codes, two methods and seven routes as templates, gives a few dozen series at most. Look at the log in step 2 of lesson 01: the request called /cours/C0038, but the metric carries route="/cours/{id}", the template of the route as FastAPI declared it. If the label contained the real identifier, every course viewed would create a series: 64 courses × 5 codes × 2 methods. On a real catalog of tens of thousands of courses, the metric would explode and Prometheus would slow down. The rule: a label must have a bounded and reasonable number of values; a unique identifier, an IP address or a timestamp have no business in a label. A route that does not exist is counted under route="inconnue" (unknown), never under its real path, for the same reason.
The lab deliberately contains an instructive exception: inscriptions_total{cours_id="C0028"} and cours_consultes_total{cours_id="…"} have one label per course. With 64 courses, that stays bounded (64 series each). With a million courses, it would be a mistake. The lab actually has a PrometheusTropDeSeries alert that rings above 100,000 series. Documentation: Prometheus — Instrumentation, cardinality.
Prometheus works in pull mode (it goes and fetches): it queries by itself, every 15 seconds in the lab (scrape_interval: 15s in prometheus/prometheus.yml), the /metrics URL of each service it monitors. This is the opposite of a push system, where the observed service sends its metrics to a collector. Pull has a direct advantage: if a service stops answering, Prometheus knows it immediately, the up metric drops to 0, without depending on the failed service to report its own absence. This is exactly what this module's practice has you trigger with casser api.
| Term | What it is | In the lab |
|---|---|---|
| scrape | One read of /metrics by Prometheus | Every 15 s, on each of the 8 targets |
| target | A /metrics URL that Prometheus reads | http://api:8000/metrics, http://node-exporter:9100/metrics… |
| exporter | A program that exposes in Prometheus format the metrics of a system that does not speak it natively | node-exporter (the host machine), cadvisor (the containers) |
| job | A group of targets doing the same work | job="api", job="prometheus", job="loki"… 8 jobs |
| instance | An individual target within a job | instance="api:8000" |
up | The metric Prometheus makes itself at every scrape: 1 if the target answered, 0 otherwise | up{job="api"} equals 1 in normal operation |
The names api:8000, node-exporter:9100 are those of Docker Compose's internal network: it is Prometheus, in its container, talking to the API in its own. From your machine, the same page is http://localhost:8000/metrics. Documentation: Prometheus — Configuration, scrape_config and Prometheus — Jobs and instances.
A structured log is written in a format the machine can split without guessing (JSON, most often), rather than a free-form sentence. The API writes on its standard output one JSON line per processed request. Here is a real line, read by .\labo.ps1 journal api on the course machine:
{"horodatage": "2026-09-15T19:33:25.839+00:00", "niveau": "INFO", "id_requete": "46bb533b33e9", "methode": "GET", "route": "/cours/{id}", "code": 200, "duree_ms": 13.9, "message": "GET /cours/C0038 -> 200"}| Field | Example | What it is |
|---|---|---|
horodatage | 2026-09-15T19:33:25.839+00:00 | The exact instant of the event, in ISO 8601 format, in UTC (+00:00) |
niveau | INFO | The severity: INFO (2xx), WARNING (404, 422), ERROR (500). Three values in the lab |
id_requete | 46bb533b33e9 | A unique 12-character identifier generated for this request, returned to the client in the x-id-requete HTTP header |
methode | GET | The HTTP method |
route | /cours/{id} | The template of the route, the same as in the metric |
code | 200 | The HTTP response code |
duree_ms | 13.9 | The processing duration, in milliseconds |
message | GET /cours/C0038 -> 200 | The readable sentence: it is here, and only here, that the real course identifier appears |
What to see: route keeps the template /cours/{id} (like the metric), but message contains C0038, the real identifier. A log can afford this detail: Loki does not index the text of the message, it only indexes four labels (service, conteneur, niveau, code) extracted by Alloy. Module 5 shows you how Alloy reads the JSON and makes these labels. Documentation: Grafana Loki — Labels.
The other lab services do not all write JSON. The webhook, at startup, writes free text: INFO: Uvicorn running on http://0.0.0.0:8090 (Press CTRL+C to quit). The charge service writes JSON, but with other fields: every 30 seconds, a summary {"niveau": "INFO", "message": "résumé des 30 dernières secondes", "requetes": {"200": 214, "total": 262, "201": 26, "500": 4, "404": 16, "422": 2}}. A structured log is not a universal format: it is a decision made service by service.
A trace follows a single request through several services: each step (a span) records its name, its duration, and its parent-child relationship with the others. It answers "the request took 800 ms, in which service was that time spent?". The lab installs no tracing tool: the catalog API and the load generator are the only two application services, linked by a simple HTTP call, which limits the pedagogical value of a distributed trace here. The log's id_requete is already the first piece of a trace: it is what OpenTelemetry would call a trace id. Module 7 says one more word about it. Documentation: OpenTelemetry — Traces.
| Service | Role | Port | URL from your machine |
|---|---|---|---|
prometheus | Reads the targets, stores the series, evaluates the rules | 9090 | http://localhost:9090 |
alertmanager | Receives alerts from Prometheus, groups and routes them | 9093 | http://localhost:9093 |
grafana | Explores and visualizes Prometheus, Loki and Alertmanager | 3000 (GRAFANA_PORT) | http://localhost:3000 (admin / aiopsatlas2026) |
loki | Stores the logs, indexed by labels | 3100 | http://localhost:3100/ready |
alloy | Discovers the containers and ships their logs to Loki | 12345 | http://localhost:12345 |
node-exporter | Host machine metrics | 9100 | http://localhost:9100/metrics |
cadvisor | Metrics of each container | 8080 | http://localhost:8080 |
api | The observed service | 8000 | http://localhost:8000/cours · http://localhost:8000/metrics |
webhook | Receives and displays the alerts | 8090 | http://localhost:8090 |
charge | Generates traffic to the API | none | none: it has no interface, only a log |
Nine ports, ten services: charge listens on nothing. Eight Prometheus targets, ten services: charge and webhook expose no /metrics.
This step-by-step assumes the lab is started (lesson 04). If you read this lesson before, keep it for later: every step is done in the browser or in a terminal, read-only.
Open http://localhost:8000/metrics in the browser. This is the raw page Prometheus reads every 15 seconds: text, one line per series, preceded by its # HELP and # TYPE lines. On the course machine, it counts 271 lines. Compare with http://localhost:9100/metrics (node-exporter: 1578 lines) and http://localhost:8080/metrics (cAdvisor: 3444 lines, for ten containers).
What to see: the lines starting with python_ and process_ at the top of the API's page are not written by the lab. The prometheus_client library adds them on its own (process memory, Python garbage collector). The course's metrics start at http_requetes_total.
Look for the API's four # TYPE. In the page, search (Ctrl+F) for # TYPE http_: you find counter for http_requetes_total and histogram for http_duree_requete_seconds. Search for # TYPE requetes_en_cours: gauge. Search for summary: no result.
What to see: the three types used, and the deliberate absence of the fourth.
Count the series of http_requetes_total in Prometheus. Open http://localhost:9090, type http_requetes_total in the query field and execute. On the course machine, the table displays 14 lines, including:
http_requetes_total{code="200", instance="api:8000", job="api", methode="GET", route="/cours", service="api"} 3241
http_requetes_total{code="500", instance="api:8000", job="api", methode="GET", route="/cours", service="api"} 32What to see: Prometheus has added three labels to those of the /metrics page: job="api" and instance="api:8000" (which identify the target) and service="api" (added by the job configuration in prometheus.yml). The value 3241 is slightly lower than the 3247 read in step 1: Prometheus shows the last scrape, which is 0 to 15 seconds old.
Read an API log line in the terminal. From the lab3 folder:
.\labo.ps1 journal apiOn the course machine, the last lines look like:
labo-api | {"horodatage": "2026-09-15T19:33:25.916+00:00", "niveau": "INFO", "id_requete": "5ccaee4d1a2a", "methode": "GET", "route": "/sante", "code": 200, "duree_ms": 1.0, "message": "GET /sante -> 200"}
labo-api | {"horodatage": "2026-09-15T19:33:26.217+00:00", "niveau": "INFO", "id_requete": "e64206564a68", "methode": "GET", "route": "/cours/{id}", "code": 200, "duree_ms": 8.2, "message": "GET /cours/C0043 -> 200"}What to see: GET /sante every few seconds is Docker checking the container's health (the healthcheck); the rest is the charge service. The labo-api | prefix is added by Docker Compose, it is not part of the JSON.
Check the labels Loki knows. Open http://localhost:3100/loki/api/v1/labels:
{"status":"success","data":["code","conteneur","niveau","service"]}Then http://localhost:3100/loki/api/v1/label/niveau/values:
{"status":"success","data":["ERROR","INFO","WARNING"]}What to see: four labels, not eight. Loki indexes neither id_requete, nor duree_ms, nor message: these fields stay in the text of the line, where LogQL can extract them on demand with | json (module 5). The three values of niveau confirm the table above.
The messages below were genuinely triggered in Prometheus, on the course machine. They will come back in module 2; you might as well recognize them now.
Forgetting the quotes around a label value: up{job=api} returns
invalid parameter "query": 1:8: parse error: unexpected identifier "api" in label matching, expected stringA label value is always a string between quotes: up{job="api"}. Even for a number: http_requetes_total{code=500} returns parse error: unexpected character inside braces: '5'; you must write code="500".
Calling rate() without a time window: rate(http_requetes_total) returns
invalid parameter "query": 1:6: parse error: expected type range vector in call to function "rate", got instant vectorrate() needs an interval between brackets: rate(http_requetes_total[1m]). Without brackets, you give it the last value (an instant vector) whereas it needs a sequence of values (a range vector).
Calling rate() on a gauge: rate(requetes_en_cours[1m]) triggers no error, but Prometheus displays a warning:
PromQL info: metric might not be a counter, name does not end in _total/_sum/_count/_bucket: "requetes_en_cours" (1:6)The result (0.017… on the course machine) is meaningless: the rate of increase of a value that goes up and down is not information. rate() is reserved for counters.
Getting a metric name wrong: http_request_total (singular, without the French e) returns an empty result, without an error message. Prometheus does not know this metric, it does not correct it. The exact name is http_requetes_total. Likewise, http_requetes_total{route="/inexistant"} returns an empty result: the unknown route is counted under route="inconnue", not under its real path.
The four Prometheus metric types are the counter (http_requetes_total, which only increases in one direction and is read with rate()), the gauge (requetes_en_cours, which goes up and down), the histogram (http_duree_requete_seconds, twelve lines per route: ten cumulative _bucket, _count, _sum) and the summary (absent from the lab, because its quantiles do not aggregate across instances). A time series is a unique combination of name and labels; http_requetes_total has 14 of them on the course machine. Cardinality explains why the metric carries route="/cours/{id}" whereas the log contains C0038 in its message. Prometheus works in pull mode: it reads (scrapes) eight targets every 15 seconds, grouped by job, identified by instance, and makes the up metric itself. A structured log of the lab is a JSON line with eight fields, including id_requete, also returned in the x-id-requete header; Loki only indexes four labels from it: service, conteneur, niveau, code. Ten services, nine ports (charge has none), eight Prometheus targets (charge and webhook expose no /metrics).
/metrics page you opened in step 1 (the # HELP, # TYPE lines, label escaping)._total for a counter, _seconds for a duration, _bytes for a size; the lab's API follows these conventions, with one exception: the names are in French.histogram_quantile.api/app.py to expose the metrics; module 3 has you add your own metric with it.up, filter by label, compute a rate(), read a histogram with histogram_quantile().