Prática orientada — Iniciar, verificar, quebrar e reparar o labo

Prática guiada51 min
Duração
60 a 90 min
Módulo
1/7
Você vai construir
um labo de dez serviços iniciado e verificado por três caminhos (o script etat, a aba Graph do Prometheus, o Explore no Grafana), depois propositalmente quebrado com casser api e reparado com reparer
Entregável
a saída completa de etat com Labo : 10/10 services, 8/8 cibles up, 0 alertes actives., mais três linhas escritas à mão: a hora em que APIInjoignable passou para firing no Prometheus, a hora em que o webhook a recebeu, a hora em que ela passou para resolved

Como ler esta página. Cada seção está recolhida sob seu título: clique em "Afficher …" para abri-la, e feche-a quando terminar para manter a página legível. Ordem de leitura: Objectif, depois En bref (os comandos a digitar), depois Le jeu de données (a ler antes de qualquer query), depois as queries Prometheus (P1 a P12) e Grafana Explore (G1 a G8), classificadas da mais simples (up, {service="api"}) à mais reveladora, com uma explicação depois de cada uma. O passo a passo detalhado, com a saída esperada de cada comando e a falha a provocar, está em anexo: anexo A para Windows (PowerShell), anexo B para Linux, macOS, WSL 2 e Git Bash. Abra apenas um anexo, o do seu sistema. O anexo C, comum, reúne os casos em que algo trava. Todas as saídas desta página foram capturadas no labo do curso; os valores que dependem do momento (contadores, durações, timestamps) serão diferentes na sua máquina, os formatos serão idênticos.

Objetivo

Você entra na equipe que opera o catálogo de cursos de uma plataforma online. Sua chefe te entrega o kit do labo: "Amanhã de manhã, eu quero a stack de observabilidade rodando na sua máquina, a API dentro dela, e a prova de que você sabe ler uma falha sem precisar me chamar." Você vai então iniciar os dez serviços, provar que o Prometheus lê corretamente seus oito targets e que o Loki recebe os logs da API, digitar doze queries PromQL e oito queries LogQL para aprender a ler o que o labo mede, depois parar a API de propósito. Você vai observar a falha se propagar: etat a vê em dois segundos, o Prometheus marca o target como DOWN, o alerta APIInjoignable passa de pending para firing, o Alertmanager o envia para o webhook. Depois você repara e observa o alerta se apagar. Reconhecer "esse serviço está parado" em dez segundos, e saber onde procurar, é o que evita horas de busca no lugar errado.

As nove etapas deste diagrama são detalhadas, com a saída esperada de cada comando, no anexo A (Windows) ou no anexo B (Linux, macOS), no final da página.

En bref: os comandos do labo

Mostrar os comandos

Kit do labo: https://github.com/hrhouma2/aiopsatlas-observabilite-labo-fr

Você clona o kit em uma pasta lab3, verifica que o Docker está pronto, inicia os dez serviços, abre as páginas web, digita as queries, depois quebra e repara. No final, etat deve exibir Labo : 10/10 services, 8/8 cibles up, 0 alertes actives., a API deve conhecer 64 cours, e o webhook deve ter recebido duas notificações para APIInjoignable: uma firing, uma resolved. Comece executando este bloco.

Windows (PowerShell)

powershell
git clone https://github.com/hrhouma2/aiopsatlas-observabilite-labo-fr.git lab3
cd lab3
ls                       # explorer le contenu : docker-compose.yml, labo.ps1, labo.sh, api/, prometheus/, grafana/, modules/
.\labo.ps1 prerequis
.\labo.ps1 demarrer
.\labo.ps1 etat          # attendu : Labo : 10/10 services, 8/8 cibles up, 0 alertes actives.

Espere dois minutos (o tempo para o Prometheus ter algumas medidas), depois abra as páginas no navegador:

text
Prometheus       http://localhost:9090          (Status → Target health : 8 cibles UP ; onglet Graph pour P1 à P12)
Grafana          http://localhost:3000          (utilisateur admin · mot de passe aiopsatlas2026 ; menu → Explore, source Loki pour G1 à G8)
Alertmanager     http://localhost:9093          (vide au départ)
API catalogue    http://localhost:8000/cours    ·   http://localhost:8000/metrics
Webhook          http://localhost:8090          (vide au départ)

As queries P1 a P12 também estão em modules\01-le-labo\requetes.txt, e G1 a G8 em modules\01-le-labo\requetes-logql.txt: abra-as em um editor e copie e cole. Depois a falha:

powershell
.\labo.ps1 casser api    # arrête le conteneur de l'API ; la charge continue de frapper dans le vide
.\labo.ps1 etat          # attendu : 9/10 services, 7/8 cibles up ; regarde aussi Targets, Alerts, 9093 et 8090
.\labo.ps1 reparer       # redémarre l'API
.\labo.ps1 etat          # attendu : Labo : 10/10 services, 8/8 cibles up, 0 alertes actives.

Se o PowerShell recusar .\labo.ps1 ("l'exécution de scripts est désactivée"): Set-ExecutionPolicy -Scope CurrentUser RemoteSigned, responda O, execute novamente. Se a porta 3000 já estiver ocupada na sua máquina, $env:GRAFANA_PORT = '3001' antes de demarrer, e substitua 3000 por 3001 nas URLs do Grafana.

Linux, macOS, WSL 2, Git Bash

bash
git clone https://github.com/hrhouma2/aiopsatlas-observabilite-labo-fr.git lab3
cd lab3
ls                       # explorer le contenu : docker-compose.yml, labo.sh, labo.ps1, api/, prometheus/, grafana/, modules/
./labo.sh prerequis
./labo.sh demarrer
./labo.sh etat           # attendu : Labo : 10/10 services, 8/8 cibles up, 0 alertes actives.

Espere dois minutos, depois abra as páginas no navegador:

text
Prometheus       http://localhost:9090          (Status → Target health : 8 cibles UP ; onglet Graph pour P1 à P12)
Grafana          http://localhost:3000          (utilisateur admin · mot de passe aiopsatlas2026 ; menu → Explore, source Loki pour G1 à G8)
Alertmanager     http://localhost:9093          (vide au départ)
API catalogue    http://localhost:8000/cours    ·   http://localhost:8000/metrics
Webhook          http://localhost:8090          (vide au départ)

As queries estão em modules/01-le-labo/requetes.txt (P1 a P12) e modules/01-le-labo/requetes-logql.txt (G1 a G8). Depois a falha:

bash
./labo.sh casser api     # arrête le conteneur de l'API ; la charge continue de frapper dans le vide
./labo.sh etat           # attendu : 9/10 services, 7/8 cibles up ; regarde aussi Targets, Alerts, 9093 et 8090
./labo.sh reparer        # redémarre l'API
./labo.sh etat           # attendu : Labo : 10/10 services, 8/8 cibles up, 0 alertes actives.

Se a porta 3000 estiver ocupada: GRAFANA_PORT=3001 ./labo.sh demarrer, depois 3001 nas URLs do Grafana.

O conjunto de dados: o que você vai manipular

Afficher le jeu de données

Antes de digitar uma única query, veja o que o labo observa. Tudo gira em torno de uma API de catálogo de cursos: um pequeno serviço web escrito em Python (FastAPI) que serve 64 cursos e registra inscrições. Um segundo serviço, charge, desempenha o papel dos usuários: ele chama a API continuamente, com requisições bem-sucedidas, alguns 404 propositais e, uma vez em cada cem, um erro 500 que a própria API fabrica. Tudo o que você vai ler no Prometheus e no Loki vem desses dois serviços. O resto do kit, resumido:

text
lab3/
├── api/
│   ├── app.py                     l'API catalogue (FastAPI) : 6 routes publiques, 3 routes /admin
│   └── donnees/cours.json         64 cours → servis par GET /cours et GET /cours/{id}
├── charge/charge.py               le générateur de trafic : GET /cours, /cours/{id}, POST /inscriptions, GET /lent, des 404
├── prometheus/
│   ├── prometheus.yml             8 cibles lues toutes les 15 s (scrape_interval: 15s)
│   └── regles/alertes.yml         10 règles d'alerte ; APIInjoignable est la première
├── alertmanager/alertmanager.yml  groupe les alertes et les envoie au webhook (group_wait: 10s)
├── alloy/config.alloy             lit les journaux des conteneurs et les pousse dans Loki
├── grafana/provisioning/          3 tableaux de bord et 3 sources de données, créés au démarrage
└── modules/01-le-labo/
    ├── requetes.txt               P1 à P12, à coller dans Prometheus
    └── requetes-logql.txt         G1 à G8, à coller dans Grafana Explore

Abra os dados você mesmo, leva dez segundos:

powershell
# Windows (PowerShell), depuis le dossier lab3
Get-Content api\donnees\cours.json -TotalCount 16
Invoke-RestMethod "http://localhost:8000/cours?limite=2"
Invoke-RestMethod http://localhost:8000/cours/C0001
(Invoke-WebRequest http://localhost:8000/metrics -UseBasicParsing).Content -split "`n" | Select-String "^http_requetes_total"
bash
# Linux, macOS, WSL 2, Git Bash, depuis le dossier lab3
head -n 16 api/donnees/cours.json
curl -s "http://localhost:8000/cours?limite=2"
curl -s http://localhost:8000/cours/C0001
curl -s http://localhost:8000/metrics | grep "^http_requetes_total"

A API de catálogo: 64 cursos em um arquivo JSON

api/donnees/cours.json é um array JSON com 64 objetos, um por curso, identificadores C0001 a C0064. O primeiro, como a API o devolve em GET /cours/C0001:

json
{"id":"C0001","titre":"Introduction à Python","categorie":"programmation","niveau":"debutant","prix":89,"duree_heures":6,"professeur":"Karim Haddad","tags":["code","algorithmes"],"note":4.8,"inscrits":2319}
CampoExemploO que é
idC0001Identificador do curso, C seguido de quatro dígitos, de C0001 a C0064
titreIntroduction à PythonTítulo exibido
categorieprogrammationUma das nove categorias: cloud, donnees, gestion, ia, outils, programmation, securite, systemes, web
niveaudebutantdebutant, intermediaire ou avance
prix89Preço em dólares, inteiro
duree_heures6Duração total, em horas
professeurKarim HaddadUm dos dez professores do catálogo
tags["code","algorithmes"]Lista de palavras-chave
note4.8Nota média sobre 5
inscrits2319Número de inscritos no carregamento; as inscrições feitas durante o labo são contadas à parte, na métrica inscriptions_total

As rotas da API e o que elas respondem no labo do curso:

RotaResposta realO que faz
GET /sante{"etat":"ok","version":"1.0.0","cours":64}O teste de saúde que o Docker chama; etatversion e cours aqui
GET /cours?limite=2{"total":64,"page":1,"limite":2,"cours":[…]}A lista paginada; filtros categorie e niveau (?categorie=cloud"total":6)
GET /cours/C0001o documento acimaUma ficha; a API conta cada consulta em cours_consultes_total{cours_id="C0001"}
GET /cours/C9999404 {"detail":"cours C9999 introuvable"}Um 404 limpo: o serviço está saudável, o recurso não existe
POST /inscriptions201 (ou 404 se o curso não existir, 422 se o corpo for inválido)Chamada pelo charge; incrementa inscriptions_total{cours_id="…"}
GET /lent{"attente_ms":305}Uma rota propositalmente lenta (300 a 900 ms) para alimentar o histograma de latência
GET /admin/etat{"taux_erreurs":0.01,"lenteur_ms":0,"inscriptions_enregistrees":855,…}As configurações de falha; casser erreurs e casser lenteur as alteram, reparer as restaura
GET /metricscerca de 270 linhas de textoO que o Prometheus lê a cada 15 segundos

Cada resposta traz um cabeçalho x-id-requete (por exemplo x-id-requete: c5c49525ea53): é o mesmo identificador do campo id_requete da linha de log escrita para essa requisição. Ele vai servir na query G7.

As métricas: o que /metrics expõe, uma linha real por tipo

A página http://localhost:8000/metrics é texto, uma série por linha, precedida de duas linhas de comentário # HELP (para que serve a métrica) e # TYPE (seu tipo). No labo do curso, ela tem cerca de 270 linhas. As quatro métricas que você vai consultar, copiadas da página:

text
# 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="500",methode="GET",route="/cours"} 32.0

# HELP requetes_en_cours Nombre de requêtes HTTP en cours de traitement à cet instant.
# TYPE requetes_en_cours gauge
requetes_en_cours 1.0

# 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.42293146001248

# HELP api_info Informations sur l'API (toujours 1) ; la version est dans le label.
# TYPE api_info gauge
api_info{version="1.0.0"} 1.0
TipoMétrica do laboComo ler
counter (contador)http_requetes_totalSó sobe: 3247 respostas 200 em /cours desde a inicialização da API. Só sua velocidade tem sentido (P5)
gauge (gauge)requetes_en_coursSobe e desce: 1 requisição sendo processada no instante da leitura. Se lê como está (P11)
histogram (histograma)http_duree_requete_secondsFaixas cumulativas: 3248 requisições em /cours levaram menos de 50 ms (le="0.05"), 3279 no total (+Inf = _count). _sum / _count = duração média (aqui 26 ms). P10 extrai um p95 disso
info (um gauge em 1)api_infoO valor é sempre 1; a informação está no label version="1.0.0"

Não há summary nessa API: o Prometheus recomenda evitá-lo em favor do histograma, que se agrega entre instâncias.

Os labels: as colunas dos seus futuros painéis

Uma série é um nome de métrica mais um conjunto de pares label="valor". Duas origens:

LabelDefinido porValores no labo
methodea APIGET, POST
routea API/sante, /cours, /cours/{id}, /inscriptions, /lent, /admin/etat, inconnue (qualquer URL que não existe)
codea API200, 201, 404, 422, 500
lea API, só no histograma0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.0, +Inf
cours_ida API, em inscriptions_total e cours_consultes_totalC0001 a C0064
jobo Prometheus, conforme prometheus.ymlprometheus, api, node-exporter, cadvisor, alertmanager, grafana, loki, alloy
instanceo Prometheuso endereço lido: api:8000, localhost:9090, grafana:3000
serviceo Prometheus, adicionado manualmente em prometheus.yml para o job apiapi

Observe a rota /cours/{id}: a API normaliza a URL antes de contar. /cours/C0001 e /cours/C0043 caem na mesma série. Sem isso, haveria 64 séries por código em vez de uma, e 64 vezes mais linhas em /metrics.

Os logs: uma linha JSON por requisição

A API escreve uma linha de log por requisição processada, em JSON. O Alloy lê a saída de cada contêiner labo-* e a envia para o Loki. Uma linha real, lida com .\labo.ps1 journal api (ou ./labo.sh journal api):

json
{"horodatage": "2026-09-15T19:33:14.781+00:00", "niveau": "ERROR", "id_requete": "dc1c2ff189a6", "methode": "GET", "route": "/cours/{id}", "code": 500, "duree_ms": 0.1, "message": "GET /cours/C0019 -> 500"}
CampoExemploO que é
horodatage2026-09-15T19:33:14.781+00:00Data e hora UTC, com precisão de milissegundos
niveauERRORINFO (2xx), WARNING (4xx), ERROR (5xx)
id_requetedc1c2ff189a6O identificador devolvido no cabeçalho x-id-requete da resposta
methode, route, codeGET, /cours/{id}, 500Os mesmos valores dos labels de http_requetes_total: é a ponte entre métricas e logs
duree_ms0.1Duração de processamento, em milissegundos
messageGET /cours/C0019 -> 500A frase legível, com a URL real desta vez (C0019, não {id})

O Loki não lê o JSON linha por linha no momento da requisição, a menos que você peça (| json, query G5). O que ele indexa são labels definidos pelo Alloy na chegada:

Label LokiValoresDefinido por
serviceapi, charge, webhook, prometheus, alertmanager, grafana, loki, alloy, node-exporter, cadvisorAlloy, conforme o nome do serviço no Compose
conteneurlabo-api, labo-chargeAlloy, conforme o nome do contêiner
niveauINFO, WARNING, ERRORAlloy, extraído do campo niveau do JSON
code200, 201, 404, 422, 500Alloy, extraído do campo code do JSON
detected_levelinfo, warn, errorO próprio Loki, que adivinha o nível; você pode ignorá-lo

O fio condutor

Guarde duas coisas que você vai reencontrar em todo lugar: a série http_requetes_total{code="500",route="/cours"}, em 32 em /metrics no momento da captura, que você reverá em 32 em P4; e o identificador dc1c2ff189a6, o da linha de erro acima, que você reencontrará em G2 e depois vai procurar sozinho em G7. As métricas contam, os logs contam histórias; ambos falam da mesma requisição.

Primeiras queries: uma noção de cada vez

As duas seções a seguir contêm vinte queries: doze para o Prometheus (P1 a P12), oito para o Loki através do Grafana Explore (G1 a G8). Elas estão classificadas da mais simples à mais reveladora, e cada uma acrescenta apenas uma novidade em relação à anterior. Se uma query parecer obscura, é quase sempre porque a anterior ainda não está clara: volte atrás em vez de continuar.

Digite cada query você mesmo, compare o resultado com o da página, leia a explicação, depois passe para a próxima. Os números serão diferentes na sua máquina: os contadores sobem desde a inicialização da sua API, não da do curso. Os formatos (o número de séries, os labels, a ordem de grandeza) devem ser os mesmos. Prometheus e Loki usam a mesma ideia de base, um conjunto de labels entre chaves, e isso é proposital: o que você aprende em P2 serve em G1.

Prometheus, na aba Graph

Afficher les 12 requêtes PromQL (P1 à P12)

Abra http://localhost:9090. Você chega na página Query (o menu do topo oferece Query, Alerts, Status). Cole uma query no campo, aperte Enter ou clique em Execute. O resultado aparece abaixo na aba Table (uma linha por série, o valor à direita); a aba Graph desenha as mesmas séries no tempo. Fique em Table para esta seção, salvo indicação contrária. Sob as abas, uma linha do tipo Load time: 40ms Result series: 8 diz quantas séries responderam.

A imagem a manter em mente: o Prometheus é um caderno de medições. A cada 15 segundos, ele passa por cada um dos seus oito targets, lê a página /metrics deles e anota cada valor com a hora. Uma query PromQL é uma pergunta feita a esse caderno.

P1. Quem responde?

promql
up
text
up{instance="localhost:9090", job="prometheus"}               1
up{instance="alloy:12345", job="alloy"}                       1
up{instance="api:8000", job="api", service="api"}             1
up{instance="cadvisor:8080", job="cadvisor"}                  1
up{instance="alertmanager:9093", job="alertmanager"}          1
up{instance="loki:3100", job="loki"}                          1
up{instance="node-exporter:9100", job="node-exporter"}        1
up{instance="grafana:3000", job="grafana"}                    1

Result series: 8, todas em 1.

O que a query pede: "Me dê o último valor da métrica up para todos os targets."

Zero parâmetro: apenas um nome de métrica. up não é exposta por nenhum target; é o Prometheus que a fabrica a cada scrape: 1 se a página /metrics respondeu, 0 caso contrário. Oito séries porque prometheus.yml declara oito jobs. Cada linha se lê: o nome da métrica, depois entre chaves os labels que o Prometheus definiu (job e instance em todas, service a mais na API), depois o valor. Equivalente SQL: SELECT * FROM up. O que o Prometheus faz que o SQL não faz: ele produziu ele mesmo essa tabela indo bater em oito portas.

Para entender bem: por que 8 e não 10?

O labo tem dez contêineres, mas o Prometheus lê apenas oito: charge e webhook não expõem uma página /metrics nessa versão do kit, então não são targets. etat conta os dois separadamente: 10/10 services (os contêineres) e 8/8 cibles up (os scrapes). Se um dia up retornar 7 séries em vez de 8, não é que um target caiu (ele estaria em 0): é que um job desapareceu da configuração. O kit tem um alerta para isso, CibleAbsente.

P2. Um único target

promql
up{job="api"}
text
up{instance="api:8000", job="api", service="api"}             1

O que a query pede: "O valor de up, apenas para as séries cujo label job vale api."

Uma única novidade: o seletor {job="api"}. As chaves filtram pelos labels, como um WHERE job = 'api'. As aspas são obrigatórias em torno do valor: up{job=api} é recusado com parse error: unexpected identifier "api" in label matching, expected string. É exatamente essa expressão, up{job="api"} == 0, que a regra APIInjoignable monitora; você a verá passar para 0 no anexo.

P3. O contador bruto

promql
http_requetes_total
text
http_requetes_total{code="200", instance="api:8000", job="api", methode="GET", route="/sante", service="api"}             91
http_requetes_total{code="200", instance="api:8000", job="api", methode="GET", route="/cours/{id}", service="api"}       2714
http_requetes_total{code="404", instance="api:8000", job="api", methode="GET", route="/cours/{id}", service="api"}        209
http_requetes_total{code="200", instance="api:8000", job="api", methode="GET", route="/lent", service="api"}              314
http_requetes_total{code="404", instance="api:8000", job="api", methode="GET", route="inconnue", service="api"}           312
http_requetes_total{code="200", instance="api:8000", job="api", methode="GET", route="/cours", service="api"}            3241
http_requetes_total{code="201", instance="api:8000", job="api", methode="POST", route="/inscriptions", service="api"}     855
http_requetes_total{code="404", instance="api:8000", job="api", methode="POST", route="/inscriptions", service="api"}      46
http_requetes_total{code="500", instance="api:8000", job="api", methode="GET", route="/cours", service="api"}              32
http_requetes_total{code="422", instance="api:8000", job="api", methode="POST", route="/inscriptions", service="api"}      51
http_requetes_total{code="500", instance="api:8000", job="api", methode="GET", route="/cours/{id}", service="api"}         27
http_requetes_total{code="200", instance="api:8000", job="api", methode="GET", route="/admin/etat", service="api"}         14
http_requetes_total{code="500", instance="api:8000", job="api", methode="GET", route="/lent", service="api"}                1
http_requetes_total{code="500", instance="api:8000", job="api", methode="POST", route="/inscriptions", service="api"}       7

Result series: 14 no labo do curso (o número exato depende das combinações que charge já produziu; ele sobe até 16 com o tempo).

O que a query pede: "Todas as séries do contador http_requetes_total, com seu valor atual."

Nada de novo na sintaxe: um nome, como P1. O que é novo é o que você lê. Compare com a página /metrics: a linha http_requetes_total{code="200",methode="GET",route="/cours"} 3247.0 virou http_requetes_total{code="200", instance="api:8000", job="api", methode="GET", route="/cours", service="api"} 3241. O Prometheus adicionou três labels (instance, job, service) e o valor difere em algumas unidades: a página foi lida em outro momento. Uma série por combinação (methode, route, code): é isso que se chama a cardinalidade da métrica, aqui 14.

A diferença essencial entre /metrics e o Prometheus. A página /metrics é o estado da API no instante em que você a abre, sem histórico. O Prometheus mantém todas as leituras, uma a cada 15 segundos, e é isso que permite P5: calcular uma velocidade supõe ter pelo menos dois pontos.

P4. Somente os erros 500

promql
http_requetes_total{code="500"}
text
http_requetes_total{code="500", instance="api:8000", job="api", methode="GET", route="/cours", service="api"}              32
http_requetes_total{code="500", instance="api:8000", job="api", methode="GET", route="/cours/{id}", service="api"}         27
http_requetes_total{code="500", instance="api:8000", job="api", methode="GET", route="/lent", service="api"}                1
http_requetes_total{code="500", instance="api:8000", job="api", methode="POST", route="/inscriptions", service="api"}       7

O que a query pede: "As séries de http_requetes_total cujo label code vale 500."

Nada de novo: é P2 aplicado a P3. Quatro séries, uma por rota afetada. O fio condutor está aqui: route="/cours" em 32, o valor lido em /metrics. O 500 é uma string, não um número: http_requetes_total{code=500} é recusado (parse error: unexpected character inside braces: '5'). Esses 500 não são uma falha: o charge provoca propositalmente um erro em cada cem (taux_erreurs: 0.01 em /admin/etat) para que as curvas de erro nunca fiquem vazias.

Bilan de P1 a P4: você ainda não calculou nada. Você leu valores instantâneos e aprendeu a filtrá-los por label.

P5. A velocidade de um contador

promql
rate(http_requetes_total[1m])
text
{code="200", instance="api:8000", job="api", methode="GET", route="/sante", service="api"}             0.11112345816201799
{code="200", instance="api:8000", job="api", methode="GET", route="/cours/{id}", service="api"}        3.000333370374486
{code="404", instance="api:8000", job="api", methode="GET", route="/cours/{id}", service="api"}        0.2666962995888432
{code="200", instance="api:8000", job="api", methode="GET", route="/lent", service="api"}              0.42226914101566837
{code="200", instance="api:8000", job="api", methode="GET", route="/cours", service="api"}             3.733748194243805
{code="201", instance="api:8000", job="api", methode="POST", route="/inscriptions", service="api"}     0.8667629736637403
{code="500", instance="api:8000", job="api", methode="GET", route="/cours", service="api"}             0.0222246916324036

Result series: 14, em requisições por segundo.

O que a query pede: "Para cada série do contador, quanto ela aumentou por segundo, em média, no último minuto?"

Uma única novidade, em duas partes inseparáveis: [1m] transforma a série em faixa (todos os valores do último minuto, em vez de apenas o último), e rate() calcula a inclinação dessa faixa. Olhe o resultado: o nome da métrica desapareceu das chaves, porque não é mais http_requetes_total, é uma velocidade derivada. /cours recebe 3,7 requisições por segundo, /sante 0,11 (uma a cada 9 segundos: é o healthcheck do Docker). Equivalente SQL: não há um simples; seriam necessárias duas leituras, uma subtração e uma divisão pelo tempo decorrido. rate() faz isso para cada série, e ainda corrige as reinicializações a zero quando a API reinicia.

Sem a faixa, o Prometheus recusa: rate(http_requetes_total)parse error: expected type range vector in call to function "rate", got instant vector. Você vai ler essa mensagem com frequência; ela quer dizer "está faltando […]".

Para entender bem: por que nunca lemos um contador como está

3241 requisições 200 em /cours não diz nada: desde quando? Se a API roda há uma hora, é calmo; há um minuto, é um ataque. Um contador só vale pela sua inclinação. É por isso que em um painel, você nunca verá http_requetes_total bruto, mas sempre rate(http_requetes_total[…]). A janela [1m] suaviza em um minuto; [5m] suaviza mais (curva mais calma, reação mais lenta). O kit usa [5m] em seus alertas e [1m] aqui para que você veja algo se mexer. Regra prática: a janela deve conter pelo menos dois scrapes, então aqui pelo menos [30s]; rate(http_requetes_total[10s]) retorna um resultado vazio.

P6. Somar por rota

promql
sum by (route) (rate(http_requetes_total[1m]))
text
{route="/sante"}          0.11112345816201799
{route="/cours/{id}"}     3.3114790532281364
{route="/lent"}           0.42226914101566837
{route="inconnue"}        0.3778197577508612
{route="/cours"}          3.7559728858762087
{route="/inscriptions"}   0.9334370485609511
{route="/admin/etat"}     0.0444493832648072

Result series: 7.

O que a query pede: "Pegue as velocidades de P5 e some-as mantendo apenas o label route."

Uma única novidade: a agregação sum by (route) (…). Ela funde todos os outros labels (code, methode, instance…) e soma o que resta. /cours/{id} passa de três séries (200, 404, 500) para uma: 3,00 + 0,27 + 0,04 = 3,31. Equivalente SQL: SELECT route, SUM(vitesse) FROM … GROUP BY route. Os parênteses em torno de route são obrigatórios: sum by route (…) é recusado (parse error: unexpected identifier "route" in grouping opts, expected "(").

P7. Somar por código

promql
sum by (code) (rate(http_requetes_total[1m]))
text
{code="200"}    7.311923547060784
{code="404"}    0.6445160573397044
{code="201"}    0.8667629736637403
{code="500"}    0.0888987665296144
{code="422"}    0.0444493832648072

O que a query pede: "A mesma soma de P6, mas agrupada por código HTTP."

Nada de novo: P6 com outro label. É o que faz o painel "Respostas por código" do painel de controle "API catalogue — signaux dorés" no Grafana. Cinco códigos, cinco linhas; os 500 a 0,09 por segundo, ou seja, pouco mais de um erro a cada doze segundos.

P8. O tráfego total

promql
sum(rate(http_requetes_total[1m]))
text
{}    8.956550727858652

O que a query pede: "Some todas as velocidades, sem manter nenhum label."

Uma única novidade: sum(…) sem by. Resultado: uma série única, com um conjunto de labels vazio ({}), o valor 8,96 requisições por segundo. É o primeiro dos quatro sinais de ouro, o tráfego. Verifique: a soma das sete linhas de P6 dá exatamente 8,96.

Bilan de P5 a P8: você sabe transformar um contador em velocidade, depois agrupar essa velocidade como quiser. Três quartos dos painéis de controle do Prometheus só fazem isso.

P9. A taxa de erros

promql
sum(rate(http_requetes_total{code=~"5.."}[1m])) / sum(rate(http_requetes_total[1m]))
text
{}    0.009925558312655085

O que a query pede: "A velocidade das respostas cujo código começa com 5, dividida pela velocidade de todas as respostas."

Duas novidades, mas pequenas. Primeiro =~: um seletor por expressão regular, "5.." = um 5 seguido de dois caracteres quaisquer, portanto todos os 5xx. Depois a divisão de dois resultados: o Prometheus divide as séries que têm os mesmos labels, e aqui os dois lados têm um conjunto vazio {}, então casam. Resultado: 0,0099, ou seja, 1%; é o valor configurado em /admin/etat (taux_erreurs: 0.01). Segundo sinal de ouro, os erros. A regra de alerta TauxErreursEleve do kit dispara quando essa mesma expressão, calculada em 5 minutos, ultrapassa 0,05.

P10. A latência p95

promql
histogram_quantile(0.95, sum by (le) (rate(http_duree_requete_seconds_bucket[5m])))
text
{}    0.09797705555555555

O que a query pede: "A partir das faixas do histograma de duração, todas as rotas juntas, abaixo de qual valor caem 95% das requisições dos últimos 5 minutos?"

Uma única novidade: histogram_quantile(0.95, …). Ela precisa receber as faixas _bucket somadas por le (é por isso que o sum by (le) é obrigatório: sem ele, ela calcula um quantil por rota e o resultado não tem mais o sentido esperado). Resultado: 0,098 segundo, ou seja, 95% das requisições são servidas em menos de 98 ms. É o terceiro sinal de ouro, a latência, e a métrica monitorada pelo alerta LatenceP95Elevee (limite: 0,5 s). Observe a distribuição das faixas de /cours no conjunto de dados: 3248 requisições de 3279 abaixo de 50 ms, mas /lent (300 a 900 ms) puxa o p95 global para cima.

Para entender bem: o que contém um bucket de histograma?

Cada linha _bucket{le="0.05"} conta as requisições que levaram no máximo 0,05 segundo (le = less or equal). As faixas são cumulativas: le="0.1" também contém tudo o que estava em le="0.05". A última, le="+Inf", contém tudo, e sempre vale _count. histogram_quantile procura a faixa onde a curva cumulativa ultrapassa 95% e interpola dentro dela. A precisão depende, portanto, da escolha das faixas: entre 0.05 e 0.1, o Prometheus assume uma distribuição uniforme. É por isso que o resultado, 0.0979…, não é um valor medido, mas uma estimativa.

P11. Um gauge, tal como é

promql
requetes_en_cours
text
requetes_en_cours{instance="api:8000", job="api", service="api"}    0

O que a query pede: "O último valor do gauge requetes_en_cours."

Nada de novo na sintaxe, é P1. O que é novo é o tipo: um gauge se lê tal como é, sem rate(). Zero ou um, dependendo do instante: a API processa cada requisição em alguns milissegundos, é raro pegar uma em andamento. É o quarto sinal de ouro, a saturação: se esse valor subisse para 50, a API estaria sobrecarregada. rate(requetes_en_cours[1m]) não gera erro, mas retorna um número sem sentido; o Prometheus não te protege dessa confusão.

P12. Os alertas, vistos pelo Prometheus

promql
ALERTS
text
Empty query result

O que a query pede: "Os alertas atualmente pending ou firing."

Nada de novo: um nome de métrica, como P1. ALERTS é, como up, fabricada pelo Prometheus: uma série por alerta ativo, com os labels alertname e alertstate. Em um labo saudável, o resultado é vazio: Empty query result. Isso não é um erro, é a melhor resposta possível. Você vai digitar essa query novamente durante a falha do anexo e vai ver aparecer ALERTS{alertname="APIInjoignable", alertstate="pending", …} e depois alertstate="firing".

Bilan de P9 a P12: os quatro sinais de ouro (tráfego P8, erros P9, latência P10, saturação P11) cabem em quatro queries, e os alertas são uma métrica como qualquer outra.

A mensagem para levar. O que o SQL também faz: filtrar por coluna ({job="api"} = WHERE), agrupar e somar (sum by (route) = GROUP BY), dividir dois agregados. O que só o Prometheus faz: ele foi buscar ele mesmo os dados a cada 15 segundos em oito serviços, ele transforma qualquer contador em velocidade com uma função (rate), ele estima um quantil a partir de faixas (histogram_quantile), e ele expõe seus próprios alertas como uma métrica (ALERTS).

Grafana, no Explore

Afficher les 8 requêtes LogQL (G1 à G8)

Abra http://localhost:3000 (usuário admin, senha aiopsatlas2026). No menu principal (ícone no topo à esquerda), clique em Explore. No topo da página, o seletor de fonte de dados oferece Prometheus, Loki e Alertmanager: escolha Loki. À direita do campo de query, dois modos: Builder (menus) e Code (você digita). Mude para Code, cole a query, depois Run query (ou Shift+Enter). Os logs aparecem embaixo, a linha mais recente primeiro. No topo à direita, o seletor de período está em Last 1 hour por padrão: mantenha assim. Essas etapas são idênticas no Windows e no Linux, é o navegador que trabalha.

A imagem a manter em mente: o Loki é um armário de registros de bordo, uma pasta por combinação de labels. Ele não lê o conteúdo das linhas para organizá-las, apenas a etiqueta da pasta. Uma query LogQL sempre começa escolhendo uma pasta, entre chaves, depois eventualmente filtrando as linhas dentro dela.

G1. Todo o log da API

logql
{service="api"}
text
2026-09-15 19:33:30.416  {"horodatage": "2026-09-15T19:33:30.416+00:00", "niveau": "INFO", "id_requete": "875939d9cdac", "methode": "GET", "route": "/cours", "code": 200, "duree_ms": 11.5, "message": "GET /cours -> 200"}
2026-09-15 19:33:30.186  {"horodatage": "2026-09-15T19:33:30.186+00:00", "niveau": "INFO", "id_requete": "6d6e21e6d17b", "methode": "GET", "route": "/cours/{id}", "code": 200, "duree_ms": 14.3, "message": "GET /cours/C0001 -> 200"}
2026-09-15 19:33:30.142  {"horodatage": "2026-09-15T19:33:30.142+00:00", "niveau": "INFO", "id_requete": "42a64fb435bf", "methode": "GET", "route": "/cours", "code": 200, "duree_ms": 32.3, "message": "GET /cours -> 200"}

O que a query pede: "Todas as linhas de log cujo label service vale api."

Zero novidade em relação a P2: um seletor entre chaves. A diferença é o resultado: linhas de texto, não números. O Grafana exibe a hora (convertida no seu fuso) e depois a linha bruta; clique em uma linha para expandir seus labels: service="api", conteneur="labo-api", niveau="INFO", code="200", detected_level="info". Cerca de nove linhas por segundo, tanto quanto P8 anunciava: uma requisição, uma linha. As chaves são obrigatórias: service="api" sozinho é recusado (parse error at line 1, col 1: syntax error: unexpected IDENTIFIER), e {service="api" sem fechamento também (syntax error: unexpected $end, expecting } or ,).

Para entender bem: pourquoi `{service="API"}` ne rend rien

Um label é uma string exata. {service="API"} em maiúsculas não retorna nenhuma linha, sem erro: a pasta não existe. O mesmo para {app="api"}: o label se chama service neste labo, não app. Quando uma query LogQL retorna zero linhas, verifique primeiro o nome e a caixa do label; no Explore, o modo Builder lista os labels e seus valores existentes, é o jeito mais seguro de descobri-los.

G2. Somente os erros

logql
{service="api", niveau="ERROR"}
text
2026-09-15 19:33:28.931  {"horodatage": "2026-09-15T19:33:28.931+00:00", "niveau": "ERROR", "id_requete": "f5e09e6ed543", "methode": "POST", "route": "/inscriptions", "code": 500, "duree_ms": 0.0, "message": "POST /inscriptions -> 500"}
2026-09-15 19:33:16.381  {"horodatage": "2026-09-15T19:33:16.381+00:00", "niveau": "ERROR", "id_requete": "18f95992a8fa", "methode": "POST", "route": "/inscriptions", "code": 500, "duree_ms": 0.0, "message": "POST /inscriptions -> 500"}
2026-09-15 19:33:14.781  {"horodatage": "2026-09-15T19:33:14.781+00:00", "niveau": "ERROR", "id_requete": "dc1c2ff189a6", "methode": "GET", "route": "/cours/{id}", "code": 500, "duree_ms": 0.1, "message": "GET /cours/C0019 -> 500"}

O que a query pede: "As linhas da API cujo label niveau vale ERROR."

Uma única novidade: dois labels no seletor, separados por vírgula, é um E. O label niveau não estava na linha originalmente: foi o Alloy que o extraiu do campo JSON niveau antes de enviar ao Loki, e é isso que torna essa query rápida. O fio condutor está aqui, terceira linha: id_requete: dc1c2ff189a6, a linha do conjunto de dados. Muito menos linhas do que em G1: cerca de uma a cada doze segundos, como P7 dizia para os 500.

G3. Por código HTTP

logql
{service="api", code="500"}
text
2026-09-15 19:33:28.931  {"horodatage": "2026-09-15T19:33:28.931+00:00", "niveau": "ERROR", "id_requete": "f5e09e6ed543", "methode": "POST", "route": "/inscriptions", "code": 500, …}
2026-09-15 19:33:16.381  {"horodatage": "2026-09-15T19:33:16.381+00:00", "niveau": "ERROR", "id_requete": "18f95992a8fa", "methode": "POST", "route": "/inscriptions", "code": 500, …}
2026-09-15 19:33:14.781  {"horodatage": "2026-09-15T19:33:14.781+00:00", "niveau": "ERROR", "id_requete": "dc1c2ff189a6", "methode": "GET", "route": "/cours/{id}", "code": 500, …}

O que a query pede: "As linhas da API cujo label code vale 500."

Nada de novo: G2 com outro label. As mesmas linhas de G2, porque nessa API todo 500 é um ERROR e vice-versa. É a versão em log de P4: onde o Prometheus diz "32 erros em /cours", o Loki mostra quais, com a URL real (/cours/C0019) e o identificador da requisição. Observe que code aqui é uma string ("500") porque é um label; no JSON da linha, é um número (500). G6 vai mostrar a diferença.

G4. Filtrar pelo texto

logql
{service="api"} |= "inscriptions"
text
2026-09-15 19:33:29.738  {"horodatage": "2026-09-15T19:33:29.738+00:00", "niveau": "INFO", "id_requete": "5afb73494ebd", "methode": "POST", "route": "/inscriptions", "code": 201, "duree_ms": 22.9, "message": "POST /inscriptions -> 201"}
2026-09-15 19:33:28.931  {"horodatage": "2026-09-15T19:33:28.931+00:00", "niveau": "ERROR", "id_requete": "f5e09e6ed543", "methode": "POST", "route": "/inscriptions", "code": 500, "duree_ms": 0.0, "message": "POST /inscriptions -> 500"}
2026-09-15 19:33:27.187  {"horodatage": "2026-09-15T19:33:27.187+00:00", "niveau": "INFO", "id_requete": "c4e29db13e12", "methode": "POST", "route": "/inscriptions", "code": 201, "duree_ms": 26.1, "message": "POST /inscriptions -> 201"}

O que a query pede: "As linhas da API que contêm o texto inscriptions."

Uma única novidade: o filtro de linha |= "…", que mantém as linhas que contêm exatamente esse texto. É o grep. Ao contrário de um label, o Loki precisa aqui abrir cada linha da pasta {service="api"} para olhar dentro: mais lento, mas você pode buscar qualquer coisa. As variantes: != (não contém), |~ (expressão regular), !~. Resultado misto: 201 e 500, tudo que toca em inscrições.

Bilan de G1 a G4: duas formas de filtrar, por label (rápido, antes de abrir as linhas) e por texto (flexível, depois). A boa query sempre começa pelo label mais restrito possível.

G5. Abrir o JSON

logql
{service="api"} | json
text
2026-09-15 19:33:30.416  {"horodatage": "2026-09-15T19:33:30.416+00:00", "niveau": "INFO", "id_requete": "875939d9cdac", "methode": "GET", "route": "/cours", "code": 200, "duree_ms": 11.5, "message": "GET /cours -> 200"}
   labels : code="200" conteneur="labo-api" duree_ms="11.5" horodatage="2026-09-15T19:33:30.416+00:00" id_requete="875939d9cdac"
            message="GET /cours -> 200" methode="GET" niveau="INFO" route="/cours" service="api" …

O que a query pede: "As linhas da API, e para cada uma, transforme os campos do JSON em labels."

Uma única novidade: o parser | json. As linhas exibidas são as mesmas de G1, mas expanda uma: ela agora tem muito mais labels (route, methode, duree_ms, id_requete, message…), um por campo do JSON. Esses labels são calculados no momento da query, não armazenados: o Loki continua indexando apenas service, conteneur, niveau, code. Você também verá code_extracted e niveau_extracted: quando um campo do JSON tem o mesmo nome de um label já definido pelo Alloy, o Loki adiciona um sufixo à cópia em vez de sobrescrever.

G6. Filtrar por um campo

logql
{service="api"} | json | duree_ms > 500
text
2026-09-15 19:33:27.838  {"horodatage": "2026-09-15T19:33:27.838+00:00", "niveau": "INFO", "id_requete": "416f1ab0eb41", "methode": "GET", "route": "/lent", "code": 200, "duree_ms": 587.4, "message": "GET /lent -> 200"}
2026-09-15 19:33:16.335  {"horodatage": "2026-09-15T19:33:16.335+00:00", "niveau": "INFO", "id_requete": "9d1cb1767846", "methode": "GET", "route": "/lent", "code": 200, "duree_ms": 737.8, "message": "GET /lent -> 200"}
2026-09-15 19:33:15.375  {"horodatage": "2026-09-15T19:33:15.375+00:00", "niveau": "INFO", "id_requete": "cab8f698f89b", "methode": "GET", "route": "/lent", "code": 200, "duree_ms": 526.2, "message": "GET /lent -> 200"}

O que a query pede: "As linhas da API cujo campo duree_ms, uma vez o JSON aberto, ultrapassa 500."

Uma única novidade: o filtro de label | duree_ms > 500, que compara um label extraído a um número. Isso só é possível depois de | json, senão duree_ms não existe. Resultado: apenas /lent, a rota propositalmente lenta (300 a 900 ms). É a versão em log de P10: o Prometheus diz "o p95 está em 98 ms"; o Loki mostra as requisições individuais que ultrapassaram um limite, com seu identificador. Equivalente SQL: WHERE duree_ms > 500, com a diferença de que a coluna não existia antes da query.

G7. Encontrar uma requisição precisa

logql
{service="api"} |= "dc1c2ff189a6"
text
2026-09-15 19:33:14.781  {"horodatage": "2026-09-15T19:33:14.781+00:00", "niveau": "ERROR", "id_requete": "dc1c2ff189a6", "methode": "GET", "route": "/cours/{id}", "code": 500, "duree_ms": 0.1, "message": "GET /cours/C0019 -> 500"}

Uma única linha.

O que a query pede: "A linha da API que contém o identificador dc1c2ff189a6."

Nada de novo: é G4 com outro texto. O que muda é o uso: na sua máquina, dc1c2ff189a6 não existe; copie um id_requete visto na sua própria saída de G2 e cole no lugar. É o gesto que você fará em produção: um usuário te dá o identificador devolvido pelo cabeçalho x-id-requete da sua resposta com erro, e você encontra em uma única query a linha exata, com a rota, o código e a duração. Uma única linha: o identificador é único por requisição.

G8. Contar as linhas: uma métrica extraída dos logs

logql
sum by (niveau) (count_over_time({service="api"}[1m]))

Desta vez, o Grafana exibe um gráfico em vez de linhas: três curvas, {niveau="INFO"} em torno de 470 a 500 linhas por minuto, {niveau="WARNING"} em torno de 40 a 50, {niveau="ERROR"} entre 2 e 10, no labo do curso. Passe o mouse sobre o gráfico para ler os valores.

O que a query pede: "Conte as linhas da API por intervalo de um minuto, depois some mantendo o label niveau."

Uma única novidade, em uma parte que você já conhece: count_over_time(…[1m]) conta as linhas de um seletor em uma faixa, exatamente como rate(…[1m]) calcula uma inclinação em P5. Ao redor, sum by (niveau) é o sum by (route) de P6, palavra por palavra. O LogQL emprestou essa gramática do PromQL de propósito: o que você aprendeu de um lado serve do outro. Compare com P7: o Prometheus conta 0,09 resposta 500 por segundo, ou seja, 5 por minuto; o Loki conta 5 linhas ERROR por minuto. Duas ferramentas, dois caminhos, o mesmo número.

A diferença essencial entre Prometheus e Loki. O Prometheus armazena números já contados pela API (http_requetes_total), o Loki armazena as linhas e pode recontá-las sob demanda (count_over_time). O primeiro é leve e rápido, e responde "quantos"; o segundo é pesado, mas mantém o detalhe, e responde "quais". O labo tem os dois porque nenhum substitui o outro.

A mensagem para levar. O que o grep também faz: buscar um texto em linhas (|=). O que só o Loki faz: organizar as linhas de dez contêineres por labels e ler apenas a pasta certa, abrir o JSON sob demanda para filtrar por um campo numérico (| json | duree_ms > 500), e transformar logs em curva com a gramática do PromQL (count_over_time).

Desafio bônus (opcional)

Três queries que combinam o que você viu, sem nenhuma noção nova. Digite-as, depois explique em uma frase o que cada uma mostra.

promql
topk(5, increase(inscriptions_total[1h]))

Os cinco cursos que receberam mais inscrições na última hora. increase é rate multiplicado pela duração da janela; topk(5, …) mantém as cinco maiores séries. No labo do curso, C0001 fica em primeiro com cerca de 177 inscrições: o charge favorece alguns cursos "populares".

promql
histogram_quantile(0.95, sum by (le, route) (rate(http_duree_requete_seconds_bucket[5m])))

P10 com um label a mais no by: um p95 por rota. Você verá /lent em torno de 0,7 s e as outras rotas abaixo de 0,03 s. Veja o que muda em relação ao p95 global de P10.

logql
{service="api"} |= "inscriptions" | json | code = 201

G4, G5 e G6 encadeados: apenas as inscrições bem-sucedidas. Verifique se o número de linhas por minuto corresponde à linha {code="201"} de P7, cerca de 0,87 por segundo, ou seja, umas cinquenta por minuto.

Anexo A — Passo a passo detalhado no Windows (PowerShell)

Afficher le pas à pas Windows

Todos os comandos são digitados no PowerShell, a partir da pasta lab3. O Docker Desktop precisa estar em execução (ícone verde). Se o PowerShell recusar executar .\labo.ps1, digite uma vez Set-ExecutionPolicy -Scope CurrentUser RemoteSigned e responda O.

A.0 — Clonar o kit

powershell
cd C:\Users\<toi>\Documents
git clone https://github.com/hrhouma2/aiopsatlas-observabilite-labo-fr.git lab3
cd lab3
ls

Você deve ver docker-compose.yml, labo.ps1, labo.sh, README.md, e as pastas alertmanager, alloy, api, charge, grafana, loki, modules, outils, prometheus, webhook. Se você já clonou o kit na lição 03, pule esta etapa e apenas faça cd lab3.

A.1 — Verificar os pré-requisitos

powershell
.\labo.ps1 prerequis
text

== Prérequis ==
  ✔ docker : Docker version 29.3.1, build c2be9cc
  ✔ le démon Docker répond
  ✔ docker compose : 5.1.1
  ✔ mémoire disponible pour Docker : 31 Go
  ✔ processeurs : 20
  ✔ port 9090 libre
  ✔ port 9093 libre
  ✔ port 3000 libre
  ✔ port 3100 libre
  ✔ port 12345 libre
  ✔ port 9100 libre
  ✔ port 8080 libre
  ✔ port 8000 libre
  ✔ port 8090 libre

Tout est prêt. Lancez : .\labo.ps1 demarrer

Ponto de verificação: a última linha é Tout est prêt.. As versões, a memória e o número de processadores são os da máquina do curso. Se uma porta estiver marcada ✘ … déjà occupé, a lição 03 explica o que fazer; para a 3000, $env:GRAFANA_PORT = '3001' basta.

A.2 — Iniciar

powershell
.\labo.ps1 demarrer

Na primeira vez, o download das seis imagens públicas leva de um a cinco minutos dependendo da sua conexão. Fim da saída esperada:

text
== Attente que chaque service soit prêt ==
  prometheus       prêt (0 s)
  alertmanager     prêt (0 s)
  loki             prêt (0 s)
  alloy           .. prêt (6 s)
  node-exporter    prêt (0 s)
  cadvisor         prêt (0 s)
  api              prêt (0 s)
  webhook          prêt (0 s)
  charge           prêt (0 s)
  grafana         ... prêt (9 s)

Le labo est prêt.
  Grafana        http://localhost:3000   (utilisateur admin · mot de passe aiopsatlas2026)
  Prometheus     http://localhost:9090   (Status → Target health, puis onglet Graph)
  Alertmanager   http://localhost:9093
  API catalogue  http://localhost:8000/cours   ·   http://localhost:8000/metrics
  Webhook        http://localhost:8090   (les alertes reçues)
  Loki           http://localhost:3100/ready   ·   Alloy   http://localhost:12345
  node-exporter  http://localhost:9100/metrics   ·   cAdvisor   http://localhost:8080

Étape suivante : .\labo.ps1 etat   (laissez tourner 2 minutes pour avoir des courbes)

Ponto de verificação: dez prêt, depois Le labo est prêt.. Na máquina do curso, com as imagens já em cache, o comando levou 44 segundos. A lição 04 comenta essa saída linha por linha.

A.3 — Ler etat

powershell
.\labo.ps1 etat
text

== Conteneurs ==
NAME                 SERVICE         STATUS
labo-alertmanager    alertmanager    Up About a minute (healthy)
labo-alloy           alloy           Up 48 seconds (healthy)
labo-api             api             Up About a minute (healthy)
labo-cadvisor        cadvisor        Up About a minute (healthy)
labo-charge          charge          Up About a minute (healthy)
labo-grafana         grafana         Up 48 seconds (healthy)
labo-loki            loki            Up About a minute (healthy)
labo-node-exporter   node-exporter   Up About a minute (healthy)
labo-prometheus      prometheus      Up About a minute (healthy)
labo-webhook         webhook         Up About a minute (healthy)

== Supervision ==
  ✔ Prometheus répond — cibles up : 8/8
     séries en mémoire : 9038
     alertes : 0 active(s), 0 en attente (pending)
  ✔ Alertmanager répond (http://localhost:9093)
  ✔ Grafana répond (http://localhost:3000)
  ✔ Loki répond (http://localhost:3100)
  ✔ API catalogue répond — version 1.0.0, 64 cours
  ✔ Webhook répond — 0 alerte(s) reçue(s) (http://localhost:8090)

Labo : 10/10 services, 8/8 cibles up, 0 alertes actives.

Ponto de verificação: dez (healthy), 8/8, 64 cours, 0 alerte(s) reçue(s), e a última linha Labo : 10/10 services, 8/8 cibles up, 0 alertes actives.. O número de séries em memória sobe nos primeiros minutos (9038 logo após a inicialização, 12.000 a 15.000 depois de uma hora na máquina do curso). Se alloy ou grafana ainda estiverem (health: starting), espere trinta segundos e execute novamente.

A.4 — Os oito targets no Prometheus

Abra http://localhost:9090, menu Status, depois Target health. Oito blocos, um por job, cada um com 1 / 1 up e uma linha:

text
api
1 / 1 up
Endpoint                    Labels                                        Last scrape   State
http://api:8000/metrics     instance="api:8000" job="api" service="api"   6.014s ago    UP

Ponto de verificação: oito UP, nenhum DOWN. A coluna Last scrape nunca ultrapassa 15 segundos: é o scrape_interval de prometheus.yml. Na linha de comando, a mesma informação:

powershell
(Invoke-RestMethod http://localhost:9090/api/v1/targets).data.activeTargets | Select-Object @{n='job';e={$_.labels.job}}, health, scrapeUrl | Sort-Object job
text
job           health scrapeUrl
---           ------ ---------
alertmanager  up     http://alertmanager:9093/metrics
alloy         up     http://alloy:12345/metrics
api           up     http://api:8000/metrics
cadvisor      up     http://cadvisor:8080/metrics
grafana       up     http://grafana:3000/metrics
loki          up     http://loki:3100/metrics
node-exporter up     http://node-exporter:9100/metrics
prometheus    up     http://localhost:9090/metrics

A.5 — As queries P1 a P12 e G1 a G8

Espere que demarrer tenha pelo menos dois minutos, depois siga as seções Prometheus, na aba Graph e Grafana, no Explore mais acima. As queries estão prontas para copiar:

powershell
Get-Content modules\01-le-labo\requetes.txt
Get-Content modules\01-le-labo\requetes-logql.txt

Ponto de verificação: P1 retorna 8 séries em 1, P12 retorna Empty query result, G1 retorna linhas JSON, G8 retorna três curvas.

A.6 — Quebrar: parar a API

Antes de quebrar, anote a hora (Get-Date -Format HH:mm:ss). Depois:

powershell
.\labo.ps1 casser api
text

== Panne : arrêt de l'API ==
 Container labo-api Stopping
 Container labo-api Stopped
  ✔ API arrêtée. La charge continue de frapper dans le vide.
  À observer : .\labo.ps1 etat  ·  http://localhost:9090/targets (api → down)
               http://localhost:9090/alerts (APIInjoignable : pending puis firing après 30 s)
               http://localhost:9093 et http://localhost:8090 (l'alerte arrive ~10 s après firing)

Pour tout remettre en ordre : .\labo.ps1 reparer

O script fez um docker compose stop api: o contêiner é parado corretamente, seus dados e sua imagem ficam intactos. Agora, observe a falha por cinco caminhos, na ordem. Você tem cerca de 70 segundos antes que o alerta chegue ao webhook: execute etat imediatamente.

Caminho 1, etat:

powershell
.\labo.ps1 etat
text

== Conteneurs ==
NAME                 SERVICE         STATUS
labo-alertmanager    alertmanager    Up 22 minutes (healthy)
labo-alloy           alloy           Up 22 minutes (healthy)
labo-api             api             Exited (0) About a minute ago
labo-cadvisor        cadvisor        Up 22 minutes (healthy)
labo-charge          charge          Up 22 minutes (healthy)
labo-grafana         grafana         Up 22 minutes (healthy)
labo-loki            loki            Up 22 minutes (healthy)
labo-node-exporter   node-exporter   Up 22 minutes (healthy)
labo-prometheus      prometheus      Up 22 minutes (healthy)
labo-webhook         webhook         Up 22 minutes (healthy)

== Supervision ==
  ✔ Prometheus répond — cibles up : 7/8
  ✘ cible api (http://api:8000/metrics) : down — Get "http://api:8000/metrics": dial tcp: lookup api on 127.0.0.11:53: no such host
     séries en mémoire : 15116
     alertes : 2 active(s), 0 en attente (pending)
  ✘ APIInjoignable [critique] — L'API catalogue ne répond plus
  ✘ TauxErreursEleve [critique] — Plus de 5 % des requêtes de l'API échouent
  ✔ Alertmanager répond (http://localhost:9093)
  ✔ Grafana répond (http://localhost:3000)
  ✔ Loki répond (http://localhost:3100)
  ✘ API catalogue ne répond pas (http://localhost:8000)
  ✔ Webhook répond — 2 alerte(s) reçue(s) (http://localhost:8090)

Labo : 9/10 services, 7/8 cibles up, 2 alertes actives.

O que é preciso ler, de cima para baixo: labo-api está Exited (0) (código 0: parada voluntária, não uma falha grave); o Prometheus só lê 7/8 targets e te diz por quê (lookup api … no such host: o nome api não existe mais na rede Docker já que o contêiner está parado); o alerta APIInjoignable está ativo; a API não responde na porta 8000; o webhook recebeu algo. Essa saída foi capturada na máquina do curso um minuto depois de casser api, quando um casser erreurs tinha acabado de ser executado alguns minutos antes: é por isso que um segundo alerta, TauxErreursEleve, também aparece. Na sua máquina, você terá apenas APIInjoignable, 1 alertes actives e 1 alerte(s) reçue(s). Se você executar etat nos primeiros 30 segundos, o alerta ainda está en attente (pending) e o webhook ainda está em 0: execute novamente um minuto depois.

Caminho 2, os targets. Recarregue http://localhost:9090StatusTarget health. O bloco api passou para 0 / 1 up, estado DOWN, e a coluna Error traz a mesma mensagem de etat: Get "http://api:8000/metrics": dial tcp: lookup api on 127.0.0.11:53: no such host. Os outros sete permanecem UP. Digite up novamente em Query: a linha up{instance="api:8000", job="api", service="api"} está em 0, as outras sete em 1. Depois up == 0: uma única linha.

Caminho 3, os alertas no Prometheus. Menu Alerts. A regra APIInjoignable muda de estado em três tempos, cronometrados na máquina do curso:

text
t+0 s   : APIInjoignable inactive      (Prometheus n'a pas encore rescrappé l'API)
t+40 s  : APIInjoignable pending       (up{job="api"} == 0 est vrai, le compte à rebours « for: 30s » tourne)
t+70 s  : APIInjoignable firing        (vrai depuis 30 s : Prometheus envoie à Alertmanager)
t+70 s  : APIInjoignable reçue par le webhook (firing)

Por que 40 segundos antes de pending: o Prometheus lê a API a cada 15 segundos, então leva até 15 segundos para um scrape falhar, depois ele avalia as regras a cada 15 segundos. Por que mais 30 antes de firing: a regra diz for: 30s. Em outra captura, pending chegou em 31 s e firing em 61 s: a ordem de grandeza é a mesma, o detalhe depende do momento em que você quebrou em relação ao ciclo de scrape. Digite ALERTS novamente em Query:

text
ALERTS{alertname="APIInjoignable", alertstate="pending", instance="api:8000", job="api", service="api", severite="critique"}    1

depois, trinta segundos depois, alertstate="firing". Anote a hora da mudança para firing: é a primeira das três linhas do seu entregável.

Caminho 4, Alertmanager. Abra http://localhost:9093. A página Alerts mostra um grupo alertname="APIInjoignable" service="api" (é o group_by: [alertname, service] de alertmanager.yml) com o alerta, seus labels (instance="api:8000", job="api", labo="observabilite", severite="critique"), seu resumo L'API catalogue ne répond plus e sua descrição. O label labo="observabilite" não estava na regra: é o external_labels de prometheus.yml, adicionado a tudo que sai do Prometheus. Na linha de comando:

powershell
(Invoke-RestMethod http://localhost:9093/api/v2/alerts) | Select-Object @{n='alerte';e={$_.labels.alertname}}, @{n='etat';e={$_.status.state}}, startsAt
text
alerte          etat   startsAt
------          ----   --------
APIInjoignable  active 2026-09-15T19:41:11.496Z

Caminho 5, o webhook. Abra http://localhost:8090. A página "Alertes reçues d'Alertmanager" não está mais vazia: uma linha APIInjoignable · critique · firing · api · L'API catalogue ne répond plus, e o cabeçalho conta 1 alerte(s) en mémoire · 1 notification(s) reçue(s). O formato bruto, http://localhost:8090/alertes.json, mostra o que o Alertmanager enviou:

json
{"recu_a":"2026-09-15T19:41:26+00:00","etat":"firing","nom":"APIInjoignable","severite":"critique","service":"api","resume":"L'API catalogue ne répond plus","description":"Prometheus n'arrive plus à lire http://api:8000/metrics depuis 30 secondes (cible api:8000).","debut":"2026-09-15T19:41:11.496Z","fin":"0001-01-01T00:00:00Z","labels":{"alertname":"APIInjoignable","instance":"api:8000","job":"api","labo":"observabilite","service":"api","severite":"critique"}}

Leia debut (19:41:11, a hora do firing no Prometheus) e recu_a (19:41:26): quinze segundos de diferença, dos quais os 10 segundos de group_wait do Alertmanager. fin no ano 0001 quer dizer "ainda não terminou". Anote recu_a: segunda linha do seu entregável.

O que a charge vê, nesse meio tempo:

powershell
.\labo.ps1 journal charge
text
labo-charge  | {"horodatage": "2026-09-15T19:40:36.600+00:00", "niveau": "WARNING", "message": "API injoignable : ConnectionError"}
labo-charge  | {"horodatage": "2026-09-15T19:40:40.925+00:00", "niveau": "WARNING", "message": "API injoignable : ConnectionError"}
labo-charge  | {"horodatage": "2026-09-15T19:40:45.393+00:00", "niveau": "WARNING", "message": "API injoignable : ConnectionError"}

E o que você vê se você mesmo chamar a API:

powershell
Invoke-RestMethod http://localhost:8000/sante -TimeoutSec 5
text
Invoke-RestMethod : Le délai de l'opération a expiré.

(Sem -TimeoutSec, o PowerShell espera mais tempo antes de desistir; curl.exe -sS http://localhost:8000/sante responde mais rápido: curl: (7) Failed to connect to localhost:8000 after 2237 ms: Could not connect to server.)

A diferença essencial entre um 404 e nenhuma resposta. Na etapa A.8, /cours/C9999 vai responder 404: a API está rodando e te diz educadamente que esse curso não existe; isso é contado em http_requetes_total{code="404"}, é escrito em um log WARNING, e up continua em 1. Aqui, Le délai de l'opération a expiré: ninguém responde, não há nem código nem log do lado da API, e é up que cai para 0. Duas situações, dois sinais, dois lugares onde procurar.

A.7 — Reparar

powershell
.\labo.ps1 reparer
text

== Réparation ==
  ✔ API redémarrée
  api             .. prêt (6 s)
  ✔ taux d'erreurs remis à 0.01, lenteur à 0 ms

Les alertes passent en « resolved » dans les minutes qui suivent (voir http://localhost:8090).

O script fez docker compose start api, esperou que /sante respondesse, depois chamou /admin/reparer (útil para os outros dois cenários de falha). Verifique que a API está trabalhando:

powershell
.\labo.ps1 journal api
text
labo-api  | {"horodatage": "2026-09-15T19:41:48.720+00:00", "niveau": "INFO", "id_requete": "6a4228a46a56", "methode": "POST", "route": "/inscriptions", "code": 201, "duree_ms": 15.6, "message": "POST /inscriptions -> 201"}
labo-api  | {"horodatage": "2026-09-15T19:41:48.914+00:00", "niveau": "WARNING", "id_requete": "b4ebe9fa32d8", "methode": "GET", "route": "inconnue", "code": 404, "duree_ms": 0.4, "message": "GET /inexistant -> 404"}
labo-api  | {"horodatage": "2026-09-15T19:41:48.937+00:00", "niveau": "INFO", "id_requete": "732adf61ee17", "methode": "GET", "route": "/cours/{id}", "code": 200, "duree_ms": 25.7, "message": "GET /cours/C0043 -> 200"}

Depois observe o alerta se apagar, na mesma ordem em que se acendeu. Cronometrado na máquina do curso depois de reparer:

text
t+15 s  : APIInjoignable firing   (Prometheus)   · webhook : firing
t+40 s  : APIInjoignable inactive (Prometheus)   · webhook : firing
t+55 s  : APIInjoignable inactive (Prometheus)   · webhook : resolved

No primeiro scrape bem-sucedido, up{job="api"} volta a 1 e a regra volta a inactive; o Alertmanager então envia uma notificação resolved ao webhook. Recarregue http://localhost:8090: duas linhas agora para APIInjoignable, uma firing e uma resolved, e em /alertes.json a segunda tem um campo fin preenchido:

json
{"recu_a":"2026-09-15T19:42:26+00:00","etat":"resolved","nom":"APIInjoignable",…,"debut":"2026-09-15T19:41:11.496Z","fin":"2026-09-15T19:41:56.496Z",}

fin menos debut: a falha durou 45 segundos aos olhos do Prometheus. Anote recu_a da linha resolved: terceira linha do seu entregável.

A.8 — Provocar um 404, para comparar

A API está rodando. Peça a ela um curso que não existe:

powershell
Invoke-RestMethod http://localhost:8000/cours/C9999
text
Invoke-RestMethod : {"detail":"cours C9999 introuvable"}

Isso é um erro HTTP 404: a API respondeu. Para ver o código em si:

powershell
try { Invoke-WebRequest http://localhost:8000/cours/C9999 -UseBasicParsing } catch { $_.Exception.Response.StatusCode.value__ }
text
404

Digite P4 novamente no Prometheus substituindo 500 por 404: a série route="/cours/{id}" aumentou em 1. Digite G1 novamente adicionando |= "C9999": sua requisição está ali, nível WARNING, com seu id_requete. Nada disso existe para a falha de A.6: uma API parada não conta nada e não escreve nada.

A.9 — Verificação final

powershell
.\labo.ps1 etat
text

== Conteneurs ==
NAME                 SERVICE         STATUS
labo-alertmanager    alertmanager    Up 23 minutes (healthy)
labo-alloy           alloy           Up 23 minutes (healthy)
labo-api             api             Up 53 seconds (healthy)
labo-cadvisor        cadvisor        Up 23 minutes (healthy)
labo-charge          charge          Up 23 minutes (healthy)
labo-grafana         grafana         Up 23 minutes (healthy)
labo-loki            loki            Up 23 minutes (healthy)
labo-node-exporter   node-exporter   Up 23 minutes (healthy)
labo-prometheus      prometheus      Up 23 minutes (healthy)
labo-webhook         webhook         Up 23 minutes (healthy)

== Supervision ==
  ✔ Prometheus répond — cibles up : 8/8
     séries en mémoire : 15395
     alertes : 0 active(s), 0 en attente (pending)
  ✔ Alertmanager répond (http://localhost:9093)
  ✔ Grafana répond (http://localhost:3000)
  ✔ Loki répond (http://localhost:3100)
  ✔ API catalogue répond — version 1.0.0, 64 cours
  ✔ Webhook répond — 2 alerte(s) reçue(s) (http://localhost:8090)

Labo : 10/10 services, 8/8 cibles up, 0 alertes actives.

O que você deve ter: labo-api novamente Up … (healthy) com um tempo mais curto que os outros (ele acabou de reiniciar), 8/8, 0 active(s), 64 cours, 2 alerte(s) reçue(s) (a firing e a resolved), e a última linha Labo : 10/10 services, 8/8 cibles up, 0 alertes actives.. Na máquina do curso, essa saída ainda trazia 1 alertes actives e 3 alerte(s) reçue(s) por causa do TauxErreursEleve residual mencionado em A.6; na sua máquina, com apenas a falha api, os valores são os de cima. Essa saída e suas três horas anotadas são seu entregável. Você pode deixar o labo rodando para as oficinas 06 e 07, ou pará-lo com .\labo.ps1 arreter: os dados são conservados e demarrer retoma de onde você estava.

Anexo B — Passo a passo detalhado no Linux, macOS, WSL 2 e Git Bash

Afficher le pas à pas Linux, macOS, WSL 2 et Git Bash

Todos os comandos são digitados em um terminal bash, a partir da pasta lab3. No macOS e no Windows (WSL 2 ou Git Bash), o Docker Desktop precisa estar em execução; no Linux nativo, docker info deve responder sem sudo (senão sudo usermod -aG docker $USER, depois abra uma nova sessão). O script bash precisa de curl.

B.0 — Clonar o kit

bash
cd ~
git clone https://github.com/hrhouma2/aiopsatlas-observabilite-labo-fr.git lab3
cd lab3
ls

Você deve ver docker-compose.yml, labo.sh, labo.ps1, README.md, e as pastas alertmanager, alloy, api, charge, grafana, loki, modules, outils, prometheus, webhook. No Linux e no macOS, torne o script executável uma vez: chmod +x labo.sh (sem isso, ./labo.sh responde bash: ./labo.sh: Permission denied). Se você já clonou o kit na lição 03, pule esta etapa e apenas faça cd lab3.

B.1 — Verificar os pré-requisitos

bash
./labo.sh prerequis
text

== Prérequis ==
  ✔ docker : Docker version 29.3.1, build c2be9cc
  ✔ le démon Docker répond
  ✔ docker compose : 5.1.1
  ✔ curl : présent
  ✔ mémoire disponible pour Docker : 31 Go
  ✔ processeurs : 20
  ✔ port 9090 libre
  ✔ port 9093 libre
  ✔ port 3000 libre
  ✔ port 3100 libre
  ✔ port 12345 libre
  ✔ port 9100 libre
  ✔ port 8080 libre
  ✔ port 8000 libre
  ✔ port 8090 libre

Tout est prêt. Lancez : ./labo.sh demarrer

Ponto de verificação: a última linha é Tout est prêt.. O script bash verifica uma linha a mais que o PowerShell, curl : présent. As versões e a memória são as da máquina do curso (Git Bash no Windows). Se uma porta estiver ocupada, a lição 03 explica o que fazer; para a 3000, GRAFANA_PORT=3001 ./labo.sh demarrer.

B.2 — Iniciar

bash
./labo.sh demarrer

Fim da saída esperada, uma vez as imagens baixadas:

text
== Attente que chaque service soit prêt ==
  prometheus       prêt (0 s)
  alertmanager     prêt (0 s)
  loki             prêt (0 s)
  alloy           .. prêt (6 s)
  node-exporter    prêt (0 s)
  cadvisor         prêt (0 s)
  api              prêt (0 s)
  webhook          prêt (0 s)
  charge           prêt (0 s)
  grafana         ... prêt (9 s)

Le labo est prêt.
  Grafana        http://localhost:3000   (utilisateur admin · mot de passe aiopsatlas2026)
  Prometheus     http://localhost:9090   (Status → Target health, puis onglet Graph)
  Alertmanager   http://localhost:9093
  API catalogue  http://localhost:8000/cours   ·   http://localhost:8000/metrics
  Webhook        http://localhost:8090   (les alertes reçues)
  Loki           http://localhost:3100/ready   ·   Alloy   http://localhost:12345
  node-exporter  http://localhost:9100/metrics   ·   cAdvisor   http://localhost:8080

Étape suivante : ./labo.sh etat   (laissez tourner 2 minutes pour avoir des courbes)

Ponto de verificação: dez prêt, depois Le labo est prêt..

B.3 — Ler etat

bash
./labo.sh etat
text

== Conteneurs ==
NAME                 SERVICE         STATUS
labo-alertmanager    alertmanager    Up 11 minutes (healthy)
labo-alloy           alloy           Up 10 minutes (healthy)
labo-api             api             Up 4 minutes (healthy)
labo-cadvisor        cadvisor        Up 11 minutes (healthy)
labo-charge          charge          Up 10 minutes (healthy)
labo-grafana         grafana         Up 10 minutes (healthy)
labo-loki            loki            Up 11 minutes (healthy)
labo-node-exporter   node-exporter   Up 11 minutes (healthy)
labo-prometheus      prometheus      Up 11 minutes (healthy)
labo-webhook         webhook         Up 11 minutes (healthy)

== Supervision ==
  ✔ Prometheus répond — cibles up : 8/8
     séries en mémoire : 10602
     alertes : 0 active(s), 0 en attente (pending)
  ✔ Alertmanager répond (http://localhost:9093)
  ✔ Grafana répond (http://localhost:3000)
  ✔ Loki répond (http://localhost:3100)
  ✔ API catalogue répond — version 1.0.0, 64 cours
  ✔ Webhook répond — 0 alerte(s) reçue(s) (http://localhost:8090)

Labo : 10/10 services, 8/8 cibles up, 0 alertes actives.

Ponto de verificação: dez (healthy), 8/8, 64 cours, e a última linha Labo : 10/10 services, 8/8 cibles up, 0 alertes actives.. O bloco Conteneurs vem de docker compose ps; você pode digitá-lo você mesmo. Terceiro caminho, em curl:

bash
curl -s http://localhost:8000/sante
curl -s http://localhost:8090/sante
text
{"etat":"ok","version":"1.0.0","cours":64}
{"etat":"ok","alertes_en_memoire":0,"notifications":0,"alertes":0}

B.4 — Os oito targets no Prometheus

No navegador, http://localhost:9090StatusTarget health: oito blocos 1 / 1 up, estado UP, coluna Last scrape sempre abaixo de 15 segundos. Em curl, com python3 para ler o JSON (ou jq se você tiver):

bash
curl -s http://localhost:9090/api/v1/targets | python3 -c 'import json,sys; [print(t["labels"]["job"].ljust(14), t["health"], t["scrapeUrl"]) for t in sorted(json.load(sys.stdin)["data"]["activeTargets"], key=lambda t: t["labels"]["job"])]'
text
alertmanager   up http://alertmanager:9093/metrics
alloy          up http://alloy:12345/metrics
api            up http://api:8000/metrics
cadvisor       up http://cadvisor:8080/metrics
grafana        up http://grafana:3000/metrics
loki           up http://loki:3100/metrics
node-exporter  up http://node-exporter:9100/metrics
prometheus     up http://localhost:9090/metrics

B.5 — As queries P1 a P12 e G1 a G8

Espere dois minutos depois de demarrer, depois siga as seções Prometheus, na aba Graph e Grafana, no Explore mais acima; elas são feitas no navegador, de forma idêntica em todos os sistemas. As queries estão prontas para copiar:

bash
cat modules/01-le-labo/requetes.txt
cat modules/01-le-labo/requetes-logql.txt

Terceiro caminho, uma query PromQL em curl:

bash
curl -s 'http://localhost:9090/api/v1/query?query=up' | python3 -m json.tool | head -n 20

Você reconhece no JSON as mesmas séries da aba Table: "metric": {"__name__": "up", "instance": "localhost:9090", "job": "prometheus"} e "value": [1789500809.696, "1"].

Ponto de verificação: P1 retorna 8 séries em 1, P12 retorna Empty query result, G1 retorna linhas JSON, G8 retorna três curvas.

B.6 — Quebrar: parar a API

Anote a hora (date +%T), depois:

bash
./labo.sh casser api
text

== Panne : arrêt de l'API ==
 Container labo-api Stopping
 Container labo-api Stopped
  ✔ API arrêtée. La charge continue de frapper dans le vide.
  À observer : ./labo.sh etat  ·  http://localhost:9090/targets (api → down)
               http://localhost:9090/alerts (APIInjoignable : pending puis firing après 30 s)
               http://localhost:9093 et http://localhost:8090 (l'alerte arrive ~10 s après firing)

Pour tout remettre en ordre : ./labo.sh reparer

O script fez docker compose stop api. Você tem cerca de 70 segundos antes que o alerta chegue ao webhook. Observe por cinco caminhos.

Caminho 1, etat:

bash
./labo.sh etat
text

== Conteneurs ==
NAME                 SERVICE         STATUS
labo-alertmanager    alertmanager    Up 22 minutes (healthy)
labo-alloy           alloy           Up 22 minutes (healthy)
labo-api             api             Exited (0) About a minute ago
labo-cadvisor        cadvisor        Up 22 minutes (healthy)
labo-charge          charge          Up 22 minutes (healthy)
labo-grafana         grafana         Up 22 minutes (healthy)
labo-loki            loki            Up 22 minutes (healthy)
labo-node-exporter   node-exporter   Up 22 minutes (healthy)
labo-prometheus      prometheus      Up 22 minutes (healthy)
labo-webhook         webhook         Up 22 minutes (healthy)

== Supervision ==
  ✔ Prometheus répond — cibles up : 7/8
  ✘ cible api (http://api:8000/metrics) : down — Get "http://api:8000/metrics": dial tcp: lookup api on 127.0.0.11:53: no such host
     séries en mémoire : 15116
     alertes : 2 active(s), 0 en attente (pending)
  ✘ APIInjoignable [critique] — L'API catalogue ne répond plus
  ✘ TauxErreursEleve [critique] — Plus de 5 % des requêtes de l'API échouent
  ✔ Alertmanager répond (http://localhost:9093)
  ✔ Grafana répond (http://localhost:3000)
  ✔ Loki répond (http://localhost:3100)
  ✘ API catalogue ne répond pas (http://localhost:8000)
  ✔ Webhook répond — 2 alerte(s) reçue(s) (http://localhost:8090)

Labo : 9/10 services, 7/8 cibles up, 2 alertes actives.

O que é preciso ler: labo-api está Exited (0) (parada voluntária); o Prometheus só lê 7/8 targets e diz por quê (lookup api … no such host: o nome api desapareceu da rede Docker); APIInjoignable está ativo; a API não responde; o webhook recebeu o alerta. Essa saída foi capturada um minuto depois de casser api, em uma máquina onde casser erreurs tinha acabado de ser executado: daí o segundo alerta TauxErreursEleve. Na sua máquina: 1 alertes actives, 1 alerte(s) reçue(s). Se você executar etat nos primeiros 30 segundos, o alerta ainda está pending e o webhook em 0: execute novamente um minuto depois. Terceiro caminho, em curl:

bash
curl -s http://localhost:8000/sante
text
curl: (7) Failed to connect to localhost:8000 after 2237 ms: Could not connect to server

Caminho 2, os targets. Recarregue http://localhost:9090StatusTarget health: o bloco api está em 0 / 1 up, estado DOWN, coluna Error: Get "http://api:8000/metrics": dial tcp: lookup api on 127.0.0.11:53: no such host. Digite up novamente em Query: up{instance="api:8000", job="api", service="api"} está em 0. Depois up == 0: uma única linha.

Caminho 3, os alertas no Prometheus. Menu Alerts. APIInjoignable muda de estado em três tempos, cronometrados na máquina do curso:

text
t+0 s   : APIInjoignable inactive      (Prometheus n'a pas encore rescrappé l'API)
t+40 s  : APIInjoignable pending       (up{job="api"} == 0 est vrai, le compte à rebours « for: 30s » tourne)
t+70 s  : APIInjoignable firing        (vrai depuis 30 s : Prometheus envoie à Alertmanager)
t+70 s  : APIInjoignable reçue par le webhook (firing)

Por que 40 segundos antes de pending: até 15 segundos para um scrape falhar (scrape_interval: 15s), depois até 15 segundos para a próxima avaliação das regras (evaluation_interval: 15s). Por que mais 30: for: 30s em alertes.yml. Digite ALERTS novamente em Query:

text
ALERTS{alertname="APIInjoignable", alertstate="pending", instance="api:8000", job="api", service="api", severite="critique"}    1

depois alertstate="firing". Anote a hora do firing: primeira linha do seu entregável. Em curl, a mesma coisa:

bash
curl -s http://localhost:9090/api/v1/alerts | python3 -m json.tool

Caminho 4, Alertmanager. Abra http://localhost:9093: a página Alerts mostra um grupo alertname="APIInjoignable" service="api" (o group_by de alertmanager.yml) com o alerta, seus labels (instance="api:8000", job="api", labo="observabilite", severite="critique"), o resumo L'API catalogue ne répond plus. O label labo="observabilite" vem do external_labels de prometheus.yml. Em curl:

bash
curl -s http://localhost:9093/api/v2/alerts | python3 -c 'import json,sys; [print(a["labels"]["alertname"], a["status"]["state"], a["startsAt"]) for a in json.load(sys.stdin)]'
text
APIInjoignable active 2026-09-15T19:41:11.496Z

Caminho 5, o webhook. Abra http://localhost:8090: uma linha APIInjoignable · critique · firing · api · L'API catalogue ne répond plus, cabeçalho 1 alerte(s) en mémoire · 1 notification(s) reçue(s). O formato bruto:

bash
curl -s http://localhost:8090/alertes.json | python3 -m json.tool
json
[
    {
        "recu_a": "2026-09-15T19:41:26+00:00",
        "etat": "firing",
        "nom": "APIInjoignable",
        "severite": "critique",
        "service": "api",
        "resume": "L'API catalogue ne répond plus",
        "description": "Prometheus n'arrive plus à lire http://api:8000/metrics depuis 30 secondes (cible api:8000).",
        "debut": "2026-09-15T19:41:11.496Z",
        "fin": "0001-01-01T00:00:00Z",
        "labels": {
            "alertname": "APIInjoignable",
            "instance": "api:8000",
            "job": "api",
            "labo": "observabilite",
            "service": "api",
            "severite": "critique"
        }
    }
]

debut (19:41:11) é a hora do firing no Prometheus; recu_a (19:41:26) chega quinze segundos depois, dos quais os 10 segundos de group_wait. fin no ano 0001: ainda não terminou. Anote recu_a: segunda linha do seu entregável.

O que a charge vê:

bash
./labo.sh journal charge
text
labo-charge  | {"horodatage": "2026-09-15T19:40:36.600+00:00", "niveau": "WARNING", "message": "API injoignable : ConnectionError"}
labo-charge  | {"horodatage": "2026-09-15T19:40:40.925+00:00", "niveau": "WARNING", "message": "API injoignable : ConnectionError"}
labo-charge  | {"horodatage": "2026-09-15T19:40:45.393+00:00", "niveau": "WARNING", "message": "API injoignable : ConnectionError"}

A diferença essencial entre um 404 e nenhuma resposta. Na etapa B.8, /cours/C9999 vai responder 404: a API está rodando e te diz que esse curso não existe; isso é contado em http_requetes_total{code="404"}, é escrito em um log WARNING, e up continua em 1. Aqui, curl: (7) Failed to connect: ninguém responde, não há nem código nem log do lado da API, e é up que cai para 0.

B.7 — Reparar

bash
./labo.sh reparer
text

== Réparation ==
  ✔ API redémarrée
  api             .. prêt (6 s)
  ✔ taux d'erreurs remis à 0.01, lenteur à 0 ms

Les alertes passent en « resolved » dans les minutes qui suivent (voir http://localhost:8090).

O script fez docker compose start api, esperou /sante, depois chamou /admin/reparer. Verifique que a API está trabalhando:

bash
./labo.sh journal api
text
labo-api  | {"horodatage": "2026-09-15T19:41:48.720+00:00", "niveau": "INFO", "id_requete": "6a4228a46a56", "methode": "POST", "route": "/inscriptions", "code": 201, "duree_ms": 15.6, "message": "POST /inscriptions -> 201"}
labo-api  | {"horodatage": "2026-09-15T19:41:48.914+00:00", "niveau": "WARNING", "id_requete": "b4ebe9fa32d8", "methode": "GET", "route": "inconnue", "code": 404, "duree_ms": 0.4, "message": "GET /inexistant -> 404"}
labo-api  | {"horodatage": "2026-09-15T19:41:48.937+00:00", "niveau": "INFO", "id_requete": "732adf61ee17", "methode": "GET", "route": "/cours/{id}", "code": 200, "duree_ms": 25.7, "message": "GET /cours/C0043 -> 200"}

Depois o alerta se apaga, cronometrado na máquina do curso depois de reparer:

text
t+15 s  : APIInjoignable firing   (Prometheus)   · webhook : firing
t+40 s  : APIInjoignable inactive (Prometheus)   · webhook : firing
t+55 s  : APIInjoignable inactive (Prometheus)   · webhook : resolved

Terceiro caminho, monitorar em loop:

bash
watch -n 5 'curl -s http://localhost:8090/alertes.json | python3 -c "import json,sys; [print(a[\"recu_a\"], a[\"nom\"], a[\"etat\"]) for a in json.load(sys.stdin)]"'
text
2026-09-15T19:42:26+00:00 APIInjoignable resolved
2026-09-15T19:41:26+00:00 APIInjoignable firing

A linha resolved traz um campo fin preenchido (2026-09-15T19:41:56.496Z): 45 segundos de falha aos olhos do Prometheus. Anote seu recu_a: terceira linha do seu entregável. Ctrl+C para sair do watch.

B.8 — Provocar um 404, para comparar

bash
curl -s -i http://localhost:8000/cours/C9999
text
HTTP/1.1 404 Not Found
date: Tue, 15 Sep 2026 20:50:01 GMT
server: uvicorn
content-length: 36
content-type: application/json
x-id-requete: bbc7be667f1d

{"detail":"cours C9999 introuvable"}

A API respondeu: um código 404, um identificador x-id-requete, um corpo JSON. Digite P4 novamente no Prometheus com 404 no lugar de 500: a série route="/cours/{id}" aumentou em 1. Digite G1 novamente adicionando |= "bbc7be667f1d" (seu próprio identificador, lido no cabeçalho): sua requisição está ali, nível WARNING. Nada disso existe para a falha de B.6: uma API parada não conta nada e não escreve nada.

B.9 — Verificação final

bash
./labo.sh etat
text

== Conteneurs ==
NAME                 SERVICE         STATUS
labo-alertmanager    alertmanager    Up 23 minutes (healthy)
labo-alloy           alloy           Up 23 minutes (healthy)
labo-api             api             Up 53 seconds (healthy)
labo-cadvisor        cadvisor        Up 23 minutes (healthy)
labo-charge          charge          Up 23 minutes (healthy)
labo-grafana         grafana         Up 23 minutes (healthy)
labo-loki            loki            Up 23 minutes (healthy)
labo-node-exporter   node-exporter   Up 23 minutes (healthy)
labo-prometheus      prometheus      Up 23 minutes (healthy)
labo-webhook         webhook         Up 23 minutes (healthy)

== Supervision ==
  ✔ Prometheus répond — cibles up : 8/8
     séries en mémoire : 15395
     alertes : 0 active(s), 0 en attente (pending)
  ✔ Alertmanager répond (http://localhost:9093)
  ✔ Grafana répond (http://localhost:3000)
  ✔ Loki répond (http://localhost:3100)
  ✔ API catalogue répond — version 1.0.0, 64 cours
  ✔ Webhook répond — 2 alerte(s) reçue(s) (http://localhost:8090)

Labo : 10/10 services, 8/8 cibles up, 0 alertes actives.

O que você deve ter: labo-api novamente Up … (healthy), 8/8, 0 active(s), 64 cours, 2 alerte(s) reçue(s) (a firing e a resolved), e Labo : 10/10 services, 8/8 cibles up, 0 alertes actives.. Na máquina do curso, essa saída ainda trazia 1 alertes actives e 3 alerte(s) reçue(s) por causa do TauxErreursEleve residual mencionado em B.6; na sua máquina, com apenas a falha api, os valores são os de cima. Essa saída e suas três horas anotadas são seu entregável. Você pode deixar rodando para as oficinas 06 e 07, ou ./labo.sh arreter: os dados são conservados.

Anexo C — Se travar (todos os sistemas)

Afficher les cas où ça coince

etat diz 9/10 services mesmo você não tendo quebrado nada. Veja qual contêiner não está (healthy). Se for grafana ou alloy logo depois de demarrer com (health: starting), espere trinta segundos. Se for labo-api em Exited, alguém (talvez você mesmo, em uma tentativa anterior) executou casser api: reparer. Se um contêiner estiver Restarting, leia seu log: .\labo.ps1 journal <service> ou ./labo.sh journal <service>.

cibles up : 7/8 e APIInjoignable ativo, mas a API responde em http://localhost:8000. O Prometheus lê a API pela rede Docker (http://api:8000/metrics), você pela porta publicada (localhost:8000). Se a API acabou de reiniciar, o Prometheus pode estar atrasado um scrape (15 s) e o alerta permanece firing até a próxima avaliação, mais alguns segundos para o webhook receber o resolved. Espere um minuto e execute etat novamente.

Os alertas demoram: pending não vira firing. APIInjoignable tem for: 30s; então é preciso até 15 s (scrape) + 15 s (avaliação) + 30 s (for) = 60 a 70 s para firing, depois 10 s de group_wait para o webhook. Não está lento, está configurado assim para evitar falsos alertas em uma falha isolada. Se depois de dois minutos nada se mexer, verifique que labo-api está de fato Exited (docker compose ps).

O webhook continua em 0 alerte(s) reçue(s) mesmo o Alertmanager mostrando o alerta. Abra http://localhost:9093Status: a seção Config deve mostrar receiver: webhook e url: http://webhook:8090/alertes. Depois journal webhook: você deve ver uma linha POST /alertes a cada notificação. Se o contêiner labo-webhook não estiver healthy, docker compose restart webhook.

Empty query result em P3, P5 ou G1, logo após demarrer. O Prometheus precisa de pelo menos um scrape (15 s) para P3, de dois para P5 (rate precisa de dois pontos em [1m]), e o Alloy leva alguns segundos para enviar a primeira linha ao Loki. Espere dois minutos depois de Le labo est prêt.. Se {service="api"} continuar vazio depois de cinco minutos, verifique http://localhost:12345 (o Alloy deve estar ready e seus componentes Healthy) e journal alloy.

P10 retorna NaN. histogram_quantile retorna NaN quando a janela [5m] ainda não contém pontos suficientes. Espere cinco minutos depois da inicialização, ou substitua [5m] por [1m] para ver um valor mais cedo (menos estável).

G1 retorna zero linhas mesmo a API estando rodando. Verifique primeiro o período (no topo à direita, Last 1 hour) e o nome do label (service, em minúsculas). Depois http://localhost:12345: o Alloy deve responder Alloy is ready. e journal alloy não deve mostrar erro repetido. Em último caso, docker compose restart alloy.

parse error no Prometheus. Os três mais frequentes, todos vistos nesta página: unexpected identifier "api" in label matching, expected string (aspas esquecidas: {job=api}); unexpected character inside braces: '5' ({code=500} em vez de {code="500"}); expected type range vector in call to function "rate", got instant vector (janela [1m] esquecida).

parse error no Loki. syntax error: unexpected IDENTIFIER: você esqueceu as chaves (service="api" em vez de {service="api"}). unexpected $end, expecting } or ,: chave de fechamento faltando. Zero linhas sem erro: nome ou caixa do label ({service="API"}, {app="api"}).

Somente Windows — Invoke-RestMethod exibe caracteres estranhos (é) nos títulos dos cursos. É a exibição do console, não a API. [Console]::OutputEncoding = [Text.Encoding]::UTF8 antes do comando, ou leia no navegador.

Somente Windows — .\labo.ps1: "l'exécution de scripts est désactivée sur ce système". Set-ExecutionPolicy -Scope CurrentUser RemoteSigned, responda O, execute novamente.

Somente Windows — demarrer falha com port is already allocated na 3000. Outro programa já está escutando (frequentemente uma aplicação Node). $env:GRAFANA_PORT = '3001' depois execute demarrer novamente; o Grafana fica então em http://localhost:3001 e etat o exibe assim.

Somente Linux nativo — permission denied while trying to connect to the Docker daemon socket. sudo usermod -aG docker $USER, feche a sessão, reabra-a, docker info deve responder.

Linux nativo e macOS — bash: ./labo.sh: Permission denied. O arquivo é salvo sem o bit de execução no repositório: chmod +x labo.sh uma vez, ou execute bash labo.sh prerequis. No Git Bash (Windows), essa questão não se coloca.

Você quer recomeçar do zero. .\labo.ps1 reinitialiser ou ./labo.sh reinitialiser remove os contêineres e os volumes: Prometheus, Loki e Grafana começam vazios de novo. Depois demarrer. Não fazer isso em um labo compartilhado com outras pessoas.