تدريب موجه — بدء المختبر والتحقق والكسر والإصلاح

تدريب موجَّه45 دقيقة
المدة
من 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، المشترك، يجمع الحالات التي تحدث فيها مشكلة. جميع مخرجات هذه الصفحة التُقطت على مختبر الدورة ؛ القيم التي تعتمد على اللحظة (العدادات والمدد والطوابع الزمنية) ستكون مختلفة عندك، لكن الأشكال ستكون متطابقة.

الهدف

تنضم إلى الفريق الذي يشغّل كتالوج دورات منصة عبر الإنترنت. تمدّ لك رئيستك حقيبة المختبر : « غداً صباحاً، أريد حزمة قابلية المراقبة تعمل على جهازك، وAPI داخلها، والدليل على أنك تعرف قراءة عطل دون الاتصال بي. » ستبدأ إذن تشغيل الخدمات العشر، وتثبت أن Prometheus يقرأ أهدافه الثمانية فعلاً وأن Loki يستقبل سجلات API، وتكتب اثني عشر استعلام PromQL وثمانية استعلامات LogQL لتتعلم قراءة ما يقيسه المختبر، ثم توقف API عمداً. ستشاهد العطل ينتشر : يراه etat في ثانيتين، ويضع Prometheus الهدف في DOWN، وينتقل التنبيه APIInjoignable من pending إلى firing، ويرسله Alertmanager إلى webhook. ثم تصلح وتشاهد التنبيه ينطفئ. التعرف على « هذه الخدمة متوقفة » في عشر ثوانٍ، ومعرفة أين تبحث عنها، هو ما يوفر ساعات من البحث في المكان الخطأ.

الخطوات التسع في هذا المخطط مفصّلة، مع المخرج المتوقع لكل أمر، في الملحق A (Windows) أو الملحق B (Linux وmacOS) في أسفل الصفحة.

باختصار : أوامر المختبر

عرض الأوامر

حقيبة المختبر : https://github.com/hrhouma2/aiopsatlas-observabilite-labo-fr

تستنسخ الحقيبة في مجلد lab3، وتتحقق من أن Docker جاهز، وتبدأ تشغيل الخدمات العشر، وتفتح صفحات الويب، وتكتب الاستعلامات، ثم تكسر وتصلح. في النهاية، يجب أن يعرض 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 في عناوين 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 في عناوين Grafana.

مجموعة البيانات : ما ستتعامل معه

عرض مجموعة البيانات

قبل كتابة استعلام واحد، انظر إلى ما يراقبه المختبر. كل شيء يدور حول API لكتالوج دورات : خدمة ويب صغيرة مكتوبة بلغة Python (FastAPI) تقدّم 64 دورة وتسجّل التسجيلات. خدمة ثانية، charge، تلعب دور المستخدمين : تستدعي API باستمرار، بطلبات ناجحة، وبضعة 404 متعمدة، ومرة من كل مئة، خطأ 500 يصنعه API بنفسه. كل ما ستقرؤه في Prometheus وفي Loki يأتي من هاتين الخدمتين. بقية الحقيبة، بوضوح :

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 /inscriptions201 (أو 404 إذا لم تكن الدورة موجودة، و422 إذا كان المحتوى غير صالح)يستدعيها charge ؛ تزيد inscriptions_total{cours_id="…"}
GET /lent{"attente_ms":305}مسار بطيء عمداً (من 300 إلى 900 مللي ثانية) لتغذية المدرج التكراري للكمون
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 من سطر السجل المكتوب لهذا الطلب. سيفيدك في الاستعلام 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)
gauge (مقياس لحظي)requetes_en_coursيرتفع وينخفض : طلب واحد قيد المعالجة في لحظة القراءة. يُقرأ كما هو (P11)
histogram (مدرج تكراري)http_duree_requete_secondsسلال تراكمية : 3248 طلباً على /cours استغرقت أقل من 50 مللي ثانية (le="0.05")، و3279 في المجموع (+Inf = _count). _sum / _count = المدة المتوسطة (هنا 26 مللي ثانية). يستخرج P10 منها p95
info (مقياس لحظي عند 1)api_infoالقيمة دائماً 1 ؛ المعلومة في التسمية version="1.0.0"

