Κατευθυνόμενη άσκηση — Εκκίνηση, έλεγχος, σπάσιμο και επιδιόρθωση του εργαστηρίου

Καθοδηγούμενη πρακτική52 λεπτά
Διάρκεια
60 έως 90 λεπτά
Ενότητα
1/7
Θα κατασκευάσεις
ένα εργαστήριο δέκα υπηρεσιών που ξεκινά και ελέγχεται από τρεις διαδρομές (το σενάριο etat, η καρτέλα Graph του Prometheus, το Explore στο Grafana), μετά σπασμένο επίτηδες με casser api και επιδιορθωμένο με reparer
Παραδοτέο
η πλήρης έξοδος του etat με Labo : 10/10 services, 8/8 cibles up, 0 alertes actives., πλέον τρεις γραμμές γραμμένες με το χέρι: η ώρα που η APIInjoignable πέρασε σε firing στον Prometheus, η ώρα που το webhook τη έλαβε, η ώρα που πέρασε σε resolved

Πώς να διαβάσεις αυτή τη σελίδα. Κάθε ενότητα είναι διπλωμένη κάτω από τον τίτλο της: κάνε κλικ στο «Εμφάνιση …» για να την ανοίξεις, και ξανακλείσ' την όταν τελειώσεις για να κρατήσεις τη σελίδα ευανάγνωστη. Σειρά ανάγνωσης: Στόχος, μετά Εν συντομία (οι εντολές να πληκτρολογήσεις), μετά Το σύνολο δεδομένων (να διαβαστεί πριν από κάθε ερώτημα), μετά τα ερωτήματα Prometheus (P1 έως P12) και Grafana Explore (G1 έως G8), ταξινομημένα από το πιο απλό (up, {service="api"}) στο πιο εκφραστικό, με μια εξήγηση μετά από καθένα. Το λεπτομερές βήμα προς βήμα, με την αναμενόμενη έξοδο κάθε εντολής και τη βλάβη που πρέπει να προκαλέσεις, βρίσκεται σε παράρτημα: παράρτημα A για Windows (PowerShell), παράρτημα B για Linux, macOS, WSL 2 και Git Bash. Άνοιξε ένα μόνο παράρτημα, αυτό του συστήματός σου. Το παράρτημα C, κοινό, συγκεντρώνει τις περιπτώσεις όπου κάτι κολλάει. Όλες οι έξοδοι αυτής της σελίδας καταγράφηκαν στο εργαστήριο του μαθήματος· οι τιμές που εξαρτώνται από τη στιγμή (μετρητές, διάρκειες, χρονοσφραγίδες) θα είναι διαφορετικές σε σένα, οι μορφές θα είναι ίδιες.

Στόχος

Μπαίνεις στην ομάδα που λειτουργεί τον κατάλογο μαθημάτων μιας διαδικτυακής πλατφόρμας. Η προϊσταμένη σου σου δίνει το kit του εργαστηρίου: «Αύριο το πρωί, θέλω τη στοίβα παρατηρησιμότητας να τρέχει στον σταθμό σου, το API μέσα, και την απόδειξη ότι ξέρεις να διαβάζεις μια βλάβη χωρίς να με καλέσεις.» Θα ξεκινήσεις λοιπόν τις δέκα υπηρεσίες, θα αποδείξεις ότι ο Prometheus διαβάζει σωστά τους οκτώ στόχους του και ότι το Loki λαμβάνει τις καταγραφές του API, θα πληκτρολογήσεις δώδεκα ερωτήματα PromQL και οκτώ ερωτήματα LogQL για να μάθεις να διαβάζεις τι μετρά το εργαστήριο, και μετά θα σταματήσεις το API επίτηδες. Θα δεις τη βλάβη να διαδίδεται: το etat τη βλέπει σε δύο δευτερόλεπτα, ο Prometheus βάζει τον στόχο σε DOWN, η ειδοποίηση APIInjoignable περνά από pending σε firing, ο Alertmanager τη στέλνει στο webhook. Μετά επιδιορθώνεις και βλέπεις την ειδοποίηση να σβήνει. Να αναγνωρίζεις «αυτή η υπηρεσία είναι σταματημένη» σε δέκα δευτερόλεπτα, και να ξέρεις πού να την ψάξεις, είναι αυτό που αποφεύγει ώρες αναζήτησης στο λάθος σημείο.

Τα εννέα βήματα αυτού του σχήματος αναλύονται, με την αναμενόμενη έξοδο κάθε εντολής, στο παράρτημα A (Windows) ή στο παράρτημα B (Linux, macOS) στο κάτω μέρος της σελίδας.

Εν συντομία: οι εντολές του εργαστηρίου

Εμφάνιση εντολών

Kit του εργαστηρίου: https://github.com/hrhouma2/aiopsatlas-observabilite-labo-fr

Κλωνοποιείς το kit σε έναν φάκελο lab3, ελέγχεις ότι το Docker είναι έτοιμο, ξεκινάς τις δέκα υπηρεσίες, ανοίγεις τις σελίδες web, πληκτρολογείς τα ερωτήματα, μετά σπας και επιδιορθώνεις. Στο τέλος, το etat πρέπει να εμφανίζει Labo : 10/10 services, 8/8 cibles up, 0 alertes actives., το API πρέπει να γνωρίζει 64 cours, και το webhook πρέπει να έχει λάβει δύο γνωστοποιήσεις για την APIInjoignable: μία firing, μία resolved. Ξεκίνα εκτελώντας αυτό το μπλοκ.

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.

Περίμενε δύο λεπτά (τον χρόνο να έχει ο Prometheus μερικές μετρήσεις), μετά άνοιξε τις σελίδες στον περιηγητή:

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)

Τα ερωτήματα P1 έως P12 βρίσκονται επίσης στο modules\01-le-labo\requetes.txt, και τα G1 έως G8 στο modules\01-le-labo\requetes-logql.txt: άνοιξέ τα σε έναν επεξεργαστή κειμένου και κάνε αντιγραφή-επικόλληση. Μετά η βλάβη:

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.

Αν το PowerShell αρνείται το .\labo.ps1 («l'exécution de scripts est désactivée», η εκτέλεση σεναρίων είναι απενεργοποιημένη): Set-ExecutionPolicy -Scope CurrentUser RemoteSigned, απάντησε O, ξαναεκτέλεσε. Αν η θύρα 3000 είναι ήδη κατειλημμένη στον υπολογιστή σου, $env:GRAFANA_PORT = '3001' πριν από το demarrer, και αντικατάστησε το 3000 με 3001 στα URL του 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.

Περίμενε δύο λεπτά, μετά άνοιξε τις σελίδες στον περιηγητή:

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)

Τα ερωτήματα βρίσκονται στο modules/01-le-labo/requetes.txt (P1 έως P12) και στο modules/01-le-labo/requetes-logql.txt (G1 έως G8). Μετά η βλάβη:

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.

Αν η θύρα 3000 είναι κατειλημμένη: GRAFANA_PORT=3001 ./labo.sh demarrer, και μετά 3001 στα URL του Grafana.

Το σύνολο δεδομένων: τι θα χειριστείς

Εμφάνιση του συνόλου δεδομένων

Πριν πληκτρολογήσεις ένα και μόνο ερώτημα, κοίτα τι παρατηρεί το εργαστήριο. Όλα περιστρέφονται γύρω από ένα API καταλόγου μαθημάτων: μια μικρή υπηρεσία web γραμμένη σε Python (FastAPI) που σερβίρει 64 μαθήματα και καταγράφει εγγραφές. Μια δεύτερη υπηρεσία, το charge, παίζει τον ρόλο των χρηστών: καλεί το API συνεχώς, με επιτυχημένα αιτήματα, μερικά εκούσια 404 και, μία φορά στις εκατό, ένα σφάλμα 500 που το API κατασκευάζει το ίδιο. Όλα όσα θα διαβάσεις στον Prometheus και στο Loki προέρχονται από αυτές τις δύο υπηρεσίες. Το υπόλοιπο kit, ξεκάθαρα:

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

Άνοιξε τα δεδομένα εσύ ο ίδιος, παίρνει δέκα δευτερόλεπτα:

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"

Το API καταλόγου: 64 μαθήματα σε ένα αρχείο JSON

Το api/donnees/cours.json είναι ένας πίνακας JSON 64 αντικειμένων, ένα ανά μάθημα, αναγνωριστικά C0001 έως C0064. Το πρώτο, όπως το επιστρέφει το API στο 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}
ΠεδίοΠαράδειγμαΤι είναι
idC0001Αναγνωριστικό του μαθήματος, C ακολουθούμενο από τέσσερα ψηφία, από C0001 έως C0064
titreIntroduction à PythonΕμφανιζόμενος τίτλος
categorieprogrammationΜία από τις εννέα κατηγορίες: cloud, donnees, gestion, ia, outils, programmation, securite, systemes, web
niveaudebutantdebutant, intermediaire ή avance
prix89Τιμή σε δολάρια, ακέραιος
duree_heures6Συνολική διάρκεια, σε ώρες
professeurKarim HaddadΈνας από τους δέκα καθηγητές του καταλόγου
tags["code","algorithmes"]Λίστα λέξεων-κλειδιών
note4.8Μέση βαθμολογία στα 5
inscrits2319Αριθμός εγγεγραμμένων κατά τη φόρτωση· οι εγγραφές που γίνονται κατά το εργαστήριο μετριούνται ξεχωριστά, στη μετρική inscriptions_total

Οι διαδρομές του API και τι απαντούν στο εργαστήριο του μαθήματος:

ΔιαδρομήΠραγματική απόκρισηΤι κάνει
GET /sante{"etat":"ok","version":"1.0.0","cours":64}Ο έλεγχος υγείας που καλεί το Docker· το etat διαβάζει version και cours από εδώ
GET /cours?limite=2{"total":64,"page":1,"limite":2,"cours":[…]}Η σελιδοποιημένη λίστα· φίλτρα categorie και niveau (?categorie=cloud"total":6)
GET /cours/C0001το παραπάνω έγγραφοΜια καρτέλα· το API μετρά κάθε προβολή στο cours_consultes_total{cours_id="C0001"}
GET /cours/C9999404 {"detail":"cours C9999 introuvable"}Ένα καθαρό 404: η υπηρεσία είναι υγιής, ο πόρος δεν υπάρχει
POST /inscriptions201404 αν το μάθημα δεν υπάρχει, 422 αν το σώμα είναι άκυρο)Καλείται από το charge· αυξάνει το inscriptions_total{cours_id="…"}
GET /lent{"attente_ms":305}Μια επίτηδες αργή διαδρομή (300 έως 900 ms) για να τροφοδοτεί το ιστόγραμμα καθυστέρησης
GET /admin/etat{"taux_erreurs":0.01,"lenteur_ms":0,"inscriptions_enregistrees":855,…}Οι ρυθμίσεις βλάβης· το casser erreurs και το casser lenteur τις αλλάζουν, το reparer τις επαναφέρει
GET /metricsπερίπου 270 γραμμές κειμένουΑυτό που διαβάζει ο Prometheus κάθε 15 δευτερόλεπτα

