كيف تقرأ هذه الصفحة. عشر خطوات، استعلام واحد في كل مرة. لكل منها : الاستعلام الذي يجب كتابته، والاستجابة الدقيقة للمختبر (علامة التبويب Table)، وما يجب النظر إليه فيها. اكتب كل استعلام بنفسك (لا نسخ ولصق) : بكتابة الأقواس المعقوفة وعلامات الاقتباس والأقواس المربعة تترسّخ القواعد النحوية. ستكون الأرقام مختلفة عندك ؛ الأشكال (عدد الأسطر، والتسميات، ورتبة المقدار) يجب أن تكون نفسها. كتل « لفهم أعمق » اختيارية. إذا لم يكن المختبر قد بدأ، عد إلى التمرين الموجه : يعطي القسم باختصار الأوامر، بما فيها الحقيبة (
https://github.com/hrhouma2/aiopsatlas-observabilite-labo-fr). لا يُنشأ ولا يُعدَّل شيء في هذه الورشة : PromQL لا يفعل سوى القراءة.
جعلك التمرين الموجه تكتب اثني عشر استعلاماً مكتوباً مسبقاً. رأيت النتائج، لكن إذا سُحبت منك الورقة، هل تعرف كتابة sum by (code) (rate(http_requetes_total{route="/cours"}[1m])) دون الخطأ في قوس ؟ هنا، تنطلق من أقصر استعلام ممكن، اسم مقياس، وتضيف قطعة واحدة فقط في كل خطوة : تسمية، أو عامل، أو دالة، أو نطاق زمني، أو تجميع. خطوتان هما فخّان متعمدان : ستستحضر رسالتي الخطأ اللتين يصادفهما كل مبتدئ، للتعرف عليهما في المرة القادمة. في النهاية، تعرف ما هو المقياس والتسمية والدالة لأنك جمّعت القطع الثلاث بنفسك.
Prometheus هو دفتر قراءات. كل 15 ثانية، يمرّ أمام كل هدف، ويقرأ صفحة /metrics الخاصة به ويدوّن كل قيمة مع الوقت. المقياس هو اسم عمود في الدفتر (up وhttp_requetes_total). التسمية هي ملصق يوضع على السطر ليقول عمّ نتحدث (job="api" وcode="200") ؛ اسم المقياس نفسه بملصقات مختلفة هو سلاسل مختلفة. الدالة هي عملية على ما قرأناه : عدّ الأسطر، أو حساب ميل، أو الجمع.
| PromQL | قاعدة SQL الكلاسيكية | في هذه الورشة |
|---|---|---|
| مقياس | جدول | up وapi_info وhttp_requetes_total |
| سلسلة | سطر في الجدول | up{instance="api:8000", job="api", service="api"} |
| تسمية | عمود | job وinstance وroute وcode |
المحدد {job="api"} | WHERE job = 'api' | الخطوة 3 |
=~ | WHERE job LIKE 'a%' (بتعبير نمطي) | الخطوة 5 |
count(…) وsum(…) | COUNT(*) وSUM(…) | الخطوتان 6 و10 |
by (code) | GROUP BY code | الخطوة 10 |
النطاق [1m] | « أسطر الدقيقة الأخيرة » | الخطوة 7 |
rate(…[1m]) | لا مكافئ بسيط : ميل في الثانية | الخطوة 8 |
| متجه لحظي | قيمة واحدة لكل سلسلة، الآن | ما يعيده up |
| متجه نطاق | عدة قيم مؤرخة لكل سلسلة | ما يعيده up[1m] |
افتح http://localhost:9090. أنت على صفحة Query. يقبل حقل الإدخال استعلاماً ؛ Execute (أو Enter) يرسله. تظهر النتيجة تحت الحقل، في علامة التبويب Table. ابقَ على Table طوال الورشة : هناك ترى التسميات مكتوبة بوضوح. ترسم علامة التبويب Graph الشيء نفسه في الزمن ؛ وتفكك علامة التبويب Explain الاستعلام.
لسطر النتيجة دائماً الشكل نفسه : اسم المقياس، ثم بين الأقواس المعقوفة التسميات مرتبة أبجدياً، ثم القيمة على اليمين :
up{instance="api:8000", job="api", service="api"} 1عندما يُذيب الاستعلام الاسم (دالة، أو تجميع)، تبقى الأقواس المعقوفة، فارغة أحياناً : {} 8. تحت علامات التبويب، يخبرك Result series: N بعدد الأسطر لديك. تُكتب النتيجة الفارغة Empty query result ؛ ويعرض الاستعلام المكتوب بشكل خاطئ مربعاً أحمر Error executing query متبوعاً بالرسالة.
api_infoما يطلبه الاستعلام : آخر قيمة للمقياس api_info، لجميع سلاسله.
api_info{instance="api:8000", job="api", service="api", version="1.0.0"} 1ما يجب النظر إليه : سطر واحد فقط، Result series: 1. القيمة 1 ولن تتغير أبداً : api_info مقياس معلومات، كل ما لديه ليقوله موجود في تسميته version="1.0.0". ثلاث تسميات أخرى لم يكتبها API : instance وjob وservice أضافها Prometheus لحظة القراءة. على الصفحة http://localhost:8000/metrics، يُكتب السطر نفسه api_info{version="1.0.0"} 1.0.
api_info وليس بـ up ؟ لأن له سلسلة واحدة. ترى الشكل الكامل لسطر نتيجة (اسم، تسميات، قيمة) دون أن تشتتك سبعة أسطر أخرى._ و:. لا شرطة، ولا مسافة، ولا نقطة. api-info سيُقرأ كـ api ناقص info.1 هنا ؛ لا يخزن Prometheus نصاً، لهذا السبب الإصدار في تسمية.upما يطلبه الاستعلام : آخر قيمة لـ up، لجميع سلاسله.
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، الاسم نفسه على كل سطر، وما يتغير من سطر إلى آخر : قيم التسميتين job وinstance. هذه هي السلسلة : اسم بالإضافة إلى مجموعة تسميات. ثماني مجموعات مختلفة، ثماني سلاسل. لاحظ أن السطر الثالث وحده يحمل service="api" : أُضيفت هذه التسمية يدوياً في prometheus.yml، للمهمة api فقط. لا يوجد up على أي صفحة /metrics : يصنعه Prometheus بنفسه، 1 إذا نجحت القراءة، و0 خلاف ذلك.
up{job="api"}ما يطلبه الاستعلام : سلاسل up التي تساوي تسميتها job بالضبط api.
up{instance="api:8000", job="api", service="api"} 1ما يجب النظر إليه : سطر واحد فقط، الثالث من الخطوة 2. الأقواس المعقوفة بعد الاسم هي مرشح : نسميها محدداً. job هو اسم التسمية، و"api" قيمتها، بين علامتي اقتباس مزدوجتين، و= المساواة الدقيقة. إنه WHERE job = 'api' في SQL، حرفياً. يمكنك وضع عدة شروط مفصولة بفواصل ؛ يجب أن تكون كلها صحيحة.
استعلامان خاطئان، عمداً. أولاً، انسَ علامات الاقتباس :
up{job=api}Error executing query
invalid parameter "query": 1:8: parse error: unexpected identifier "api" in label matching, expected stringما يجب النظر إليه : parse error، لم يبحث Prometheus حتى : الاستعلام مكتوب بشكل خاطئ. 1:8 هو الموضع (السطر 1، الحرف 8، مباشرة بعد up{job=). expected string : كان يتوقع سلسلة نصية بين علامتي اقتباس. قيمة التسمية هي دائماً سلسلة نصية، حتى عندما تشبه رقماً : {code=500} يعطي عائلة الخطأ نفسها، و{code="500"} هو الشكل الصحيح.
ثم، ضع علامات الاقتباس لكن غيّر حالة الأحرف :
up{job="API"}Empty query resultما يجب النظر إليه : لا خطأ، ولا سطر. هذا هو الفخ الأخبث : الاستعلام صحيح، إنه ببساطة يطلب سلسلة غير موجودة. قيم التسميات حساسة لحالة الأحرف وللإملاء ("api " بمسافة لا يعمل أيضاً). عندما تحصل على Empty query result دون سبب، أعد كتابة الخطوة 2 وأعد قراءة القيم الدقيقة.
لرسالة خطأ Prometheus ثلاثة أجزاء : parse error (الاستعلام غير سليم البنية) أو bad_data (الاستعلام سليم البنية لكن يستحيل تنفيذه)، وموضع سطر:عمود، وجملة تقول ما كان يتوقعه. اذهب دائماً إلى الموضع المشار إليه : الخطأ هناك أو قبله مباشرة. تغطي رسائل هذه الورشة الثلاث الغالبية العظمى من الحالات : expected string (علامات الاقتباس)، وexpected "(" (الأقواس حول by)، وexpected type range vector (الأقواس المربعة، الخطوة 9).
up{job=~"a.*"}ما يطلبه الاستعلام : سلاسل up التي تطابق تسميتها job التعبير النمطي a.* : حرف a متبوع بأي شيء.
up{instance="alloy:12345", job="alloy"} 1
up{instance="api:8000", job="api", service="api"} 1
up{instance="alertmanager:9093", job="alertmanager"} 1ما يجب النظر إليه : ثلاثة أسطر، المهام الثلاث التي تبدأ بـ a. جديد واحد فقط مقارنة بالخطوة 3 : =~ بدلاً من =. يجب أن يطابق التعبير النمطي القيمة بأكملها : "a" وحده لن يعيد شيئاً، يلزم "a.*". عوامل الاختيار الأربعة : = (يساوي)، و!= (مختلف : up{job!="api"} يعيد السبعة الأخرى)، و=~ (يطابق)، و!~ (لا يطابق). =~ هو ما استخدمه التمرين الموجه في {code=~"5.."} لالتقاط جميع 5xx.
count(up)ما يطلبه الاستعلام : عدد السلاسل التي يعيدها up.
{} 8ما يجب النظر إليه : سطر واحد فقط، واختفى الاسم : {} فارغة، ثم 8. هذه أول دالة في الورشة، والنتيجة لم تعد up، بل رقماً محسوباً من up. count تجميع : يأخذ عدة سلاسل ويصنع منها واحدة. أبناء عمومته : sum (مجموع القيم : sum(up) يعطي أيضاً 8 ما دام كل شيء عند 1، و7 بمجرد سقوط هدف)، وmin، وmax، وavg. لوحة معلومات الحقيبة والأمر etat يعدّان الأهداف بهذه الطريقة بالضبط.
http_requetes_total{route="/cours", code="200"}[1m]ما يطلبه الاستعلام : جميع قيم هذه السلسلة المقروءة خلال الدقيقة الأخيرة، وليس الأخيرة فقط.
http_requetes_total{code="200", instance="api:8000", job="api", methode="GET", route="/cours", service="api"}
2439 @1789505608.199
2499 @1789505623.199
2550 @1789505638.196
2609 @1789505653.197ما يجب النظر إليه : سلسلة واحدة، لكن أربع قيم، كل منها متبوعة بـ @ وتاريخ بالثواني. خمس عشرة ثانية بين كل اثنتين : إنه scrape_interval. يرتفع العداد من 2439 إلى 2609 : 170 طلباً 200 على /cours في 45 ثانية. جديد واحد فقط : الأقواس المربعة [1m] بعد المحدد. تحوّل المتجه اللحظي (قيمة واحدة لكل سلسلة) إلى متجه نطاق (قائمة قيم مؤرخة لكل سلسلة). انقر على علامة التبويب Graph : ترفض هذا الاستعلام (Error executing query ثم invalid expression type "range vector" for range query, must be Scalar or instant Vector). لا نرسم نطاقاً خاماً، بل نعطيه لدالة. هذه هي الخطوة 8. عد إلى Table.
تحتوي الدقيقة على أربع فترات من 15 ثانية، إذن أربع أو خمس قراءات حسب اللحظة التي تشغّل فيها الاستعلام بالنسبة إلى دورة الكشط. إذا أعدت كتابة الاستعلام عدة مرات، فسترى أحياناً خمسة أسطر. التواريخ @1789505608.199 هي ثوانٍ منذ 1 يناير 1970 (وقت Unix) ؛ تحوّلها علامة التبويب Graph إلى ساعات مقروءة.
rate(http_requetes_total{route="/cours", code="200"}[1m])ما يطلبه الاستعلام : السرعة التي ازداد بها هذا العداد، بالوحدات في الثانية، محسوبة على نطاق الدقيقة الأخيرة.
{code="200", instance="api:8000", job="api", methode="GET", route="/cours", service="api"} 3.7779456864749545ما يجب النظر إليه : قيمة واحدة مجدداً، واختفى الاسم http_requetes_total من الأقواس المعقوفة : لم يعد عداداً، بل سرعة. 3,78 طلباً في الثانية. تحقق مع الخطوة 7 : 170 طلباً في 45 ثانية تعطي 3,78. جديد واحد فقط : الدالة rate()، التي تأخذ متجه نطاق وتعيد متجهاً لحظياً. إنها أهم دالة في PromQL : العداد الخام لا يُقرأ أبداً، أما ميله فنعم.
rate بلا نطاقrate(http_requetes_total{route="/cours", code="200"})Error executing query
invalid parameter "query": 1:6: parse error: expected type range vector in call to function "rate", got instant vectorما يجب النظر إليه : expected type range vector … got instant vector. أعطيت rate متجهاً لحظياً (قيمة واحدة)، وكانت تريد نطاقاً (عدة قيم مؤرخة) : بدون نقطتين، لا ميل. الحركة الصحيحة هي الخطوة 8، مع [1m]. ستقرأ هذه الرسالة كثيراً ؛ إنها تعني دائماً « ينقص […] ».
بديل لا يسبب خطأً لكنه لا يعيد شيئاً :
rate(http_requetes_total{route="/cours", code="200"}[10s])Empty query resultما يجب النظر إليه : نطاق من 10 ثوانٍ لا يحتوي في أحسن الأحوال إلا على قراءة واحدة (تفصل بينها 15 ثانية)، ويحتاج rate إلى اثنتين على الأقل. قاعدة عملية : يجب أن يساوي النطاق ضعفي scrape_interval على الأقل، إذن [30s] كحد أدنى هنا ؛ [1m] أو [5m] في الحياة الواقعية.
sum by (code) (rate(http_requetes_total{route="/cours"}[1m]))ما يطلبه الاستعلام : سرعة جميع سلاسل /cours (كل الرموز)، مجموعة مع الاحتفاظ بالتسمية code فقط.
{code="200"} 3.7779456864749545
{code="500"} 0ما يجب النظر إليه : سطران، ولم تبقَ سوى تسمية واحدة في الأقواس المعقوفة : code. ذابت كل الأخرى (instance وjob وmethode وroute وservice) في المجموع. جديد واحد فقط : sum by (code) (…)، تجميع الخطوة 6 مع عبارة by. الأقواس حول code إلزامية (sum by code (…) يعطي parse error: … expected "("). يستحق السطر {code="500"} 0 نظرة : يساوي صفراً لأن لا 500 وقع على /cours خلال الدقيقة الأخيرة (ينتج API حوالي واحد كل اثنتي عشرة ثانية، لجميع المسارات مجتمعة). الصفر ليس غياباً : السلسلة موجودة، ميلها فقط معدوم. إذا شغّلت .\labo.ps1 casser erreurs (أو ./labo.sh casser erreurs) وأعدت كتابة هذا الاستعلام بعد دقيقة، يرتفع السطر الثاني ؛ وreparer يعيده إلى الانخفاض.
هذه النتيجة هي مخرجك المطلوب : السطران، وجملة لكل منهما (« يقدّم /cours 3,78 استجابة 200 في الثانية » ؛ « لا استجابة 500 على /cours في الدقيقة الأخيرة »).
أعد الاستعلامات العشرة من الذاكرة، بالترتيب، وضع علامة :
api_info يعيد سلسلة واحدة، القيمة 1، مع version="1.0.0" في التسميات.up يعيد 8 سلاسل، كلها عند 1 (وإلا، فقد سقط هدف : سيخبرك etat أيّه).up{job="api"} يعيد سلسلة واحدة.up{job=api} يعيد parse error … expected string ؛ up{job="API"} يعيد Empty query result.up{job=~"a.*"} يعيد 3 سلاسل : alloy وapi وalertmanager.count(up) يعيد {} 8.http_requetes_total{route="/cours", code="200"}[1m] يعيد سلسلة واحدة مع 4 أو 5 قيم مؤرخة @….rate(…[1m]) يعيد قيمة واحدة، بين 3 و4 طلبات في الثانية على مختبر الدورة.rate(…) بلا أقواس مربعة يعيد expected type range vector … got instant vector.sum by (code) (rate(http_requetes_total{route="/cours"}[1m])) يعيد سلسلتين، {code="200"} و{code="500"}.لا شيء للتنظيف : لم تنشئ شيئاً. لا يزال etat يعرض 8/8 cibles up والعدد نفسه من السلاسل في الذاكرة، بفارق بضع عشرات (يواصل Prometheus الجمع).
Empty query result على api_info أو http_requetes_total. لم يُقرأ API بعد، أو هو متوقف. etat : إذا كان labo-api في حالة Exited، نفّذ reparer ؛ إذا كان كل شيء healthy، انتظر 15 ثانية (كشط واحد) وأعد التشغيل.
يعيد up 7 سلاسل بدلاً من 8، كلها عند 1. اختفت مهمة من الإعداد، وليس هدفاً سقط (سيكون عند 0). اذهب إلى Status → Target health وقارن مع المهام الثماني في الخطوة 2. إذا عدّلت prometheus/prometheus.yml، أعد الملف الأصلي (git checkout prometheus/prometheus.yml) ثم docker compose restart prometheus.
تعيد الخطوة 7 Empty query result. يجب أن توجد القيم الأربع للنطاق : مباشرة بعد demarrer، يجب الانتظار دقيقة. إذا كان API قد أعيد تشغيله للتو (reparer)، الأمر نفسه.
تعيد الخطوة 8 قيمة سالبة أو ضخمة. مستحيل من حيث المبدأ : يتعامل rate مع عمليات تصفير العداد. إذا رأيت ذلك، تحقق من أنك لم تكتب rate على مقياس لحظي (requetes_en_cours) : لا يسبب ذلك خطأً لكنه بلا معنى.
تبقى علامة التبويب Graph فارغة. لاستعلام نطاق (الخطوة 7)، هذا طبيعي : يعرض Graph invalid expression type "range vector". للأخرى، وسّع الفترة (الزر -/+ فوق الرسم البياني) : مباشرة بعد بدء التشغيل، لا توجد سوى بضع دقائق من البيانات.
parse error لا تتعرف عليه. اذهب إلى الموضع سطر:عمود في الرسالة. عدّ أقواسك : sum by (code) (rate(x[1m])) فيه ثلاثة أزواج. تحقق من كل علامة اقتباس : تأتي اثنتين اثنتين، مستقيمة (")، وليست طباعية أبداً (“ ”) ؛ النسخ واللصق من معالج نصوص يستبدلها أحياناً.