لا يوجد summary في هذا الـ API : ينصح Prometheus بعدم استخدامه لصالح المدرج التكراري، الذي يُجمَّع بين المثيلات.

التسميات : أعمدة جداولك المستقبلية

السلسلة هي اسم مقياس بالإضافة إلى مجموعة أزواج label="valeur". مصدران :

التسميةيضعهاالقيم على المختبر
methodeAPIGET وPOST
routeAPI/sante و/cours و/cours/{id} و/inscriptions و/lent و/admin/etat وinconnue (أي عنوان غير موجود)
codeAPI200 و201 و404 و422 و500
leAPI، على المدرج التكراري فقط0.005 و0.01 و0.025 و0.05 و0.1 و0.25 و0.5 و1.0 و2.0 و+Inf
cours_idAPI، على inscriptions_total وcours_consultes_totalمن C0001 إلى C0064
jobPrometheus، وفقاً لـ prometheus.ymlprometheus وapi وnode-exporter وcadvisor وalertmanager وgrafana وloki وalloy
instancePrometheusالعنوان المقروء : api:8000 وlocalhost:9090 وgrafana:3000
servicePrometheus، مضافة يدوياً في prometheus.yml للمهمة apiapi

لاحظ المسار /cours/{id} : يوحّد API العنوان قبل العدّ. /cours/C0001 و/cours/C0043 يقعان في السلسلة نفسها. بدون ذلك، ستكون هناك 64 سلسلة لكل رمز بدلاً من واحدة، و64 ضعفاً من الأسطر في /metrics.

السجلات : سطر JSON لكل طلب

يكتب API سطر سجل لكل طلب يعالجه، بصيغة JSON. يقرأ Alloy مخرج كل حاوية 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القيم نفسها التي في تسميات http_requetes_total : هذا هو الجسر بين المقاييس والسجلات
duree_ms0.1مدة المعالجة، بالمللي ثانية
messageGET /cours/C0019 -> 500الجملة المقروءة، مع العنوان الحقيقي هذه المرة (C0019، وليس {id})

لا يقرأ Loki الـ JSON سطراً بسطر عند الاستعلام، إلا إذا طلبت منه ذلك (| json، الاستعلام G5). ما يفهرسه هو تسميات يضعها Alloy عند الوصول :

تسمية Lokiالقيميضعها
serviceapi وcharge وwebhook وprometheus وalertmanager وgrafana وloki وalloy وnode-exporter وcadvisorAlloy، وفقاً لاسم خدمة Compose
conteneurlabo-api وlabo-chargeAlloy، وفقاً لاسم الحاوية
niveauINFO وWARNING وERRORAlloy، مستخرجة من الحقل niveau في الـ JSON
code200 و201 و404 و422 و500Alloy، مستخرجة من الحقل code في الـ JSON
detected_levelinfo وwarn وerrorLoki نفسه، الذي يخمّن المستوى ؛ يمكنك تجاهلها

الخيط الناظم

احتفظ بشيئين ستجدهما في كل مكان : السلسلة http_requetes_total{code="500",route="/cours"}، بقيمة 32 على /metrics لحظة الالتقاط، والتي ستراها مجدداً بقيمة 32 في P4 ؛ والمعرّف dc1c2ff189a6، معرّف سطر الخطأ أعلاه، الذي ستجده في G2 ثم ستبحث عنه بنفسك في G7. المقاييس تعدّ، والسجلات تروي ؛ وكلاهما يتحدث عن الطلب نفسه.

الاستعلامات الأولى : مفهوم واحد في كل مرة

يحتوي القسمان التاليان على عشرين استعلاماً : اثنا عشر لـ Prometheus (من P1 إلى P12)، وثمانية لـ Loki عبر Grafana Explore (من G1 إلى G8). إنها مرتبة من الأبسط إلى الأكثر تعبيراً، وكل واحد منها لا يضيف سوى جديد واحد مقارنة بسابقه. إذا بدا لك استعلام غامضاً، فذلك يعني في الغالب أن السابق لم يتضح بعد : عد إلى الوراء بدلاً من المتابعة.