Κάθε απόκριση φέρει μια κεφαλίδα x-id-requete (για παράδειγμα x-id-requete: c5c49525ea53): είναι το ίδιο αναγνωριστικό με το πεδίο id_requete της γραμμής log που γράφτηκε για αυτό το αίτημα. Θα σου χρησιμεύσει στο ερώτημα G7.

Οι μετρικές: τι εκθέτει το /metrics, μια πραγματική γραμμή ανά τύπο

Η σελίδα http://localhost:8000/metrics είναι κείμενο, μια σειρά ανά γραμμή, με πριν από αυτήν δύο γραμμές σχολίου # HELP (σε τι χρησιμεύει η μετρική) και # TYPE (ο τύπος της). Στο εργαστήριο του μαθήματος, έχει περίπου 270 γραμμές. Οι τέσσερις μετρικές που θα ερωτήσεις, αντιγραμμένες από τη σελίδα:

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
ΤύποςΜετρική του εργαστηρίουΠώς να τη διαβάσεις
counter (μετρητής)http_requetes_totalΜόνο ανεβαίνει: 3247 αποκρίσεις 200 στο /cours από την εκκίνηση του API. Μόνο η ταχύτητά της έχει νόημα (P5)
gaugerequetes_en_coursΑνεβαίνει και κατεβαίνει: 1 αίτημα υπό επεξεργασία τη στιγμή της ανάγνωσης. Διαβάζεται ως έχει (P11)
histogram (ιστόγραμμα)http_duree_requete_secondsΣωρευτικά καλάθια: 3248 αιτήματα στο /cours πήραν λιγότερο από 50 ms (le="0.05"), 3279 συνολικά (+Inf = _count). _sum / _count = μέση διάρκεια (εδώ 26 ms). Το P10 βγάζει από εδώ ένα p95
info (ένα gauge στο 1)api_infoΗ τιμή είναι πάντα 1· η πληροφορία βρίσκεται στο label version="1.0.0"

Δεν υπάρχει summary σε αυτό το API: ο Prometheus το αποθαρρύνει προς όφελος του ιστογράμματος, που συναθροίζεται μεταξύ instances.

Τα labels: οι στήλες των μελλοντικών πινάκων σου

Μια σειρά είναι ένα όνομα μετρικής πλέον ένα σύνολο ζευγών label="τιμή". Δύο προελεύσεις:

LabelΤοποθετείται απόΤιμές στο εργαστήριο
methodeτο APIGET, POST
routeτο API/sante, /cours, /cours/{id}, /inscriptions, /lent, /admin/etat, inconnue (κάθε URL που δεν υπάρχει)
codeτο API200, 201, 404, 422, 500
leτο API, μόνο στο ιστόγραμμα0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.0, +Inf
cours_idτο API, στο inscriptions_total και στο cours_consultes_totalC0001 έως C0064
jobο Prometheus, σύμφωνα με το prometheus.ymlprometheus, api, node-exporter, cadvisor, alertmanager, grafana, loki, alloy
instanceο Prometheusη διεύθυνση που διαβάζεται: api:8000, localhost:9090, grafana:3000
serviceο Prometheus, προστέθηκε με το χέρι στο prometheus.yml για το job apiapi

Πρόσεξε τη διαδρομή /cours/{id}: το API κανονικοποιεί το URL πριν μετρήσει. Το /cours/C0001 και το /cours/C0043 πέφτουν στην ίδια σειρά. Χωρίς αυτό, θα υπήρχαν 64 σειρές ανά κωδικό αντί για μία, και 64 φορές περισσότερες γραμμές στο /metrics.

Οι καταγραφές: μια γραμμή JSON ανά αίτημα

Το API γράφει μια γραμμή ημερολογίου ανά επεξεργασμένο αίτημα, σε JSON. Το Alloy διαβάζει την έξοδο κάθε container labo-* και την προωθεί στο Loki. Μια πραγματική γραμμή, διαβασμένη με .\labo.ps1 journal api./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"}
ΠεδίοΠαράδειγμαΤι είναι
horodatage2026-09-15T19:33:14.781+00:00Ημερομηνία και ώρα UTC, στο χιλιοστό του δευτερολέπτου
niveauERRORINFO (2xx), WARNING (4xx), ERROR (5xx)
id_requetedc1c2ff189a6Το αναγνωριστικό που επιστρέφεται στην κεφαλίδα x-id-requete της απόκρισης
methode, route, codeGET, /cours/{id}, 500Οι ίδιες τιμές με τα labels του http_requetes_total: είναι η γέφυρα μεταξύ μετρικών και καταγραφών
duree_ms0.1Διάρκεια επεξεργασίας, σε χιλιοστά του δευτερολέπτου
messageGET /cours/C0019 -> 500Η αναγνώσιμη φράση, με το πραγματικό URL αυτή τη φορά (C0019, όχι {id})

Το Loki δεν διαβάζει το JSON γραμμή προς γραμμή τη στιγμή του ερωτήματος, εκτός αν του το ζητήσεις (| json, ερώτημα G5). Αυτό που ευρετηριάζει είναι labels που τοποθετεί το Alloy κατά την άφιξη:

Label LokiΤιμέςΤοποθετείται από
serviceapi, charge, webhook, prometheus, alertmanager, grafana, loki, alloy, node-exporter, cadvisorτο Alloy, σύμφωνα με το όνομα της υπηρεσίας Compose
conteneurlabo-api, labo-chargeτο Alloy, σύμφωνα με το όνομα του container
niveauINFO, WARNING, ERRORτο Alloy, εξαγόμενο από το πεδίο niveau του JSON
code200, 201, 404, 422, 500το Alloy, εξαγόμενο από το πεδίο code του JSON
detected_levelinfo, warn, errorτο ίδιο το Loki, που μαντεύει το επίπεδο· μπορείς να το αγνοήσεις

Το κόκκινο νήμα

Κράτα δύο πράγματα που θα ξαναβρείς παντού: τη σειρά http_requetes_total{code="500",route="/cours"}, στο 32 στο /metrics τη στιγμή της καταγραφής, που θα ξαναδείς στο 32 στο P4· και το αναγνωριστικό dc1c2ff189a6, αυτό της παραπάνω γραμμής σφάλματος, που θα ξαναβρείς στο G2 και μετά θα πας να το ψάξεις μόνος σου στο G7. Οι μετρικές μετρούν, οι καταγραφές αφηγούνται· και οι δύο μιλούν για το ίδιο αίτημα.

Πρώτα ερωτήματα: μια έννοια τη φορά

Οι δύο ενότητες που ακολουθούν περιέχουν είκοσι ερωτήματα: δώδεκα για τον Prometheus (P1 έως P12), οκτώ για το Loki μέσα από το Grafana Explore (G1 έως G8). Είναι ταξινομημένα από το πιο απλό στο πιο εκφραστικό, και καθένα προσθέτει μόνο μία καινοτομία σε σχέση με το προηγούμενο. Αν ένα ερώτημα σου φαίνεται σκοτεινό, είναι σχεδόν πάντα γιατί το προηγούμενο δεν είναι ακόμη σαφές: γύρνα πίσω αντί να συνεχίσεις.

Πληκτρολόγησε κάθε ερώτημα εσύ ο ίδιος, σύγκρινε το αποτέλεσμα με αυτό της σελίδας, διάβασε την εξήγηση, και μετά πέρασε στο επόμενο. Οι αριθμοί θα είναι διαφορετικοί σε σένα: οι μετρητές ανεβαίνουν από την εκκίνηση του δικού σου API, όχι αυτού του μαθήματος. Οι μορφές (ο αριθμός σειρών, τα labels, η τάξη μεγέθους) πρέπει να είναι οι ίδιες. Ο Prometheus και το Loki χρησιμοποιούν την ίδια αρχική ιδέα, ένα σύνολο labels μέσα σε άγκιστρα, και είναι επίτηδες: αυτό που μαθαίνεις στο P2 χρησιμεύει στο G1.

Prometheus, στην καρτέλα Graph

Εμφάνιση των 12 ερωτημάτων PromQL (P1 έως P12)

Άνοιξε το http://localhost:9090. Φτάνεις στη σελίδα Query (το πάνω μενού προτείνει Query, Alerts, Status). Επικόλλησε ένα ερώτημα στο πεδίο, πάτησε Enter ή κάνε κλικ στο Execute. Το αποτέλεσμα εμφανίζεται από κάτω στην καρτέλα Table (μια γραμμή ανά σειρά, η τιμή δεξιά)· η καρτέλα Graph σχεδιάζει τις ίδιες σειρές στον χρόνο. Μείνε στο Table για αυτή την ενότητα, εκτός αν λέγεται διαφορετικά. Κάτω από τις καρτέλες, μια γραμμή του τύπου Load time: 40ms Result series: 8 σου λέει πόσες σειρές απάντησαν.

Η εικόνα που πρέπει να κρατήσεις στο μυαλό: ο Prometheus είναι ένα σημειωματάριο μετρήσεων. Κάθε 15 δευτερόλεπτα, περνά μπροστά από καθέναν από τους οκτώ στόχους του, διαβάζει τη σελίδα /metrics τους και σημειώνει κάθε τιμή με την ώρα. Ένα ερώτημα PromQL είναι μια ερώτηση που τίθεται σε αυτό το σημειωματάριο.

P1. Ποιος απαντά;

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, όλες στο 1.

Τι ζητά το ερώτημα: «Δώσε μου την τελευταία τιμή της μετρικής up για όλους τους στόχους.»

