Retome o painel de controle do carro da lição 01. O odômetro só aumenta: é um contador (counter), como http_requetes_total. O medidor de combustível sobe ao encher e desce ao rodar: é um gauge (gauge), como requetes_en_cours. O caderno de manutenção que anota "12 trajetos de menos de 10 km, 30 de menos de 50 km, 3 de mais de 100 km" encaixa cada trajeto em uma faixa: é um histograma, como http_duree_requete_seconds. E a caixa-preta, que escreve uma linha por evento com a hora exata, é o log JSON da API. O Prometheus não recebe nada: é ele que vem ler os contadores a cada 15 segundos, como um fiscal que passa para conferir os medidores de cada carro da frota.
Uma métrica Prometheus sempre tem um nome, eventualmente labels, e um valor numérico. O tipo diz como ler esse valor no tempo. O Prometheus conhece quatro: o contador, o gauge, o histograma e o summary. Os três primeiros são usados pela API de catálogo; o quarto não é, e o motivo é instrutivo. Todas as linhas abaixo foram copiadas de http://localhost:8000/metrics na máquina do curso; os valores serão diferentes na sua.
Contador (counter). Um contador só aumenta, ou volta a zero se o serviço reiniciar. Ele conta eventos acumulados. A API expõe http_requetes_total, com três labels: o código de resposta, o método HTTP e a rota:
# 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.0O que você nunca faz com um contador: ler seu valor bruto para dizer "há 3247 requisições agora". Um contador nunca diminui; o que interessa é sua velocidade de crescimento, calculada com a função rate() (módulo 2). Documentação: Prometheus — Metric types, Counter.
Gauge (gauge). Um gauge sobe e desce livremente: um valor em um dado instante. A API expõe requetes_en_cours, o número de requisições em processamento no instante exato da coleta:
# HELP requetes_en_cours Nombre de requêtes HTTP en cours de traitement à cet instant.
# TYPE requetes_en_cours gauge
requetes_en_cours 1.0Uma requisição está em curso no instante da coleta; no instante seguinte, esse número pode ser 0 ou 4. A API também expõe gauges de configuração, cujo valor é mais uma informação do que uma medida: api_info{version="1.0.0"} 1.0 (a versão está no label, o valor sempre é 1) e api_panne_taux_erreurs 0.01 (a proporção de requisições que a API faz falhar propositalmente: 1% em funcionamento normal). Documentação: Prometheus — Metric types, Gauge.
Histograma (histogram). Um histograma mede a distribuição de um valor, como a duração de uma requisição HTTP: quantas requisições levaram menos de 5 ms, menos de 10 ms, menos de 25 ms…? A API expõe http_duree_requete_seconds. Diferente de um contador ou um gauge, um histograma publica várias linhas para uma única métrica: um contador por faixa (bucket), mais uma contagem total e uma soma. Aqui estão as doze linhas da rota /cours:
# 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.42293146001248Cada _bucket{le="…"} (le de less or equal) conta as requisições mais rápidas ou iguais a esse limite, de forma cumulativa: as 32 requisições de menos de 5 ms também são contadas nas 74 de menos de 10 ms, e nas 3279 do bucket +Inf (todas). _count é o número total de observações (3279, igual ao bucket +Inf), _sum a soma de todas as durações (85,4 segundos no total, ou seja, 26 ms em média por requisição). É isso que permite reconstruir um quantil depois, com histogram_quantile() (módulo 2). Leitura direta: 3248 requisições de 3279 levaram menos de 50 ms, ou seja, 99%. Documentação: Prometheus — Metric types, Histogram.
Summary (summary), e por que o labo não o usa. Um summary também mede uma distribuição, mas calcula seus quantis diretamente no programa observado, antes de publicá-los: uma linha {quantile="0.5"}, uma linha {quantile="0.9"}, mais _sum e _count. O problema: um quantil calculado no lado do cliente não pode ser recombinado com o de outra instância. Se o labo executasse três cópias da API, você não poderia fazer a média dos três quantile="0.9" para obter o verdadeiro percentil 90 do conjunto. Um histograma publica contadores brutos por faixa: o Prometheus pode somá-los entre instâncias antes de calcular o quantil. Por isso não existe nenhuma linha summary no /metrics da API do labo. Documentação: Prometheus — Histograms and summaries.
| Tipo | O que mede | No labo | Pergunta que responde |
|---|---|---|---|
| Contador | Um acumulado que só aumenta | http_requetes_total, inscriptions_total, cours_consultes_total | "Quantas requisições por segundo?" (com rate) |
| Gauge | Um valor instantâneo que sobe e desce | requetes_en_cours, api_disque_libre_octets, api_info | "Quantas neste momento?" |
| Histograma | Uma distribuição, por faixas cumulativas | http_duree_requete_seconds | "95% das requisições levam menos de quanto?" |
| Summary | Uma distribuição, quantis calculados no cliente | nenhum | A mesma, mas sem poder agregar entre instâncias |
Cada combinação única de nome de métrica e labels forma uma série temporal: uma sequência de pares (timestamp, valor) que o Prometheus armazena e consulta. http_requetes_total{code="200",methode="GET",route="/cours"} é uma série; http_requetes_total{code="201",methode="POST",route="/inscriptions"} é outra. Na máquina do curso, http_requetes_total conta 14 séries (14 combinações de código, método e rota que surgiram desde a inicialização). Os labels permitem filtrar e agrupar sem mudar o nome: "os erros de servidor, só em /inscriptions" se lê http_requetes_total{route="/inscriptions",code=~"5.."}. Documentação: Prometheus — Data model.
A cardinalidade de uma métrica é o número de séries distintas que ela produz. http_requetes_total{code,methode,route}, com cinco códigos, dois métodos e sete rotas em template, dá algumas dezenas de séries no máximo. Olhe o log da etapa 2 da lição 01: a requisição chamou /cours/C0038, mas a métrica traz route="/cours/{id}", o template da rota tal como o FastAPI a declarou. Se o label contivesse o identificador real, cada curso consultado criaria uma série: 64 cursos × 5 códigos × 2 métodos. Em um catálogo real com dezenas de milhares de cursos, a métrica explodiria e o Prometheus ficaria lento. A regra: um label deve ter um número limitado e razoável de valores; um identificador único, um endereço IP ou um timestamp não têm nada a fazer em um label. Uma rota que não existe é contada em route="inconnue", nunca no seu caminho real, pelo mesmo motivo.
O labo contém propositalmente uma exceção instrutiva: inscriptions_total{cours_id="C0028"} e cours_consultes_total{cours_id="…"} têm um label por curso. Com 64 cursos, isso continua limitado (64 séries cada). Com um milhão de cursos, seria um erro. O labo, aliás, tem um alerta PrometheusTropDeSeries que soa acima de 100.000 séries. Documentação: Prometheus — Instrumentation, cardinality.
O Prometheus funciona em pull (ele vai buscar): ele mesmo consulta, a cada 15 segundos no labo (scrape_interval: 15s em prometheus/prometheus.yml), a URL /metrics de cada serviço que monitora. É o inverso de um sistema em push, onde o serviço observado envia suas métricas para um coletor. O pull tem uma vantagem direta: se um serviço parar de responder, o Prometheus sabe imediatamente, a métrica up passa para 0, sem depender do serviço em falha para sinalizar sua própria ausência. É exatamente isso que a prática deste módulo faz você provocar com casser api.
| Termo | O que é | No labo |
|---|---|---|
| scrape | Uma leitura de /metrics pelo Prometheus | A cada 15 s, em cada um dos 8 targets |
| target (alvo) | Uma URL /metrics que o Prometheus lê | http://api:8000/metrics, http://node-exporter:9100/metrics… |
| exporter | Um programa que expõe no formato Prometheus as métricas de um sistema que não fala esse formato nativamente | node-exporter (a máquina hospedeira), cadvisor (os contêineres) |
| job | Um grupo de targets que fazem o mesmo trabalho | job="api", job="prometheus", job="loki"… 8 jobs |
| instance | Um target individual dentro de um job | instance="api:8000" |
up | A métrica que o Prometheus mesmo fabrica em cada scrape: 1 se o target respondeu, 0 caso contrário | up{job="api"} vale 1 em funcionamento normal |
Os nomes api:8000, node-exporter:9100 são os da rede interna do Docker Compose: é o Prometheus, dentro do seu contêiner, falando com a API dentro do dela. Da sua máquina, a mesma página é http://localhost:8000/metrics. Documentação: Prometheus — Configuration, scrape_config e Prometheus — Jobs and instances.
Um log estruturado é escrito em um formato que a máquina decompõe sem adivinhar (JSON, na maioria das vezes), em vez de uma frase livre. A API escreve na sua saída padrão uma linha JSON por requisição processada. Aqui está uma linha real, lida por .\labo.ps1 journal api na máquina do curso:
{"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"}| Campo | Exemplo | O que é |
|---|---|---|
horodatage | 2026-09-15T19:33:25.839+00:00 | O instante exato do evento, no formato ISO 8601, em UTC (+00:00) |
niveau | INFO | A gravidade: INFO (2xx), WARNING (404, 422), ERROR (500). Três valores no labo |
id_requete | 46bb533b33e9 | Um identificador único de 12 caracteres gerado para essa requisição, devolvido ao cliente no cabeçalho HTTP x-id-requete |
methode | GET | O método HTTP |
route | /cours/{id} | O template da rota, o mesmo que na métrica |
code | 200 | O código de resposta HTTP |
duree_ms | 13.9 | A duração do processamento, em milissegundos |
message | GET /cours/C0038 -> 200 | A frase legível: é aqui, e só aqui, que aparece o identificador real do curso |
O que é preciso ver: route mantém o template /cours/{id} (como a métrica), mas message contém C0038, o identificador real. Um log pode se permitir esse detalhe: o Loki não indexa o texto da mensagem, ele indexa apenas quatro labels (service, conteneur, niveau, code) extraídos pelo Alloy. O módulo 5 mostra como o Alloy lê o JSON e fabrica esses labels. Documentação: Grafana Loki — Labels.
Os outros serviços do labo não escrevem todos em JSON. O webhook, na inicialização, escreve texto livre: INFO: Uvicorn running on http://0.0.0.0:8090 (Press CTRL+C to quit). O serviço charge escreve JSON, mas com outros campos: a cada 30 segundos, um resumo {"niveau": "INFO", "message": "résumé des 30 dernières secondes", "requetes": {"200": 214, "total": 262, "201": 26, "500": 4, "404": 16, "422": 2}}. Um log estruturado não é um formato universal: é uma decisão tomada serviço por serviço.
Um traço acompanha uma única requisição por vários serviços: cada etapa (um span) registra seu nome, sua duração, e sua relação pai-filho com as outras. Ele responde a "a requisição levou 800 ms, em qual serviço esse tempo foi gasto?". O labo não instala nenhuma ferramenta de rastreamento: a API de catálogo e o gerador de carga são os dois únicos serviços de aplicação, ligados por uma simples chamada HTTP, o que limita o interesse pedagógico de um traço distribuído aqui. O id_requete do log já é o primeiro pedaço de um traço: é ele que o OpenTelemetry chamaria de trace id. O módulo 7 fala um pouco mais sobre isso. Documentação: OpenTelemetry — Traces.
| Serviço | Papel | Porta | URL na sua máquina |
|---|---|---|---|
prometheus | Lê os targets, armazena as séries, avalia as regras | 9090 | http://localhost:9090 |
alertmanager | Recebe os alertas do Prometheus, os agrupa e os roteia | 9093 | http://localhost:9093 |
grafana | Explora e visualiza Prometheus, Loki e Alertmanager | 3000 (GRAFANA_PORT) | http://localhost:3000 (admin / aiopsatlas2026) |
loki | Armazena os logs, indexados por labels | 3100 | http://localhost:3100/ready |
alloy | Descobre os contêineres e transporta seus logs para o Loki | 12345 | http://localhost:12345 |
node-exporter | Métricas da máquina hospedeira | 9100 | http://localhost:9100/metrics |
cadvisor | Métricas de cada contêiner | 8080 | http://localhost:8080 |
api | O serviço observado | 8000 | http://localhost:8000/cours · http://localhost:8000/metrics |
webhook | Recebe e exibe os alertas | 8090 | http://localhost:8090 |
charge | Gera tráfego para a API | nenhuma | nenhuma: não tem interface, só um log |
Nove portas, dez serviços: charge não escuta em nenhuma. Oito targets Prometheus, dez serviços: charge e webhook não expõem /metrics.
Este passo a passo assume o labo iniciado (lição 04). Se você está lendo esta lição antes, guarde-o para depois: cada etapa é feita no navegador ou em um terminal, apenas em modo de leitura.
Abra http://localhost:8000/metrics no navegador. É a página bruta que o Prometheus lê a cada 15 segundos: texto, uma linha por série, precedida de suas linhas # HELP e # TYPE. Na máquina do curso, ela tem 271 linhas. Compare com http://localhost:9100/metrics (node-exporter: 1578 linhas) e http://localhost:8080/metrics (cAdvisor: 3444 linhas, para dez contêineres).
O que é preciso ver: as linhas que começam com python_ e process_ no topo da página da API não são escritas pelo labo. A biblioteca prometheus_client as adiciona sozinha (memória do processo, coletor de lixo do Python). As métricas do curso começam em http_requetes_total.
Procure os quatro # TYPE da API. Na página, procure (Ctrl+F) por # TYPE http_: você encontra counter para http_requetes_total e histogram para http_duree_requete_seconds. Procure # TYPE requetes_en_cours: gauge. Procure summary: nenhum resultado.
O que é preciso ver: os três tipos usados, e a ausência proposital do quarto.
Conte as séries de http_requetes_total no Prometheus. Abra http://localhost:9090, digite http_requetes_total no campo de query e execute. Na máquina do curso, a tabela exibe 14 linhas, entre elas:
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"} 32O que é preciso ver: o Prometheus adicionou três labels aos da página /metrics: job="api" e instance="api:8000" (que identificam o target) e service="api" (adicionado pela configuração do job em prometheus.yml). O valor 3241 é um pouco menor que o 3247 lido na etapa 1: o Prometheus mostra o último scrape, que aconteceu entre 0 e 15 segundos atrás.
Leia uma linha de log da API no terminal. Na pasta lab3:
.\labo.ps1 journal apiNa máquina do curso, as últimas linhas se parecem com:
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"}O que é preciso ver: GET /sante a cada poucos segundos, é o Docker verificando a saúde do contêiner (o healthcheck); o resto vem do serviço charge. O prefixo labo-api | é adicionado pelo Docker Compose, não faz parte do JSON.
Verifique os labels que o Loki conhece. Abra http://localhost:3100/loki/api/v1/labels:
{"status":"success","data":["code","conteneur","niveau","service"]}Depois http://localhost:3100/loki/api/v1/label/niveau/values:
{"status":"success","data":["ERROR","INFO","WARNING"]}O que é preciso ver: quatro labels, não oito. O Loki não indexa id_requete, nem duree_ms, nem message: esses campos ficam no texto da linha, onde o LogQL pode extraí-los quando necessário com | json (módulo 5). Os três valores de niveau confirmam a tabela acima.
As mensagens abaixo foram provocadas de verdade no Prometheus, na máquina do curso. Elas voltarão no módulo 2; melhor já reconhecê-las agora.
Esquecer as aspas em torno de um valor de label: up{job=api} retorna
invalid parameter "query": 1:8: parse error: unexpected identifier "api" in label matching, expected stringUm valor de label é sempre uma string entre aspas: up{job="api"}. Mesmo para um número: http_requetes_total{code=500} retorna parse error: unexpected character inside braces: '5'; é preciso escrever code="500".
Chamar rate() sem uma janela de tempo: rate(http_requetes_total) retorna
invalid parameter "query": 1:6: parse error: expected type range vector in call to function "rate", got instant vectorrate() precisa de um intervalo entre colchetes: rate(http_requetes_total[1m]). Sem colchetes, você dá a ele o último valor (um instant vector), quando ele precisa de uma sequência de valores (um range vector).
Chamar rate() em um gauge: rate(requetes_en_cours[1m]) não gera erro, mas o Prometheus exibe um aviso:
PromQL info: metric might not be a counter, name does not end in _total/_sum/_count/_bucket: "requetes_en_cours" (1:6)O resultado (0.017… na máquina do curso) não tem nenhum sentido: a velocidade de crescimento de um valor que sobe e desce não é uma informação útil. rate() é reservado a contadores.
Errar o nome de uma métrica: http_request_total (no singular, sem o "e" francês) retorna um resultado vazio, sem mensagem de erro. O Prometheus não conhece essa métrica, ele não a corrige. O nome exato é http_requetes_total. Da mesma forma, http_requetes_total{route="/inexistant"} retorna um resultado vazio: a rota desconhecida é contada em route="inconnue", não no seu caminho real.
Os quatro tipos de métricas Prometheus são o contador (http_requetes_total, que só aumenta e se lê com rate()), o gauge (requetes_en_cours, que sobe e desce), o histograma (http_duree_requete_seconds, doze linhas por rota: dez _bucket cumulativos, _count, _sum) e o summary (ausente do labo, porque seus quantis não se agregam entre instâncias). Uma série temporal é uma combinação única de nome e labels; http_requetes_total tem 14 na máquina do curso. A cardinalidade explica por que a métrica traz route="/cours/{id}" enquanto o log contém C0038 na sua mensagem. O Prometheus funciona em pull: ele lê (scrape) oito targets a cada 15 segundos, agrupados por job, identificados por instance, e fabrica ele mesmo a métrica up. Um log estruturado do labo é uma linha JSON com oito campos, incluindo id_requete, devolvido também no cabeçalho x-id-requete; o Loki indexa apenas quatro desses como labels: service, conteneur, niveau, code. Dez serviços, nove portas (charge não tem nenhuma), oito targets Prometheus (charge e webhook não expõem /metrics).
/metrics que você abriu na etapa 1 (as linhas # HELP, # TYPE, o escape dos labels)._total para um contador, _seconds para uma duração, _bytes para um tamanho; a API do labo respeita essas convenções, com uma exceção: os nomes estão em francês.histogram_quantile.api/app.py para expor as métricas; o módulo 3 faz você adicionar sua própria métrica com ela.up, filtrar por label, calcular um rate(), ler um histograma com histogram_quantile().