اكتب كل استعلام بنفسك، وقارن النتيجة بنتيجة الصفحة، واقرأ الشرح، ثم انتقل إلى التالي. ستكون الأرقام مختلفة عندك : ترتفع العدادات منذ بدء تشغيل API الخاص بك، لا API الدورة. الأشكال (عدد السلاسل، والتسميات، ورتبة المقدار) يجب أن تكون نفسها. يستخدم Prometheus وLoki فكرة الانطلاق نفسها، مجموعة تسميات بين أقواس معقوفة، وهذا مقصود : ما تتعلمه في P2 يفيد في G1.

Prometheus، في علامة التبويب Graph

عرض استعلامات 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 هو من يصنعه عند كل كشط : 1 إذا استجابت صفحة /metrics، و0 خلاف ذلك. ثماني سلاسل لأن prometheus.yml يعلن ثماني مهام. يُقرأ كل سطر هكذا : اسم المقياس، ثم بين الأقواس المعقوفة التسميات التي وضعها Prometheus (job وinstance على الكل، وservice إضافة على API)، ثم القيمة. المكافئ في SQL : SELECT * FROM up. ما يفعله Prometheus ولا يفعله SQL : أنتج هذا الجدول بنفسه بالذهاب للطرق على ثمانية أبواب.

لفهم أعمق : لماذا 8 وليس 10 ؟

للمختبر عشر حاويات لكن Prometheus لا يقرأ سوى ثمانٍ : charge وwebhook لا يعرضان صفحة /metrics في هذا الإصدار من الحقيبة، لذلك ليسا هدفين. يعدّ etat الاثنين على حدة : 10/10 services (الحاويات) و8/8 cibles up (عمليات الكشط). إذا أعاد up يوماً 7 سلاسل بدلاً من 8، فليس ذلك لأن هدفاً سقط (سيكون بقيمة 0) : بل لأن مهمة اختفت من الإعداد. للحقيبة تنبيه لذلك، CibleAbsente.

P2. هدف واحد فقط

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

ما يطلبه الاستعلام : « قيمة up، فقط للسلاسل التي تساوي تسميتها job القيمة api. »