Μηδέν παράμετροι: μόνο ένα όνομα μετρικής. Η up δεν εκτίθεται από κανέναν στόχο· είναι ο Prometheus που την κατασκευάζει σε κάθε scrape: 1 αν η σελίδα /metrics απάντησε, 0 αλλιώς. Οκτώ σειρές γιατί το prometheus.yml δηλώνει οκτώ jobs. Κάθε γραμμή διαβάζεται: το όνομα της μετρικής, μετά μέσα σε άγκιστρα τα labels που τοποθέτησε ο Prometheus (job και instance σε όλες, service επιπλέον στο API), μετά η τιμή. Ισοδύναμο SQL: SELECT * FROM up. Αυτό που κάνει ο Prometheus και δεν κάνει η SQL: παρήγαγε ο ίδιος αυτόν τον πίνακα χτυπώντας οκτώ πόρτες.

Για να καταλάβεις καλά: γιατί 8 και όχι 10;

Το εργαστήριο έχει δέκα containers αλλά ο Prometheus διαβάζει μόνο οκτώ: το charge και το webhook δεν εκθέτουν σελίδα /metrics σε αυτή την έκδοση του kit, άρα δεν είναι στόχοι. Το etat τα μετρά ξεχωριστά: 10/10 services (τα containers) και 8/8 cibles up (τα scrapes). Αν κάποια μέρα το up επιστρέψει 7 σειρές αντί για 8, δεν είναι ότι ένας στόχος έπεσε (θα ήταν στο 0): είναι ότι ένα job εξαφανίστηκε από τη ρύθμιση. Το kit έχει μια ειδοποίηση για αυτό, την CibleAbsente.

P2. Ένας μόνο στόχος

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

Τι ζητά το ερώτημα: «Η τιμή της up, μόνο για τις σειρές των οποίων το label job ισούται με api

Μία μόνο καινοτομία: ο επιλογέας {job="api"}. Τα άγκιστρα φιλτράρουν πάνω στα labels, όπως ένα WHERE job = 'api'. Τα εισαγωγικά είναι υποχρεωτικά γύρω από την τιμή: το up{job=api} απορρίπτεται με parse error: unexpected identifier "api" in label matching, expected string. Αυτή ακριβώς η έκφραση, up{job="api"} == 0, είναι αυτή που παρακολουθεί ο κανόνας APIInjoignable· θα τη δεις να περνά στο 0 στο παράρτημα.

P3. Ο ακατέργαστος μετρητής

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 στο εργαστήριο του μαθήματος (ο ακριβής αριθμός εξαρτάται από τους συνδυασμούς που το charge έχει ήδη παραγάγει· ανεβαίνει έως 16 με τον χρόνο).

Τι ζητά το ερώτημα: «Όλες οι σειρές του μετρητή http_requetes_total, με την τρέχουσα τιμή τους.»

Τίποτα νέο στη σύνταξη: ένα όνομα, όπως στο P1. Αυτό που είναι νέο είναι τι διαβάζεις. Σύγκρινε με τη σελίδα /metrics: η γραμμή http_requetes_total{code="200",methode="GET",route="/cours"} 3247.0 έγινε http_requetes_total{code="200", instance="api:8000", job="api", methode="GET", route="/cours", service="api"} 3241. Ο Prometheus πρόσθεσε τρία labels (instance, job, service) και η τιμή διαφέρει κατά μερικές μονάδες: η σελίδα διαβάστηκε σε άλλη στιγμή. Μια σειρά ανά συνδυασμό (methode, route, code): αυτό είναι που ονομάζουμε πληθικότητα της μετρικής, εδώ 14.

Η ουσιώδης διαφορά μεταξύ /metrics και Prometheus. Η σελίδα /metrics είναι η κατάσταση του API τη στιγμή που την ανοίγεις, χωρίς ιστορικό. Ο Prometheus κρατά όλες τις αναγνώσεις, μία κάθε 15 δευτερόλεπτα, και αυτό είναι που επιτρέπει το P5: για να υπολογίσεις μια ταχύτητα χρειάζεσαι τουλάχιστον δύο σημεία.

P4. Μόνο τα σφάλματα 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

Τι ζητά το ερώτημα: «Οι σειρές του http_requetes_total των οποίων το label code ισούται με 500

Τίποτα νέο: είναι το P2 εφαρμοσμένο στο P3. Τέσσερις σειρές, μία ανά επηρεασμένη διαδρομή. Το κόκκινο νήμα είναι εδώ: route="/cours" στο 32, η τιμή που διαβάστηκε στο /metrics. Το 500 είναι συμβολοσειρά, όχι αριθμός: το http_requetes_total{code=500} απορρίπτεται (parse error: unexpected character inside braces: '5'). Αυτά τα 500 δεν είναι βλάβη: το charge προκαλεί επίτηδες ένα σφάλμα στα εκατό (taux_erreurs: 0.01 στο /admin/etat) για να μην είναι ποτέ κενές οι καμπύλες σφαλμάτων.

Απολογισμός P1 έως P4: δεν έχεις υπολογίσει ακόμη τίποτα. Διάβασες στιγμιαίες τιμές και έμαθες να τις φιλτράρεις ανά label.

P5. Η ταχύτητα ενός μετρητή

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, σε αιτήματα ανά δευτερόλεπτο.

Τι ζητά το ερώτημα: «Για κάθε σειρά του μετρητή, κατά πόσο αυξήθηκε ανά δευτερόλεπτο, κατά μέσο όρο, στο τελευταίο λεπτό;»

Μία μόνο καινοτομία, σε δύο αδιαχώριστα κομμάτια: το [1m] μετατρέπει τη σειρά σε εύρος (όλες οι τιμές του τελευταίου λεπτού, αντί μόνο της τελευταίας), και το rate() υπολογίζει την κλίση αυτού του εύρους. Κοίτα το αποτέλεσμα: το όνομα της μετρικής εξαφανίστηκε από τα άγκιστρα, γιατί δεν είναι πλέον http_requetes_total, είναι μια παράγωγη ταχύτητα. Το /cours λαμβάνει 3,7 αιτήματα ανά δευτερόλεπτο, το /sante 0,11 (ένα κάθε 9 δευτερόλεπτα: είναι το healthcheck του Docker). Ισοδύναμο SQL: δεν υπάρχει απλό· θα χρειάζονταν δύο αναγνώσεις, μια αφαίρεση και μια διαίρεση με τον χρόνο που πέρασε. Το rate() το κάνει για κάθε σειρά, και διορθώνει επιπλέον τους μηδενισμούς όταν το API επανεκκινεί.

Χωρίς το εύρος, ο Prometheus αρνείται: το rate(http_requetes_total) δίνει parse error: expected type range vector in call to function "rate", got instant vector. Θα διαβάσεις αυτό το μήνυμα συχνά· σημαίνει «λείπει το […]».

Για να καταλάβεις καλά: γιατί δεν διαβάζουμε ποτέ έναν μετρητή ως έχει

3241 αιτήματα 200 στο /cours δεν λέει τίποτα: από πότε; Αν το API τρέχει μία ώρα, είναι ήρεμο· αν τρέχει ένα λεπτό, είναι επίθεση. Ένας μετρητής αξίζει μόνο για την κλίση του. Γι' αυτό σε έναν πίνακα ελέγχου, δεν θα δεις ποτέ το http_requetes_total ακατέργαστο, αλλά πάντα rate(http_requetes_total[…]). Το παράθυρο [1m] εξομαλύνει σε ένα λεπτό· το [5m] εξομαλύνει περισσότερο (πιο ήρεμη καμπύλη, πιο αργή αντίδραση). Το kit χρησιμοποιεί [5m] στις ειδοποιήσεις του και [1m] εδώ για να βλέπεις κάτι να κινείται. Πρακτικός κανόνας: το παράθυρο πρέπει να περιέχει τουλάχιστον δύο scrapes, άρα εδώ τουλάχιστον [30s]· το rate(http_requetes_total[10s]) επιστρέφει κενό αποτέλεσμα.

P6. Άθροιση ανά διαδρομή

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.

Τι ζητά το ερώτημα: «Πάρε τις ταχύτητες του P5 και άθροισέ τις κρατώντας μόνο το label route

Μία μόνο καινοτομία: η συνάθροιση sum by (route) (…). Λιώνει όλα τα άλλα labels (code, methode, instance…) και αθροίζει ό,τι μένει. Το /cours/{id} περνά από τρεις σειρές (200, 404, 500) σε μία: 3,00 + 0,27 + 0,04 = 3,31. Ισοδύναμο SQL: SELECT route, SUM(vitesse) FROM … GROUP BY route. Οι παρενθέσεις γύρω από το route είναι υποχρεωτικές: το sum by route (…) απορρίπτεται (parse error: unexpected identifier "route" in grouping opts, expected "(").

P7. Άθροιση ανά κωδικό

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

Τι ζητά το ερώτημα: «Το ίδιο άθροισμα με το P6, αλλά ομαδοποιημένο ανά κωδικό HTTP.»

Τίποτα νέο: το P6 με ένα άλλο label. Αυτό κάνει ο πίνακας (panel) «Réponses par code» (Αποκρίσεις ανά κωδικό) του πίνακα ελέγχου «API catalogue — signaux dorés» στο Grafana. Πέντε κωδικοί, πέντε γραμμές· τα 500 στο 0,09 ανά δευτερόλεπτο, δηλαδή λίγο πάνω από ένα σφάλμα κάθε δώδεκα δευτερόλεπτα.

P8. Η συνολική κίνηση

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

Τι ζητά το ερώτημα: «Άθροισε όλες τις ταχύτητες, χωρίς να κρατήσεις κανένα label.»

Μία μόνο καινοτομία: το sum(…) χωρίς by. Αποτέλεσμα: μια μοναδική σειρά, με ένα κενό σύνολο labels ({}), η τιμή 8,96 αιτήματα ανά δευτερόλεπτο. Είναι το πρώτο από τα τέσσερα χρυσά σήματα, η κίνηση. Έλεγξε: το άθροισμα των επτά γραμμών του P6 κάνει πράγματι 8,96.

Απολογισμός P5 έως P8: ξέρεις να μετατρέπεις έναν μετρητή σε ταχύτητα, και μετά να ομαδοποιείς αυτή την ταχύτητα όπως θέλεις. Τα τρία τέταρτα των πινάκων ελέγχου Prometheus δεν κάνουν παρά αυτό.

P9. Το ποσοστό σφαλμάτων

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

Τι ζητά το ερώτημα: «Η ταχύτητα των αποκρίσεων των οποίων ο κωδικός αρχίζει με 5, διαιρεμένη με την ταχύτητα όλων των αποκρίσεων.»

Δύο καινοτομίες, αλλά μικρές. Πρώτα το =~: ένας επιλογέας με κανονική έκφραση, "5.." = ένα 5 ακολουθούμενο από δύο οποιουσδήποτε χαρακτήρες, άρα όλα τα 5xx. Μετά η διαίρεση δύο αποτελεσμάτων: ο Prometheus διαιρεί τις σειρές που έχουν τα ίδια labels, και εδώ οι δύο πλευρές έχουν ένα κενό σύνολο {}, άρα ζευγαρώνουν. Αποτέλεσμα: 0,0099, δηλαδή 1 %· είναι η τιμή που έχει ρυθμιστεί στο /admin/etat (taux_erreurs: 0.01). Δεύτερο χρυσό σήμα, τα σφάλματα. Ο κανόνας ειδοποίησης TauxErreursEleve του kit ενεργοποιείται όταν αυτή η ίδια έκφραση, υπολογισμένη σε 5 λεπτά, ξεπερνά το 0,05.

P10. Η καθυστέρηση p95

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

Τι ζητά το ερώτημα: «Από τα καλάθια του ιστογράμματος διάρκειας, όλες οι διαδρομές μαζί, κάτω από ποια τιμή πέφτει το 95 % των αιτημάτων των 5 τελευταίων λεπτών;»

Μία μόνο καινοτομία: το histogram_quantile(0.95, …). Θέλει ως είσοδο τα καλάθια _bucket αθροισμένα ανά le (γι' αυτό το sum by (le) είναι υποχρεωτικό: χωρίς αυτό, υπολογίζει ένα ποσοστημόριο ανά διαδρομή και το αποτέλεσμα δεν έχει πλέον το νόημα που περίμενες). Αποτέλεσμα: 0,098 δευτερόλεπτα, άρα το 95 % των αιτημάτων σερβίρεται σε λιγότερο από 98 ms. Είναι το τρίτο χρυσό σήμα, η καθυστέρηση, και η μετρική που παρακολουθεί η ειδοποίηση LatenceP95Elevee (κατώφλι: 0,5 s). Κοίτα την κατανομή των καλαθιών του /cours στο σύνολο δεδομένων: 3248 αιτήματα από τα 3279 κάτω από 50 ms, αλλά το /lent (300 έως 900 ms) τραβά το συνολικό p95 προς τα πάνω.

Για να καταλάβεις καλά: τι περιέχει ένα καλάθι ιστογράμματος;

Κάθε γραμμή _bucket{le="0.05"} μετρά τα αιτήματα που πήραν το πολύ 0,05 δευτερόλεπτα (le = less or equal). Τα καλάθια είναι σωρευτικά: το le="0.1" περιέχει επίσης όλα όσα ήταν στο le="0.05". Το τελευταίο, le="+Inf", περιέχει τα πάντα, και ισούται πάντα με _count. Το histogram_quantile ψάχνει το καλάθι όπου η σωρευτική καμπύλη ξεπερνά το 95 % και παρεμβάλλει στο εσωτερικό του. Η ακρίβεια εξαρτάται λοιπόν από την επιλογή των καλαθιών: μεταξύ 0.05 και 0.1, ο Prometheus υποθέτει ομοιόμορφη κατανομή. Γι' αυτό το αποτέλεσμα, 0.0979…, δεν είναι μετρημένη τιμή αλλά εκτίμηση.

P11. Ένα gauge, ως έχει

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

Τι ζητά το ερώτημα: «Η τελευταία τιμή του gauge requetes_en_cours

Τίποτα νέο στη σύνταξη, είναι το P1. Αυτό που είναι νέο είναι ο τύπος: ένα gauge διαβάζεται ως έχει, χωρίς rate(). Μηδέν ή ένα, ανάλογα με τη στιγμή: το API επεξεργάζεται κάθε αίτημα σε λίγα χιλιοστά του δευτερολέπτου, είναι σπάνιο να πιάσεις ένα σε εξέλιξη. Είναι το τέταρτο χρυσό σήμα, ο κορεσμός: αν αυτή η τιμή ανέβαινε στο 50, το API θα ήταν υπερφορτωμένο. Το rate(requetes_en_cours[1m]) δεν προκαλεί σφάλμα, αλλά επιστρέφει έναν αριθμό που δεν σημαίνει τίποτα· ο Prometheus δεν σε προστατεύει από αυτή τη σύγχυση.

P12. Οι ειδοποιήσεις, όπως τις βλέπει ο Prometheus

promql
ALERTS
text
Empty query result

Τι ζητά το ερώτημα: «Οι ειδοποιήσεις που είναι αυτή τη στιγμή pending ή firing

Τίποτα νέο: ένα όνομα μετρικής, όπως στο P1. Η ALERTS είναι, όπως η up, κατασκευασμένη από τον Prometheus: μια σειρά ανά ενεργή ειδοποίηση, με τα labels alertname και alertstate. Σε ένα υγιές εργαστήριο, το αποτέλεσμα είναι κενό: Empty query result. Δεν είναι σφάλμα, είναι η καλύτερη δυνατή απάντηση. Θα το ξαναπληκτρολογήσεις κατά τη βλάβη του παραρτήματος και θα δεις να εμφανίζεται ALERTS{alertname="APIInjoignable", alertstate="pending", …} και μετά alertstate="firing".

Απολογισμός P9 έως P12: τα τέσσερα χρυσά σήματα (κίνηση P8, σφάλματα P9, καθυστέρηση P10, κορεσμός P11) χωρούν σε τέσσερα ερωτήματα, και οι ειδοποιήσεις είναι μια μετρική όπως οι άλλες.

Το μήνυμα που πρέπει να περάσει. Αυτό που κάνει και η SQL: φιλτράρισμα ανά στήλη ({job="api"} = WHERE), ομαδοποίηση και άθροιση (sum by (route) = GROUP BY), διαίρεση δύο συναθροίσεων. Αυτό που κάνει μόνο ο Prometheus: πήγε να πάρει ο ίδιος τα δεδομένα κάθε 15 δευτερόλεπτα από οκτώ υπηρεσίες, μετατρέπει οποιονδήποτε μετρητή σε ταχύτητα με μια συνάρτηση (rate), εκτιμά ένα ποσοστημόριο από καλάθια (histogram_quantile), και εκθέτει τις δικές του ειδοποιήσεις ως μετρική (ALERTS).

Grafana, στο Explore

Εμφάνιση των 8 ερωτημάτων LogQL (G1 έως G8)

Άνοιξε το http://localhost:3000 (χρήστης admin, κωδικός aiopsatlas2026). Στο κύριο μενού (εικονίδιο πάνω αριστερά), κάνε κλικ στο Explore. Στην κορυφή της σελίδας, ο επιλογέας πηγής δεδομένων προτείνει Prometheus, Loki και Alertmanager: διάλεξε Loki. Δεξιά του πεδίου ερωτήματος, δύο τρόποι: Builder (μενού) και Code (πληκτρολογείς). Πέρασε σε Code, επικόλλησε το ερώτημα, μετά Run query (ή Shift+Enter). Οι καταγραφές εμφανίζονται κάτω, η πιο πρόσφατη γραμμή πρώτη. Πάνω δεξιά, ο επιλογέας περιόδου είναι στο Last 1 hour από προεπιλογή: κράτησέ τον. Αυτά τα βήματα είναι ίδια σε Windows και σε Linux, είναι ο περιηγητής που δουλεύει.

Η εικόνα που πρέπει να κρατήσεις στο μυαλό: το Loki είναι μια ντουλάπα ημερολογίων πλοήγησης, ένα ντοσιέ ανά συνδυασμό labels. Δεν διαβάζει το περιεχόμενο των γραμμών για να τις ταξινομήσει, μόνο την ετικέτα του ντοσιέ. Ένα ερώτημα LogQL αρχίζει λοιπόν πάντα με την επιλογή ενός ντοσιέ, μέσα σε άγκιστρα, και μετά ενδεχομένως με το φιλτράρισμα των γραμμών μέσα του.

G1. Ολόκληρο το ημερολόγιο του 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"}

Τι ζητά το ερώτημα: «Όλες οι γραμμές ημερολογίου των οποίων το label service ισούται με api

Μηδέν καινοτομία σε σχέση με το P2: ένας επιλογέας μέσα σε άγκιστρα. Η διαφορά είναι το αποτέλεσμα: γραμμές κειμένου, όχι αριθμοί. Το Grafana εμφανίζει την ώρα (μετατρεπόμενη στη ζώνη ώρας σου) και μετά την ακατέργαστη γραμμή· κάνε κλικ σε μια γραμμή για να ξεδιπλώσεις τα labels της: service="api", conteneur="labo-api", niveau="INFO", code="200", detected_level="info". Εννέα γραμμές ανά δευτερόλεπτο περίπου, όσες ανακοίνωνε το P8: ένα αίτημα, μια γραμμή. Τα άγκιστρα είναι υποχρεωτικά: το service="api" μόνο του απορρίπτεται (parse error at line 1, col 1: syntax error: unexpected IDENTIFIER), και το {service="api" χωρίς κλείσιμο επίσης (syntax error: unexpected $end, expecting } or ,).

Για να καταλάβεις καλά: γιατί το `{service="API"}` δεν επιστρέφει τίποτα

Ένα label είναι μια ακριβής συμβολοσειρά. Το {service="API"} με κεφαλαία δεν επιστρέφει καμία γραμμή, χωρίς σφάλμα: το ντοσιέ δεν υπάρχει. Το ίδιο για το {app="api"}: το label ονομάζεται service σε αυτό το εργαστήριο, όχι app. Όταν ένα ερώτημα LogQL επιστρέφει μηδέν γραμμές, έλεγξε πρώτα το όνομα και τα πεζά/κεφαλαία του label· στο Explore, ο τρόπος Builder σου καταγράφει τα υπάρχοντα labels και τις τιμές τους, είναι ο ασφαλέστερος τρόπος να τα ανακαλύψεις.

G2. Μόνο τα σφάλματα

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

Τι ζητά το ερώτημα: «Οι γραμμές του API των οποίων το label niveau ισούται με ERROR

Μία μόνο καινοτομία: δύο labels στον επιλογέα, χωρισμένα με κόμμα, είναι ένα ΚΑΙ. Το label niveau δεν βρίσκεται στη γραμμή αρχικά: είναι το Alloy που το εξήγαγε από το πεδίο JSON niveau πριν το στείλει στο Loki, και αυτό είναι που κάνει αυτό το ερώτημα γρήγορο. Το κόκκινο νήμα είναι εδώ, τρίτη γραμμή: id_requete: dc1c2ff189a6, η γραμμή του συνόλου δεδομένων. Πολύ λιγότερες γραμμές από ό,τι στο G1: περίπου μία κάθε δώδεκα δευτερόλεπτα, όπως έλεγε το P7 για τα 500.

G3. Ανά κωδικό 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, …}