جديد واحد فقط : المحدد {job="api"}. تصفّي الأقواس المعقوفة حسب التسميات، مثل 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 ثلاث تسميات (instance وjob وservice) وتختلف القيمة ببضع وحدات : قُرئت الصفحة في لحظة أخرى. سلسلة لكل تركيبة (methode وroute وcode) : هذا ما يُسمى العدد الكلي (cardinalité) للمقياس، هنا 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 التي تساوي تسميتها 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 : لم تحسب شيئاً بعد. قرأت قيماً لحظية وتعلمت تصفيتها حسب التسمية.

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] تنعّم أكثر (منحنى أهدأ، وردّ فعل أبطأ). تستخدم الحقيبة [5m] في تنبيهاتها و[1m] هنا حتى ترى شيئاً يتحرك. قاعدة عملية : يجب أن تحتوي النافذة على كشطين على الأقل، إذن هنا [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 واجمعها مع الاحتفاظ بالتسمية route فقط. »

جديد واحد فقط : التجميع sum by (route) (…). يُذيب جميع التسميات الأخرى (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 بتسمية أخرى. هذا ما يفعله اللوح « Réponses par code » (الاستجابات حسب الرمز) في لوحة المعلومات « API catalogue — signaux dorés » في Grafana. خمسة رموز، خمسة أسطر ؛ الـ 500 عند 0,09 في الثانية، أي أكثر بقليل من خطأ واحد كل اثنتي عشرة ثانية.

P8. الحركة الإجمالية

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

ما يطلبه الاستعلام : « اجمع كل السرعات، دون الاحتفاظ بأي تسمية. »

جديد واحد فقط : sum(…) بدون by. النتيجة : سلسلة واحدة، بمجموعة تسميات فارغة ({})، والقيمة 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 السلاسل التي لها التسميات نفسها، وهنا للطرفين مجموعة فارغة {}، إذن تتطابق. النتيجة : 0,0099، أي 1 % ؛ إنها القيمة المضبوطة في /admin/etat (taux_erreurs: 0.01). الإشارة الذهبية الثانية، الأخطاء. تنطلق قاعدة التنبيه TauxErreursEleve في الحقيبة عندما يتجاوز هذا التعبير نفسه، محسوباً على 5 دقائق، القيمة 0,05.

P10. الكمون p95

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

ما يطلبه الاستعلام : « انطلاقاً من سلال المدرج التكراري للمدة، لجميع المسارات مجتمعة، تحت أي قيمة تقع 95 % من طلبات الدقائق الخمس الأخيرة ؟ »

جديد واحد فقط : histogram_quantile(0.95, …). تريد كمدخل السلال _bucket مجموعة حسب le (لهذا السبب sum by (le) إلزامي : بدونه، تحسب كمّيّاً لكل مسار ولا تعود للنتيجة المعنى الذي كنت تتوقعه). النتيجة : 0,098 ثانية، إذن 95 % من الطلبات تُخدم في أقل من 98 مللي ثانية. هذه هي الإشارة الذهبية الثالثة، الكمون، والمقياس الذي يراقبه التنبيه LatenceP95Elevee (العتبة : 0,5 ثانية). انظر إلى توزيع سلال /cours في مجموعة البيانات : 3248 طلباً من 3279 تحت 50 مللي ثانية، لكن /lent (من 300 إلى 900 مللي ثانية) يسحب 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. مقياس لحظي، كما هو

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

ما يطلبه الاستعلام : « آخر قيمة للمقياس اللحظي requetes_en_cours. »

لا جديد في الصياغة، إنه P1. الجديد هو النوع : يُقرأ المقياس اللحظي كما هو، دون rate(). صفر أو واحد، حسب اللحظة : يعالج API كل طلب في بضع مللي ثوانٍ، ومن النادر التقاط واحد قيد المعالجة. هذه هي الإشارة الذهبية الرابعة، التشبع : لو ارتفعت هذه القيمة إلى 50، لكان API مثقلاً. rate(requetes_en_cours[1m]) لا يسبب خطأً، لكنه يعيد رقماً لا معنى له ؛ لا يحميك Prometheus من هذا الالتباس.

P12. التنبيهات، كما يراها Prometheus

promql
ALERTS
text
Empty query result

ما يطلبه الاستعلام : « التنبيهات الحالية pending أو firing. »

لا جديد : اسم مقياس، مثل P1. ALERTS، مثل up، يصنعه Prometheus : سلسلة لكل تنبيه نشط، مع التسميتين 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

عرض استعلامات 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 هو خزانة دفاتر يوميات، مصنّف لكل تركيبة تسميات. لا يقرأ محتوى الأسطر لترتيبها، بل فقط ملصق المصنّف. يبدأ استعلام 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"}

ما يطلبه الاستعلام : « جميع أسطر السجل التي تساوي تسميتها service القيمة api. »

لا جديد مقارنة بـ P2 : محدد بين أقواس معقوفة. الفرق هو النتيجة : أسطر نصية، لا أرقام. يعرض Grafana الوقت (محوَّلاً إلى منطقتك الزمنية) ثم السطر الخام ؛ انقر على سطر لعرض تسمياته : 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"}` شيئاً

التسمية سلسلة نصية دقيقة. {service="API"} بالأحرف الكبيرة لا يعيد أي سطر، دون خطأ : المصنّف غير موجود. الأمر نفسه مع {app="api"} : التسمية تُسمى service في هذا المختبر، وليس app. عندما يعيد استعلام LogQL صفر سطر، تحقق أولاً من اسم التسمية وحالة أحرفها ؛ في Explore، يعرض لك الوضع Builder التسميات وقيمها الموجودة، وهذه أضمن طريقة لاكتشافها.

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 التي تساوي تسميتها niveau القيمة ERROR. »

جديد واحد فقط : تسميتان في المحدد، مفصولتان بفاصلة، وهذا يعني «و». التسمية 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 التي تساوي تسميتها code القيمة 500. »

لا جديد : G2 بتسمية أخرى. الأسطر نفسها كما في G2، لأن في هذا الـ API كل 500 هو ERROR والعكس صحيح. هذه نسخة السجل من P4 : حيث يقول لك Prometheus « 32 خطأً على /cours »، يريك Loki أيّها، مع العنوان الحقيقي (/cours/C0019) ومعرّف الطلب. لاحظ أن code هنا سلسلة نصية ("500") لأنها تسمية ؛ في 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. خلافاً للتسمية، يجب على Loki هنا فتح كل سطر في المصنّف {service="api"} للنظر داخله : أبطأ، لكن يمكنك البحث عن أي شيء. البدائل : != (لا يحتوي)، و|~ (تعبير نمطي)، و!~. النتيجة مختلطة : 201 و500، كل ما يتعلق بالتسجيلات.

حصيلة من G1 إلى G4 : طريقتان للتصفية، حسب التسمية (سريعة، قبل فتح الأسطر) وحسب النص (مرنة، بعده). يبدأ الاستعلام الجيد دائماً بأضيق تسمية ممكنة.

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 إلى تسميات. »

جديد واحد فقط : المحلل | json. الأسطر المعروضة هي نفسها كما في G1، لكن افتح واحداً منها : له الآن تسميات أكثر بكثير (route وmethode وduree_ms وid_requete وmessage…)، واحدة لكل حقل في الـ JSON. هذه التسميات تُحسب لحظة الاستعلام، ولا تُخزَّن : لا يزال Loki لا يفهرس سوى service وconteneur وniveau وcode. سترى أيضاً code_extracted وniveau_extracted : عندما يحمل حقل في الـ JSON الاسم نفسه لتسمية وضعها 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. »

جديد واحد فقط : مرشح التسمية | duree_ms > 500، الذي يقارن تسمية مستخرجة برقم. هذا ممكن فقط بعد | json، وإلا فإن duree_ms غير موجود. النتيجة : /lent فقط، المسار البطيء عمداً (من 300 إلى 900 مللي ثانية). هذه نسخة السجل من P10 : يقول Prometheus « p95 عند 98 مللي ثانية » ؛ ويعرض 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 لكل شريحة دقيقة واحدة، ثم اجمع مع الاحتفاظ بالتسمية 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 : ترتيب أسطر عشر حاويات حسب التسميات وقراءة المصنّف الصحيح فقط، وفتح الـ 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 بتسمية إضافية في by : p95 لكل مسار. سترى /lent حول 0,7 ثانية والمسارات الأخرى تحت 0,03 ثانية. انظر إلى ما يغيّره ذلك مقارنة بـ 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 — استنساخ الحقيبة

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. إذا كنت قد استنسخت الحقيبة مسبقاً في الدرس 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.. على جهاز الدورة، والصور في الذاكرة المؤقتة مسبقاً، استغرق الأمر 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. ثماني كتل، واحدة لكل مهمة، كل منها بـ 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 ثماني سلاسل بقيمة 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 : أُوقفت الحاوية بشكل نظيف، وبياناتها وصورتها سليمة. الآن، راقب العطل عبر خمسة مسارات، بالترتيب. لديك حوالي 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 لأن الحاوية متوقفة) ؛ التنبيه APIInjoignable نشط ؛ لا يستجيب API على المنفذ 8000 ؛ استلم webhook شيئاً. التُقط هذا المخرج على جهاز الدورة بعد دقيقة من casser api، بينما كان casser erreurs قد شُغّل قبل بضع دقائق : لهذا السبب يظهر أيضاً تنبيه ثانٍ، TauxErreursEleve. على مختبرك، لن يكون لديك سوى APIInjoignable و1 alertes actives و1 alerte(s) reçue(s). إذا شغّلت etat خلال الثواني الثلاثين الأولى، فسيكون التنبيه لا يزال 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 ثانية حتى يفشل كشط، ثم يقيّم القواعد كل 15 ثانية. لماذا 30 ثانية إضافية قبل firing : تقول القاعدة for: 30s. في التقاط آخر، وصل pending عند 31 ثانية وfiring عند 61 ثانية : رتبة المقدار هي نفسها، والتفاصيل تعتمد على اللحظة التي كسرت فيها بالنسبة إلى دورة الكشط. أعد كتابة 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) مع التنبيه، وتسمياته (instance="api:8000" وjob="api" وlabo="observabilite" وseverite="critique")، وملخصه L'API catalogue ne répond plus ووصفه. التسمية 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) : فارق خمس عشرة ثانية، منها ثوانٍ group_wait العشر الخاصة بـ Alertmanager. fin في العام 0001 تعني « لم ينتهِ بعد ». دوّن recu_a : السطر الثاني في مخرجك المطلوب.