Τι ζητά το ερώτημα: «Οι γραμμές του API των οποίων το label code ισούται με 500

Τίποτα νέο: το G2 με ένα άλλο label. Οι ίδιες γραμμές με το G2, γιατί σε αυτό το API κάθε 500 είναι ένα ERROR και αντίστροφα. Είναι η εκδοχή ημερολογίου του P4: εκεί που ο Prometheus σου λέει «32 σφάλματα στο /cours», το Loki σου δείχνει ποια, με το πραγματικό URL (/cours/C0019) και το αναγνωριστικό του αιτήματος. Πρόσεξε ότι το code είναι εδώ συμβολοσειρά ("500") γιατί είναι label· στο JSON της γραμμής, είναι αριθμός (500). Το G6 θα σου δείξει τη διαφορά.

G4. Φιλτράρισμα στο κείμενο

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

Τι ζητά το ερώτημα: «Οι γραμμές του API που περιέχουν το κείμενο inscriptions

Μία μόνο καινοτομία: το φίλτρο γραμμής |= "…", που κρατά τις γραμμές που περιέχουν ακριβώς αυτό το κείμενο. Είναι το grep. Αντίθετα με ένα label, το Loki πρέπει εδώ να ανοίξει κάθε γραμμή του ντοσιέ {service="api"} για να κοιτάξει μέσα: πιο αργό, αλλά μπορείς να ψάξεις οτιδήποτε. Οι παραλλαγές: != (δεν περιέχει), |~ (κανονική έκφραση), !~. Μεικτό αποτέλεσμα: 201 και 500, όλα όσα αφορούν τις εγγραφές.

Απολογισμός G1 έως G4: δύο τρόποι φιλτραρίσματος, ανά label (γρήγορο, πριν ανοίξουν οι γραμμές) και ανά κείμενο (ευέλικτο, μετά). Το σωστό ερώτημα αρχίζει πάντα με το στενότερο δυνατό label.

G5. Άνοιγμα του 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" …

Τι ζητά το ερώτημα: «Οι γραμμές του API, και για καθεμία, μετέτρεψε τα πεδία του JSON σε labels.»

Μία μόνο καινοτομία: ο αναλυτής | json. Οι εμφανιζόμενες γραμμές είναι οι ίδιες με το G1, αλλά ξεδίπλωσε μία: έχει τώρα πολύ περισσότερα labels (route, methode, duree_ms, id_requete, message…), ένα ανά πεδίο του JSON. Αυτά τα labels υπολογίζονται τη στιγμή του ερωτήματος, δεν αποθηκεύονται: το Loki ευρετηριάζει πάντα μόνο τα service, conteneur, niveau, code. Θα δεις επίσης code_extracted και niveau_extracted: όταν ένα πεδίο του JSON έχει το ίδιο όνομα με ένα label που έχει ήδη τοποθετήσει το Alloy, το Loki προσθέτει κατάληξη στο αντίγραφο αντί να το αντικαταστήσει.

G6. Φιλτράρισμα σε ένα πεδίο

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

Τι ζητά το ερώτημα: «Οι γραμμές του API των οποίων το πεδίο duree_ms, αφού ανοιχτεί το JSON, ξεπερνά το 500.»

Μία μόνο καινοτομία: το φίλτρο label | duree_ms > 500, που συγκρίνει ένα εξαγόμενο label με έναν αριθμό. Αυτό είναι δυνατό μόνο μετά το | json, αλλιώς το duree_ms δεν υπάρχει. Αποτέλεσμα: μόνο /lent, η επίτηδες αργή διαδρομή (300 έως 900 ms). Είναι η εκδοχή ημερολογίου του P10: ο Prometheus λέει «το p95 είναι στα 98 ms»· το Loki δείχνει τα μεμονωμένα αιτήματα που ξεπέρασαν ένα κατώφλι, με το αναγνωριστικό τους. Ισοδύναμο SQL: WHERE duree_ms > 500, με τη διαφορά ότι η στήλη δεν υπήρχε πριν από το ερώτημα.

G7. Εύρεση ενός συγκεκριμένου αιτήματος

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

Μία μόνο γραμμή.

Τι ζητά το ερώτημα: «Η γραμμή του API που περιέχει το αναγνωριστικό dc1c2ff189a6

Τίποτα νέο: είναι το G4 με ένα άλλο κείμενο. Αυτό που αλλάζει είναι η χρήση: σε σένα, το dc1c2ff189a6 δεν υπάρχει· αντίγραψε ένα id_requete που είδες στη δική σου έξοδο του G2 και επικόλλησέ το στη θέση του. Είναι η κίνηση που θα κάνεις στην παραγωγή: ένας χρήστης σου δίνει το αναγνωριστικό που επιστρέφεται από την κεφαλίδα x-id-requete της απόκρισής του σε σφάλμα, και βρίσκεις με ένα ερώτημα την ακριβή γραμμή, με τη διαδρομή, τον κωδικό και τη διάρκεια. Μία μόνο γραμμή: το αναγνωριστικό είναι μοναδικό ανά αίτημα.

G8. Μέτρηση γραμμών: μια μετρική βγαλμένη από τις καταγραφές

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

Αυτή τη φορά, το Grafana εμφανίζει ένα γράφημα αντί για γραμμές: τρεις καμπύλες, {niveau="INFO"} γύρω στις 470 έως 500 γραμμές ανά λεπτό, {niveau="WARNING"} γύρω στις 40 έως 50, {niveau="ERROR"} μεταξύ 2 και 10, στο εργαστήριο του μαθήματος. Πέρασε το ποντίκι πάνω από το γράφημα για να διαβάσεις τις τιμές.

Τι ζητά το ερώτημα: «Μέτρησε τις γραμμές του API ανά διάστημα ενός λεπτού, και μετά άθροισε κρατώντας το label niveau

Μία μόνο καινοτομία, σε ένα κομμάτι που ήδη γνωρίζεις: το count_over_time(…[1m]) μετρά τις γραμμές ενός επιλογέα σε ένα εύρος, ακριβώς όπως το rate(…[1m]) υπολογίζει μια κλίση στο P5. Γύρω του, το sum by (niveau) είναι το sum by (route) του P6, λέξη προς λέξη. Η LogQL δανείστηκε αυτή τη γραμματική από την PromQL επίτηδες: αυτό που έμαθες από τη μία πλευρά χρησιμεύει από την άλλη. Σύγκρινε με το P7: ο Prometheus μετρά 0,09 απόκριση 500 ανά δευτερόλεπτο, δηλαδή 5 ανά λεπτό· το Loki μετρά 5 γραμμές ERROR ανά λεπτό. Δύο εργαλεία, δύο διαδρομές, ο ίδιος αριθμός.

Η ουσιώδης διαφορά μεταξύ Prometheus και Loki. Ο Prometheus αποθηκεύει αριθμούς που έχει ήδη μετρήσει το API (http_requetes_total), το Loki αποθηκεύει τις γραμμές και μπορεί να τις ξαναμετρήσει κατά παραγγελία (count_over_time). Ο πρώτος είναι ελαφρύς και γρήγορος, και απαντά «πόσα»· ο δεύτερος είναι βαρύς αλλά κρατά τη λεπτομέρεια, και απαντά «ποια». Το εργαστήριο έχει και τα δύο γιατί κανένα δεν αντικαθιστά το άλλο.

Το μήνυμα που πρέπει να περάσει. Αυτό που κάνει και το grep: αναζήτηση ενός κειμένου σε γραμμές (|=). Αυτό που κάνει μόνο το Loki: τακτοποιεί τις γραμμές δέκα containers ανά labels και διαβάζει μόνο το σωστό ντοσιέ, ανοίγει το JSON κατά παραγγελία για να φιλτράρει σε ένα αριθμητικό πεδίο (| json | duree_ms > 500), και μετατρέπει καταγραφές σε καμπύλη με τη γραμματική της PromQL (count_over_time).

Πρόκληση μπόνους (προαιρετική)

Τρία ερωτήματα που συνδυάζουν όσα είδες, χωρίς νέα έννοια. Πληκτρολόγησέ τα, και μετά εξήγησε σε μία φράση τι δείχνει καθένα.

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

Τα πέντε μαθήματα που έλαβαν τις περισσότερες εγγραφές την τελευταία ώρα. Το increase είναι το rate πολλαπλασιασμένο με τη διάρκεια του παραθύρου· το topk(5, …) κρατά τις πέντε μεγαλύτερες σειρές. Στο εργαστήριο του μαθήματος, το C0001 έρχεται πρώτο με περίπου 177 εγγραφές: το charge ευνοεί μερικά «δημοφιλή» μαθήματα.

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

Το P10 με ένα label παραπάνω στο by: ένα p95 ανά διαδρομή. Θα δεις το /lent γύρω στα 0,7 s και τις άλλες διαδρομές κάτω από 0,03 s. Κοίτα τι αλλάζει σε σχέση με το συνολικό p95 του P10.

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

Τα G4, G5 και G6 αλυσιδωτά: μόνο οι επιτυχημένες εγγραφές. Έλεγξε ότι ο αριθμός γραμμών ανά λεπτό αντιστοιχεί στη γραμμή {code="201"} του P7, περίπου 0,87 ανά δευτερόλεπτο, δηλαδή καμιά πενηνταριά ανά λεπτό.

Παράρτημα A — Λεπτομερές βήμα προς βήμα σε Windows (PowerShell)

Εμφάνιση του βήμα προς βήμα για Windows

Όλες οι εντολές πληκτρολογούνται στο PowerShell, από τον φάκελο lab3. Το Docker Desktop πρέπει να είναι εκκινημένο (πράσινο εικονίδιο). Αν το PowerShell αρνείται να εκτελέσει το .\labo.ps1, πληκτρολόγησε μία φορά Set-ExecutionPolicy -Scope CurrentUser RemoteSigned και απάντησε O.

A.0 — Κλωνοποίηση του kit

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

Πρέπει να δεις docker-compose.yml, labo.ps1, labo.sh, README.md, και τους φακέλους alertmanager, alloy, api, charge, grafana, loki, modules, outils, prometheus, webhook. Αν έχεις ήδη κλωνοποιήσει το kit στο μάθημα 03, παράλειψε αυτό το βήμα και κάνε απλώς cd lab3.

A.1 — Έλεγχος των προαπαιτήσεων

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

Σημείο ελέγχου: η τελευταία γραμμή είναι Tout est prêt.. Οι εκδόσεις, η μνήμη και ο αριθμός επεξεργαστών είναι αυτοί του μηχανήματος του μαθήματος. Αν μια θύρα σημειώνεται ✘ … déjà occupé, το μάθημα 03 εξηγεί τι να κάνεις· για την 3000, το $env:GRAFANA_PORT = '3001' αρκεί.

A.2 — Εκκίνηση

powershell
.\labo.ps1 demarrer

Την πρώτη φορά, η λήψη των έξι δημόσιων εικόνων παίρνει ένα έως πέντε λεπτά ανάλογα με τη σύνδεσή σου. Τέλος της αναμενόμενης εξόδου:

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)

Σημείο ελέγχου: δέκα prêt, μετά Le labo est prêt.. Στο μηχάνημα του μαθήματος, με τις εικόνες ήδη σε cache, η εντολή πήρε 44 δευτερόλεπτα. Το μάθημα 04 σχολιάζει αυτή την έξοδο γραμμή προς γραμμή.

A.3 — Ανάγνωση του 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.

Σημείο ελέγχου: δέκα (healthy), 8/8, 64 cours, 0 alerte(s) reçue(s), και η τελευταία γραμμή Labo : 10/10 services, 8/8 cibles up, 0 alertes actives.. Ο αριθμός σειρών στη μνήμη ανεβαίνει τα πρώτα λεπτά (9038 αμέσως μετά την εκκίνηση, 12 000 έως 15 000 μετά από μία ώρα στο μηχάνημα του μαθήματος). Αν το alloy ή το grafana είναι ακόμη (health: starting), περίμενε τριάντα δευτερόλεπτα και ξαναεκτέλεσε.

A.4 — Οι οκτώ στόχοι στον Prometheus

Άνοιξε το http://localhost:9090, μενού Status, μετά Target health. Οκτώ μπλοκ, ένα ανά job, καθένα με 1 / 1 up και μια γραμμή:

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

Σημείο ελέγχου: οκτώ UP, κανένα DOWN. Η στήλη Last scrape δεν ξεπερνά ποτέ τα 15 δευτερόλεπτα: είναι το scrape_interval του prometheus.yml. Στη γραμμή εντολών, η ίδια πληροφορία:

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 — Τα ερωτήματα P1 έως P12 και G1 έως G8

Περίμενε το demarrer να έχει τουλάχιστον δύο λεπτά πίσω του, και μετά ακολούθησε τις ενότητες Prometheus, στην καρτέλα Graph και Grafana, στο Explore παραπάνω. Τα ερωτήματα είναι έτοιμα για αντιγραφή:

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

Σημείο ελέγχου: το P1 επιστρέφει 8 σειρές στο 1, το P12 επιστρέφει Empty query result, το G1 επιστρέφει γραμμές JSON, το G8 επιστρέφει τρεις καμπύλες.

A.6 — Σπάσιμο: σταμάτημα του API

Πριν σπάσεις, σημείωσε την ώρα (Get-Date -Format HH:mm:ss). Μετά:

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

Το σενάριο έκανε ένα docker compose stop api: το container σταμάτησε καθαρά, τα δεδομένα και η εικόνα του είναι άθικτα. Τώρα, παρατήρησε τη βλάβη από πέντε διαδρομές, με τη σειρά. Έχεις περίπου 70 δευτερόλεπτα πριν φτάσει η ειδοποίηση στο webhook: εκτέλεσε το etat αμέσως.

Διαδρομή 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.

Τι πρέπει να διαβάσεις, από πάνω προς τα κάτω: το labo-api είναι Exited (0) (κωδικός 0: εκούσιο σταμάτημα, όχι κατάρρευση)· ο Prometheus διαβάζει πλέον μόνο 7/8 στόχους και σου λέει γιατί (lookup api … no such host: το όνομα api δεν υπάρχει πλέον στο δίκτυο Docker αφού το container είναι σταματημένο)· η ειδοποίηση APIInjoignable είναι ενεργή· το API δεν απαντά στη θύρα 8000· το webhook έλαβε κάτι. Αυτή η έξοδος καταγράφηκε στο μηχάνημα του μαθήματος ένα λεπτό μετά το casser api, ενώ ένα casser erreurs είχε μόλις παιχτεί λίγα λεπτά πριν: γι' αυτό εμφανίζεται επίσης μια δεύτερη ειδοποίηση, TauxErreursEleve. Στο δικό σου εργαστήριο, θα έχεις μόνο την APIInjoignable, 1 alertes actives και 1 alerte(s) reçue(s). Αν εκτελέσεις το etat στα πρώτα 30 δευτερόλεπτα, η ειδοποίηση είναι ακόμη en attente (pending) και το webhook είναι ακόμη στο 0: ξαναεκτέλεσε ένα λεπτό αργότερα.

Διαδρομή 2, οι στόχοι. Επαναφόρτωσε το http://localhost:9090StatusTarget health. Το μπλοκ api πέρασε σε 0 / 1 up, κατάσταση DOWN, και η στήλη Error φέρει το ίδιο μήνυμα με το etat: Get "http://api:8000/metrics": dial tcp: lookup api on 127.0.0.11:53: no such host. Οι επτά άλλοι μένουν UP. Ξαναπληκτρολόγησε up στο Query: η γραμμή up{instance="api:8000", job="api", service="api"} είναι στο 0, οι επτά άλλες στο 1. Μετά up == 0: μία μόνο γραμμή.

Διαδρομή 3, οι ειδοποιήσεις στον Prometheus. Μενού Alerts. Ο κανόνας APIInjoignable αλλάζει κατάσταση σε τρεις χρόνους, χρονομετρημένους στο μηχάνημα του μαθήματος:

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)

Γιατί 40 δευτερόλεπτα πριν από το pending: ο Prometheus διαβάζει το API κάθε 15 δευτερόλεπτα, άρα χρειάζονται έως 15 δευτερόλεπτα για να αποτύχει ένα scrape, και μετά αξιολογεί τους κανόνες κάθε 15 δευτερόλεπτα. Γιατί 30 ακόμη πριν από το firing: ο κανόνας λέει for: 30s. Σε μια άλλη καταγραφή, το pending έφτασε στα 31 s και το firing στα 61 s: η τάξη μεγέθους είναι η ίδια, η λεπτομέρεια εξαρτάται από τη στιγμή που έσπασες σε σχέση με τον κύκλο scrape. Ξαναπληκτρολόγησε ALERTS στο Query:

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

και μετά, τριάντα δευτερόλεπτα αργότερα, alertstate="firing". Σημείωσε την ώρα του περάσματος σε firing: είναι η πρώτη από τις τρεις γραμμές του παραδοτέου σου.

Διαδρομή 4, Alertmanager. Άνοιξε το http://localhost:9093. Η σελίδα Alerts δείχνει μια ομάδα alertname="APIInjoignable" service="api" (είναι το group_by: [alertname, service] του alertmanager.yml) με την ειδοποίηση, τα labels της (instance="api:8000", job="api", labo="observabilite", severite="critique"), την περίληψή της L'API catalogue ne répond plus και την περιγραφή της. Το label labo="observabilite" δεν ήταν στον κανόνα: είναι το external_labels του prometheus.yml, που προστίθεται σε όλα όσα βγαίνουν από τον Prometheus. Στη γραμμή εντολών:

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

Διαδρομή 5, το webhook. Άνοιξε το http://localhost:8090. Η σελίδα «Alertes reçues d'Alertmanager» (Ειδοποιήσεις που λήφθηκαν από τον Alertmanager) δεν είναι πλέον κενή: μια γραμμή APIInjoignable · critique · firing · api · L'API catalogue ne répond plus, και η κεφαλίδα μετρά 1 alerte(s) en mémoire · 1 notification(s) reçue(s). Η ακατέργαστη μορφή, http://localhost:8090/alertes.json, δείχνει τι έστειλε ο Alertmanager:

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, η ώρα του firing στον Prometheus) και το recu_a (19:41:26): δεκαπέντε δευτερόλεπτα διαφορά, εκ των οποίων τα 10 δευτερόλεπτα του group_wait του Alertmanager. Το fin στο έτος 0001 σημαίνει «δεν έχει τελειώσει ακόμη». Σημείωσε το recu_a: δεύτερη γραμμή του παραδοτέου σου.

Τι βλέπει το charge, στο μεταξύ:

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

Και τι βλέπεις αν καλέσεις το API εσύ ο ίδιος:

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

(Χωρίς -TimeoutSec, το PowerShell περιμένει περισσότερο πριν παραιτηθεί· το curl.exe -sS http://localhost:8000/sante απαντά πιο γρήγορα: curl: (7) Failed to connect to localhost:8000 after 2237 ms: Could not connect to server.)

Η ουσιώδης διαφορά μεταξύ ενός 404 και καμίας απόκρισης. Στο βήμα A.8, το /cours/C9999 θα απαντήσει 404: το API τρέχει και σου λέει ευγενικά ότι αυτό το μάθημα δεν υπάρχει· μετριέται στο http_requetes_total{code="404"}, γράφεται σε ένα log WARNING, και το up μένει στο 1. Εδώ, Le délai de l'opération a expiré (η λειτουργία έληξε): κανείς δεν απαντά, δεν υπάρχει ούτε κωδικός ούτε log από την πλευρά του API, και είναι το up που πέφτει στο 0. Δύο καταστάσεις, δύο σήματα, δύο σημεία όπου πρέπει να ψάξεις.

A.7 — Επιδιόρθωση

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).