ما يراه الحمل، في هذه الأثناء :

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"}، ويُكتب في سجل WARNING، ويبقى up عند 1. هنا، Le délai de l'opération a expiré (انتهت مهلة العملية) : لا أحد يجيب، لا رمز ولا سجل من جهة 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

عند أول كشط ناجح، يعود 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 — استنساخ الحقيبة

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). إذا كنت قد استنسخت الحقيبة مسبقاً في الدرس 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 ثماني سلاسل بقيمة 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 خلال الثواني الثلاثين الأولى، فسيكون التنبيه لا يزال 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_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) مع التنبيه، وتسمياته (instance="api:8000" وjob="api" وlabo="observabilite" وseverite="critique")، والملخص L'API catalogue ne répond plus. تأتي التسمية 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) بعد خمس عشرة ثانية، منها ثوانٍ group_wait العشر. fin في العام 0001 : لم ينتهِ بعد. دوّن recu_a : السطر الثاني في مخرجك المطلوب.

ما يراه الحمل :

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"}، ويُكتب في سجل WARNING، ويبقى up عند 1. هنا، curl: (7) Failed to connect : لا أحد يجيب، لا رمز ولا سجل من جهة 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 بينما لم تكسر شيئاً. انظر أي حاوية ليست (healthy). إذا كانت grafana أو alloy مباشرة بعد demarrer مع (health: starting)، انتظر ثلاثين ثانية. إذا كانت labo-api في حالة Exited، فقد شغّل أحدهم (ربما أنت، في محاولة سابقة) casser api : reparer. إذا كانت حاوية في حالة 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 متأخراً بكشط واحد (15 ثانية) ويبقى التنبيه firing حتى التقييم التالي، ثم بضع ثوانٍ إضافية حتى يستلم webhook الـ resolved. انتظر دقيقة وأعد تشغيل etat.