Το σενάριο έκανε docker compose start api, περίμενε το /sante να απαντήσει, και μετά κάλεσε το /admin/reparer (χρήσιμο για τα δύο άλλα σενάρια βλάβης). Έλεγξε ότι το API δουλεύει:

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

Μετά κοίτα την ειδοποίηση να σβήνει, με την ίδια σειρά που άναψε. Χρονομετρημένο στο μηχάνημα του μαθήματος μετά το 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

Στο πρώτο επιτυχημένο scrape, το up{job="api"} επανέρχεται στο 1 και ο κανόνας ξαναπερνά σε inactive· ο Alertmanager στέλνει τότε μια γνωστοποίηση resolved στο webhook. Επαναφόρτωσε το http://localhost:8090: δύο γραμμές τώρα για την APIInjoignable, μία firing και μία resolved, και στο /alertes.json η δεύτερη έχει ένα πεδίο fin συμπληρωμένο:

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 μείον debut: η βλάβη διήρκεσε 45 δευτερόλεπτα στα μάτια του Prometheus. Σημείωσε το recu_a της γραμμής resolved: τρίτη γραμμή του παραδοτέου σου.

A.8 — Πρόκληση ενός 404, για σύγκριση

Το API τρέχει. Ζήτησέ του ένα μάθημα που δεν υπάρχει:

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

Είναι ένα σφάλμα HTTP 404: το API απάντησε. Για να δεις τον ίδιο τον κωδικό:

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

Ξαναπληκτρολόγησε το P4 στον Prometheus αντικαθιστώντας το 500 με 404: η σειρά route="/cours/{id}" αυξήθηκε κατά 1. Ξαναπληκτρολόγησε το G1 προσθέτοντας |= "C9999": το αίτημά σου είναι εκεί, επίπεδο WARNING, με το id_requete του. Τίποτα από όλα αυτά δεν υπάρχει για τη βλάβη του A.6: ένα σταματημένο API δεν μετρά τίποτα και δεν γράφει τίποτα.

A.9 — Τελικός έλεγχος

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.

Τι πρέπει να έχεις: το labo-api ξανά Up … (healthy) με μικρότερο χρόνο από τα άλλα (μόλις επανεκκίνησε), 8/8, 0 active(s), 64 cours, 2 alerte(s) reçue(s)firing και η resolved), και η τελευταία γραμμή Labo : 10/10 services, 8/8 cibles up, 0 alertes actives.. Στο μηχάνημα του μαθήματος, αυτή η έξοδος έφερε ακόμη 1 alertes actives και 3 alerte(s) reçue(s) λόγω του υπολειμματικού TauxErreursEleve που αναφέρθηκε στο A.6· σε σένα, με τη μόνη βλάβη api, οι τιμές είναι οι παραπάνω. Αυτή η έξοδος και οι τρεις σημειωμένες ώρες σου είναι το παραδοτέο σου. Μπορείς να αφήσεις το εργαστήριο να τρέχει για τα εργαστήρια 06 και 07, ή να το σταματήσεις με .\labo.ps1 arreter: τα δεδομένα διατηρούνται και το demarrer συνεχίζει από εκεί που ήσουν.

Παράρτημα B — Λεπτομερές βήμα προς βήμα σε Linux, macOS, WSL 2 και Git Bash

Εμφάνιση του βήμα προς βήμα για Linux, macOS, WSL 2 και Git Bash

Όλες οι εντολές πληκτρολογούνται σε ένα τερματικό bash, από τον φάκελο lab3. Σε macOS και Windows (WSL 2 ή Git Bash), το Docker Desktop πρέπει να είναι εκκινημένο· σε εγγενές Linux, το docker info πρέπει να απαντά χωρίς sudo (αλλιώς sudo usermod -aG docker $USER, και μετά άνοιξε μια νέα συνεδρία). Το σενάριο bash χρειάζεται το curl.

B.0 — Κλωνοποίηση του kit

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

Πρέπει να δεις docker-compose.yml, labo.sh, labo.ps1, README.md, και τους φακέλους alertmanager, alloy, api, charge, grafana, loki, modules, outils, prometheus, webhook. Σε Linux και macOS, κάνε το σενάριο εκτελέσιμο μία φορά: chmod +x labo.sh (χωρίς αυτό, το ./labo.sh απαντά bash: ./labo.sh: Permission denied). Αν έχεις ήδη κλωνοποιήσει το kit στο μάθημα 03, παράλειψε αυτό το βήμα και κάνε απλώς cd lab3.

B.1 — Έλεγχος των προαπαιτήσεων

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

Σημείο ελέγχου: η τελευταία γραμμή είναι Tout est prêt.. Το σενάριο bash ελέγχει μια γραμμή παραπάνω από το PowerShell, curl : présent. Οι εκδόσεις και η μνήμη είναι αυτές του μηχανήματος του μαθήματος (Git Bash σε Windows). Αν μια θύρα είναι κατειλημμένη, το μάθημα 03 εξηγεί τι να κάνεις· για την 3000, GRAFANA_PORT=3001 ./labo.sh demarrer.

B.2 — Εκκίνηση

bash
./labo.sh demarrer

Τέλος της αναμενόμενης εξόδου, μόλις κατέβουν οι εικόνες:

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)

Σημείο ελέγχου: δέκα prêt, μετά Le labo est prêt..

B.3 — Ανάγνωση του 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.

Σημείο ελέγχου: δέκα (healthy), 8/8, 64 cours, και η τελευταία γραμμή Labo : 10/10 services, 8/8 cibles up, 0 alertes actives.. Το μπλοκ Conteneurs προέρχεται από το docker compose ps· μπορείς να το πληκτρολογήσεις εσύ ο ίδιος. Τρίτη διαδρομή, σε 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 — Οι οκτώ στόχοι στον Prometheus

Στον περιηγητή, http://localhost:9090StatusTarget health: οκτώ μπλοκ 1 / 1 up, κατάσταση UP, στήλη Last scrape πάντα κάτω από 15 δευτερόλεπτα. Σε curl, με python3 για να διαβάσεις το JSON (ή jq αν το έχεις):

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 — Τα ερωτήματα P1 έως P12 και G1 έως G8

Περίμενε δύο λεπτά μετά το demarrer, και μετά ακολούθησε τις ενότητες Prometheus, στην καρτέλα Graph και Grafana, στο Explore παραπάνω· γίνονται στον περιηγητή, με τον ίδιο τρόπο σε όλα τα συστήματα. Τα ερωτήματα είναι έτοιμα για αντιγραφή:

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

Τρίτη διαδρομή, ένα ερώτημα PromQL σε curl:

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

Αναγνωρίζεις στο JSON τις ίδιες σειρές με την καρτέλα Table: "metric": {"__name__": "up", "instance": "localhost:9090", "job": "prometheus"} και "value": [1789500809.696, "1"].

Σημείο ελέγχου: το P1 επιστρέφει 8 σειρές στο 1, το P12 επιστρέφει Empty query result, το G1 επιστρέφει γραμμές JSON, το G8 επιστρέφει τρεις καμπύλες.

B.6 — Σπάσιμο: σταμάτημα του API

Σημείωσε την ώρα (date +%T), μετά:

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

Το σενάριο έκανε docker compose stop api. Έχεις περίπου 70 δευτερόλεπτα πριν φτάσει η ειδοποίηση στο webhook. Παρατήρησε από πέντε διαδρομές.

Διαδρομή 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.

Τι πρέπει να διαβάσεις: το labo-api είναι Exited (0) (εκούσιο σταμάτημα)· ο Prometheus διαβάζει πλέον μόνο 7/8 στόχους και λέει γιατί (lookup api … no such host: το όνομα api εξαφανίστηκε από το δίκτυο Docker)· η APIInjoignable είναι ενεργή· το API δεν απαντά· το webhook έλαβε την ειδοποίηση. Αυτή η έξοδος καταγράφηκε ένα λεπτό μετά το casser api, σε ένα μηχάνημα όπου το casser erreurs είχε μόλις παιχτεί: εξ ου και η δεύτερη ειδοποίηση TauxErreursEleve. Σε σένα: 1 alertes actives, 1 alerte(s) reçue(s). Αν εκτελέσεις το etat στα πρώτα 30 δευτερόλεπτα, η ειδοποίηση είναι ακόμη pending και το webhook στο 0: ξαναεκτέλεσε ένα λεπτό αργότερα. Τρίτη διαδρομή, σε 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

Διαδρομή 2, οι στόχοι. Επαναφόρτωσε το http://localhost:9090StatusTarget health: το μπλοκ api είναι στο 0 / 1 up, κατάσταση DOWN, στήλη Error: Get "http://api:8000/metrics": dial tcp: lookup api on 127.0.0.11:53: no such host. Ξαναπληκτρολόγησε up στο Query: το up{instance="api:8000", job="api", service="api"} είναι στο 0. Μετά up == 0: μία μόνο γραμμή.

Διαδρομή 3, οι ειδοποιήσεις στον Prometheus. Μενού Alerts. Η APIInjoignable αλλάζει κατάσταση σε τρεις χρόνους, χρονομετρημένους στο μηχάνημα του μαθήματος:

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)

Γιατί 40 δευτερόλεπτα πριν από το pending: έως 15 δευτερόλεπτα για να αποτύχει ένα scrape (scrape_interval: 15s), και μετά έως 15 δευτερόλεπτα για την επόμενη αξιολόγηση των κανόνων (evaluation_interval: 15s). Γιατί 30 ακόμη: for: 30s στο alertes.yml. Ξαναπληκτρολόγησε ALERTS στο Query:

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

και μετά alertstate="firing". Σημείωσε την ώρα του firing: πρώτη γραμμή του παραδοτέου σου. Σε curl, το ίδιο:

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

Διαδρομή 4, Alertmanager. Άνοιξε το http://localhost:9093: η σελίδα Alerts δείχνει μια ομάδα alertname="APIInjoignable" service="api" (το group_by του alertmanager.yml) με την ειδοποίηση, τα labels της (instance="api:8000", job="api", labo="observabilite", severite="critique"), την περίληψη L'API catalogue ne répond plus. Το label labo="observabilite" προέρχεται από τα external_labels του prometheus.yml. Σε 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

Διαδρομή 5, το webhook. Άνοιξε το http://localhost:8090: μια γραμμή APIInjoignable · critique · firing · api · L'API catalogue ne répond plus, κεφαλίδα 1 alerte(s) en mémoire · 1 notification(s) reçue(s). Η ακατέργαστη μορφή:

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) είναι η ώρα του firing στον Prometheus· το recu_a (19:41:26) φτάνει δεκαπέντε δευτερόλεπτα αργότερα, εκ των οποίων τα 10 δευτερόλεπτα του group_wait. Το fin στο έτος 0001: δεν έχει τελειώσει ακόμη. Σημείωσε το recu_a: δεύτερη γραμμή του παραδοτέου σου.

Τι βλέπει το charge:

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

Η ουσιώδης διαφορά μεταξύ ενός 404 και καμίας απόκρισης. Στο βήμα B.8, το /cours/C9999 θα απαντήσει 404: το API τρέχει και σου λέει ότι αυτό το μάθημα δεν υπάρχει· μετριέται στο http_requetes_total{code="404"}, γράφεται σε ένα log WARNING, και το up μένει στο 1. Εδώ, curl: (7) Failed to connect: κανείς δεν απαντά, δεν υπάρχει ούτε κωδικός ούτε log από την πλευρά του API, και είναι το up που πέφτει στο 0.

B.7 — Επιδιόρθωση

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).

Το σενάριο έκανε docker compose start api, περίμενε το /sante, και μετά κάλεσε το /admin/reparer. Έλεγξε ότι το API δουλεύει:

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

Μετά η ειδοποίηση σβήνει, χρονομετρημένο στο μηχάνημα του μαθήματος μετά το 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

Τρίτη διαδρομή, επιτήρηση σε βρόχο:

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

Η γραμμή resolved φέρει ένα πεδίο fin συμπληρωμένο (2026-09-15T19:41:56.496Z): 45 δευτερόλεπτα βλάβης στα μάτια του Prometheus. Σημείωσε το recu_a της: τρίτη γραμμή του παραδοτέου σου. Ctrl+C για να βγεις από το watch.

B.8 — Πρόκληση ενός 404, για σύγκριση

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

Το API απάντησε: έναν κωδικό 404, ένα αναγνωριστικό x-id-requete, ένα σώμα JSON. Ξαναπληκτρολόγησε το P4 στον Prometheus με 404 στη θέση του 500: η σειρά route="/cours/{id}" αυξήθηκε κατά 1. Ξαναπληκτρολόγησε το G1 προσθέτοντας |= "bbc7be667f1d" (το δικό σου αναγνωριστικό, διαβασμένο στην κεφαλίδα): το αίτημά σου είναι εκεί, επίπεδο WARNING. Τίποτα από όλα αυτά δεν υπάρχει για τη βλάβη του B.6: ένα σταματημένο API δεν μετρά τίποτα και δεν γράφει τίποτα.

B.9 — Τελικός έλεγχος

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.

Τι πρέπει να έχεις: το labo-api ξανά Up … (healthy), 8/8, 0 active(s), 64 cours, 2 alerte(s) reçue(s)firing και η resolved), και Labo : 10/10 services, 8/8 cibles up, 0 alertes actives.. Στο μηχάνημα του μαθήματος, αυτή η έξοδος έφερε ακόμη 1 alertes actives και 3 alerte(s) reçue(s) λόγω του υπολειμματικού TauxErreursEleve που αναφέρθηκε στο B.6· σε σένα, με τη μόνη βλάβη api, οι τιμές είναι οι παραπάνω. Αυτή η έξοδος και οι τρεις σημειωμένες ώρες σου είναι το παραδοτέο σου. Μπορείς να το αφήσεις να τρέχει για τα εργαστήρια 06 και 07, ή ./labo.sh arreter: τα δεδομένα διατηρούνται.

Παράρτημα C — Αν κολλήσει (όλα τα συστήματα)

Εμφάνιση των περιπτώσεων όπου κάτι κολλάει

Το etat λέει 9/10 services ενώ δεν έσπασες τίποτα. Κοίτα ποιο container δεν είναι (healthy). Αν είναι το grafana ή το alloy αμέσως μετά το demarrer με (health: starting), περίμενε τριάντα δευτερόλεπτα. Αν είναι το labo-api σε Exited, κάποιος (ίσως εσύ, σε προηγούμενη δοκιμή) εκτέλεσε casser api: reparer. Αν ένα container είναι Restarting, διάβασε το ημερολόγιό του: .\labo.ps1 journal <service> ή ./labo.sh journal <service>.

cibles up : 7/8 και APIInjoignable ενεργή, αλλά το API απαντά στο http://localhost:8000. Ο Prometheus διαβάζει το API μέσα από το δίκτυο Docker (http://api:8000/metrics), εσύ μέσα από τη δημοσιευμένη θύρα (localhost:8000). Αν το API μόλις επανεκκίνησε, ο Prometheus μπορεί να έχει καθυστέρηση ενός scrape (15 s) και η ειδοποίηση μένει firing έως την επόμενη αξιολόγηση, και μετά λίγα δευτερόλεπτα ακόμη για να λάβει το webhook το resolved. Περίμενε ένα λεπτό και ξαναεκτέλεσε το etat.

Οι ειδοποιήσεις αργούν: το pending δεν περνά σε firing. Η APIInjoignable έχει for: 30s· χρειάζονται λοιπόν έως 15 s (scrape) + 15 s (αξιολόγηση) + 30 s (for) = 60 έως 70 s για το firing, και μετά 10 s group_wait για το webhook. Δεν είναι αργό, είναι ρυθμισμένο για να αποφεύγονται ψευδείς ειδοποιήσεις σε μια μεμονωμένη αποτυχία. Αν μετά από δύο λεπτά τίποτα δεν κινείται, έλεγξε ότι το labo-api είναι πράγματι Exited (docker compose ps).

Το webhook μένει στο 0 alerte(s) reçue(s) ενώ ο Alertmanager δείχνει την ειδοποίηση. Άνοιξε το http://localhost:9093Status: η ενότητα Config πρέπει να δείχνει receiver: webhook και url: http://webhook:8090/alertes. Μετά journal webhook: πρέπει να δεις μια γραμμή POST /alertes σε κάθε γνωστοποίηση. Αν το container labo-webhook δεν είναι healthy, docker compose restart webhook.

Empty query result στο P3, P5 ή G1, αμέσως μετά το demarrer. Ο Prometheus χρειάζεται τουλάχιστον ένα scrape (15 s) για το P3, δύο για το P5 (το rate θέλει δύο σημεία στο [1m]), και το Alloy χρειάζεται λίγα δευτερόλεπτα για να στείλει την πρώτη γραμμή στο Loki. Περίμενε δύο λεπτά μετά το Le labo est prêt.. Αν το {service="api"} μένει κενό μετά από πέντε λεπτά, έλεγξε το http://localhost:12345 (το Alloy πρέπει να είναι ready και τα συστατικά του Healthy) και journal alloy.

Το P10 επιστρέφει NaN. Το histogram_quantile επιστρέφει NaN όταν το παράθυρο [5m] δεν περιέχει ακόμη αρκετά σημεία. Περίμενε πέντε λεπτά μετά την εκκίνηση, ή αντικατάστησε το [5m] με [1m] για να δεις μια τιμή νωρίτερα (λιγότερο σταθερή).

Το G1 επιστρέφει μηδέν γραμμές ενώ το API τρέχει. Έλεγξε πρώτα την περίοδο (πάνω δεξιά, Last 1 hour) και το όνομα του label (service, με πεζά). Μετά το http://localhost:12345: το Alloy πρέπει να απαντά Alloy is ready. και το journal alloy δεν πρέπει να δείχνει επαναλαμβανόμενο σφάλμα. Ως έσχατη λύση, docker compose restart alloy.

parse error στον Prometheus. Τα τρία πιο συχνά, όλα ορατά σε αυτή τη σελίδα: unexpected identifier "api" in label matching, expected string (ξεχασμένα εισαγωγικά: {job=api}unexpected character inside braces: '5' ({code=500} αντί για {code="500"}expected type range vector in call to function "rate", got instant vector (ξεχασμένο παράθυρο [1m]).

parse error στο Loki. syntax error: unexpected IDENTIFIER: ξέχασες τα άγκιστρα (service="api" αντί για {service="api"}). unexpected $end, expecting } or ,: λείπει το άγκιστρο κλεισίματος. Μηδέν γραμμές χωρίς σφάλμα: όνομα ή πεζά/κεφαλαία του label ({service="API"}, {app="api"}).

Μόνο Windows — το Invoke-RestMethod εμφανίζει περίεργους χαρακτήρες (é) στους τίτλους των μαθημάτων. Είναι η εμφάνιση της κονσόλας, όχι το API. [Console]::OutputEncoding = [Text.Encoding]::UTF8 πριν από την εντολή, ή διάβασε στον περιηγητή.

Μόνο Windows — .\labo.ps1: «l'exécution de scripts est désactivée sur ce système» (η εκτέλεση σεναρίων είναι απενεργοποιημένη σε αυτό το σύστημα). Set-ExecutionPolicy -Scope CurrentUser RemoteSigned, απάντησε O, ξαναεκτέλεσε.

Μόνο Windows — το demarrer αποτυγχάνει με port is already allocated στην 3000. Ένα άλλο πρόγραμμα ακούει ήδη (συχνά μια εφαρμογή Node). $env:GRAFANA_PORT = '3001' και μετά ξαναεκτέλεσε το demarrer· το Grafana είναι τότε στο http://localhost:3001 και το etat το εμφανίζει έτσι.

Μόνο εγγενές Linux — permission denied while trying to connect to the Docker daemon socket. sudo usermod -aG docker $USER, κλείσε τη συνεδρία, ξανάνοιξέ την, το docker info πρέπει να απαντά.

Εγγενές Linux και macOS — bash: ./labo.sh: Permission denied. Το αρχείο είναι αποθηκευμένο χωρίς το bit εκτέλεσης στο αποθετήριο: chmod +x labo.sh μία φορά, ή εκτέλεσε bash labo.sh prerequis. Σε Git Bash (Windows), το ερώτημα δεν τίθεται.

Θέλεις να ξεκινήσεις από το μηδέν. Το .\labo.ps1 reinitialiser ή ./labo.sh reinitialiser διαγράφει τα containers και τους τόμους: ο Prometheus, το Loki και το Grafana ξεκινούν κενά. Μετά demarrer. Να μην το κάνεις σε ένα εργαστήριο που μοιράζεσαι με άλλους.