التنبيهات تتأخر : pending لا ينتقل إلى firing. لـ APIInjoignable القيمة for: 30s ؛ إذن يلزم حتى 15 ثانية (كشط) + 15 ثانية (تقييم) + 30 ثانية (for) = من 60 إلى 70 ثانية لـ firing، ثم 10 ثوانٍ من 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 مع كل إشعار. إذا لم تكن الحاوية labo-webhook في حالة healthy، نفّذ docker compose restart webhook.

Empty query result على P3 أو P5 أو G1، مباشرة بعد demarrer. يحتاج Prometheus إلى كشط واحد على الأقل (15 ثانية) لـ 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) ومن اسم التسمية (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 , : قوس إغلاق مفقود. صفر سطر دون خطأ : اسم التسمية أو حالة أحرفها ({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. الملف محفوظ دون بت التنفيذ في المستودع : chmod +x labo.sh مرة واحدة، أو شغّل bash labo.sh prerequis. تحت Git Bash (Windows)، لا يُطرح هذا السؤال.

تريد البدء من الصفر. .\labo.ps1 reinitialiser أو ./labo.sh reinitialiser يحذف الحاويات ومجلدات التخزين : يبدأ Prometheus وLoki وGrafana فارغين. ثم demarrer. لا تفعل ذلك على مختبر مشترك مع أشخاص آخرين.