محرك التنبيهات
لا يفشل نظام المراقبة لأن عتبة موجودة; يفشل لأن ذروة واحدة تصبح 500 تنبيه متطابق. يبني هذا المشروع المحرك الصغير الأمين خلف ذلك الحكم: صف Rule يراقب نافذة متدحرجة من العينات, ويُطلق تنبيهًا فقط حين تصمد العتبة فعلًا, ثم يصمت خلال فترة تبريد حتى تُبلَّغ الحادثة المستمرة مرة واحدة بدل كل ثانية. تُسلسل الحالة إلى JSON فينجو المحرك من إعادة تشغيل في منتصف حادثة, وكل ذلك يعمل على دفق اصطناعي حتمي يمكنك إنجابه بالضبط. ينتج المحرك تنبيهين حقيقيين بالضبط من دفق من ثماني عينات مُقيَّد ، لا أكثر, لا أقل ، وستعرف لماذا.
هذا يفترض صفوفًا وطرائق وتقطيعًا زائد راحة مع JSON كبيانات. لا شيء هنا مُقيَّم؛ هذا المشروع اختياري وغير مُقيَّم ، راجع مشاريع من العالم الحقيقي للاطلاع على القائمة الكاملة والنامية.
🎯 ما ستفعله
- تعريف صف
Ruleيحمل حالة المقياس والعامل والعتبة والنافذة والتبريد. - تمرير سلسلة زمنية اصطناعية عبر القاعدة والتنبؤ بأي العيّنتين ستنبهان.
- تنفيذ التبريد الذي يحوّل الانفجارات إلى حوادث منفصلة.
- أخذ لقطة لقاعدة واستعادتها من/إلى JSON دون فقدان حالتها في منتصف الحادثة.
- تجميع تنبيهات كل قاعدة وطباعة سطر الملخص.
أين تُشغّل هذا
محليًا باستخدام uv هو المسار الموصى به ، المحرك Python نقي (يلزم json فقط), فالـuv init العادي يمنحك كل شيء.
Google Colab وKaggle Notebooks وBinder تشغّل كل خطوة دون تعديل ، لا تبعيات pip, والدفق الاصطناعي حتمي. لا شيء خاص بالمنصة يقف بين دفتر والمحرك الكامل.
الإعداد
كل ما يلزم قبل أن يعمل المحرك: مشروع في مجلد, ومفردات مشتركة لما هما «عيّنة» و«قاعدة».
أعِدَّ المشروع
uv init alerting-engine
cd alerting-engine
لا تبعيات. يقرأ المحرك دفقًا من عينات {"metric": value} وقائمة قواعد; كلاهما كائنات Python عادية.
✅ قائمة التحقق
- ✅ يُنشئ
uv init alerting-engineالمشروع وملفmain.py. - ✅ ينجح
uv run python3 -c "import json"(json هو الاستيراد الوحيد).
🤔 سؤال (أسئلة) سقراطي(ة) :
- قاعدة بلا نافذة وبلا تبريد ما هي إلا مقارنة بنقطة واحدة. ما الذي ينكسر فعلًا في الإنتاج حين تُقيَّم عتبة على عينة واحدة بلا كبت ، وأي الآليتين (النافذة, التبريد) يصلح فشل «ذروة واحدة = 500 تنبيه»؟
- يغذّي المحرك سلسلة زمنية اصطناعية, حتمية عبر الأجهزة. لماذا يشتري لك ذلك شيئًا لا يستطيع دفق دائم البث إعطاءه ، وماذا تفقد لو استبدلت البذرة بتيار أجهزة استشعار حقيقي؟
الخطوة 1: عرّف صف القاعدة الجوهري
1.1 البنَّاء (constructor)
👟 تلميح البداية : اكتب Rule(metric, op, threshold, window=5, cooldown=3) يحمل وسائط القاعدة زائد حالتين تتغيران مع الزمن: history (العينات المتدحرجة) وlast_fired (زمن آخر تنبيه).
# main.py
import json
class Rule:
def __init__(self, metric, op, threshold, window=5, cooldown=3):
self.metric = metric
self.op = op
self.threshold = threshold
self.window = window
self.cooldown = cooldown
self.history = []
self.last_fired = -10**9
البنَّاء هو إعداد القاعدة كله: أي مقياس تراقب, وأي اتجاه (gt أو lt), وأي حد يُعد خرقًا, ومقبضا الكبت الاثنان. الحقلان القابلان للتحوير ، history وlast_fired ، ليسا وسيطي بنَّاء عن قصد: يمثلان حالة القاعدة المتعلَّمة عبر الزمن, وهي بالضبط ما ستُسلسله الخطوة 4.
🎯 الناتج المتوقع : لا ناتج من البناء ، لكن r.metric == "load", وr.window == 5, وr.history == [] كلها true.
🩹 إذا لم يعمل : إن غاب metric, فمررت وسيطًا موضعيًا إلى حقل غير مدرج في __init__. إن كان window افتراضيًا 5 لكنك استدعيت Rule("load", "gt", 5.0, 4), فمررت 4 وسائط موضعية فقط ، يصبح window هو الموضع الرابع ويبقى cooldown على افتراضيه.
1.2 مثِّل الخرق
👟 تلميح البداية : أضف مساعدة _is_breach(value) تجيب عن «هل عيّنة واحدة فوق العتبة (لـgt) أو تحتها (لـlt)?» ، القرار الرياضي الوحيد للمحرك.
# main.py (continued)
def _is_breach(self, value):
if self.op == "gt":
return value > self.threshold
if self.op == "lt":
return value < self.threshold
raise ValueError(f"unknown op {self.op}")
print(Rule("a", "gt", 5.0)._is_breach(6.0))
print(Rule("a", "lt", 5.0)._is_breach(6.0))
_is_breach مسند نقي: نفس القيمة, نفس الإجابة, كل مرة. إبقاؤها طريقة منفصلة يعني أن منطق النافذة في الخطوة 2 لا يضطر أبدًا لمعرفة إن كان gt أو lt يعني «سيئ» ، يسأل هذه الطريقة فقط. raise على عامل مجهول هو الحارس الذي يفشل بسرعة فيلتقط التهجئة الخاطئة "LT" بدل الصمت وعدم التنبيه إلى الأبد.
🎯 الناتج المتوقع : True ثم False ، القاعدة الأولى تخترق على 6.0 > 5, والثانية لا تفعل لأن 6.0 < 5 خاطئة.
🩹 إذا لم يعمل : إن طبع كلاهما True, ففرع lt نسي <. إن ظهر ValueError, فاستدعيت البنَّاء بـop="lt" بحروف مختلفة عما يفحصه الأسلوب ، طبّع op.lower() في البنَّاء.
1.3 تحقّق من الصف
✅ قائمة التحقق
- ✅
Rule("load", "gt", 5.0)تملكwindow=5وcooldown=3وhistoryفارغة وlast_firedفي الماضي البعيد. - ✅ تُعيد
_is_breachقيمًا منطقية وترفع على عامل مجهول. - ✅ تتصرف قاعدتا
gtوltعكسيًّا على نفس القيمة.
🤔 سؤال (أسئلة) سقراطي(ة) :
last_fired = -10**9هو حارس «منذ زمن بعيد». لماذا السالب حرفيًا «منذ زمن بعيد», لا مجرد «صفر» ، وكيف ستبدو نسخةlast_fired = Noneمن فحص التبريد؟- تقرر
_is_breachعلى عيّنة واحدة, لكن الخطوة 2 ترفع ذلك إلى نافذة. ما الفرق المفهومي بين «عيّنة واحدة تساوي 6.0» و«أقصى آخر 5 عينات لي يساوي 6.0» ، وأيهما تعريف أفضل للحادثة؟
الخطوة 2: راقب نافذة متدحرجة
عيّنة واحدة ضجيج; نافذة إشارة. تحوّل الخطوة 2 المسند النقي _is_breach إلى قرار بنوافذ ، لكن بحذر, كي يبقى «التبريد» من الخطوة 3 منفصلًا.
2.1 غذِّ القاعدة عيّناتك
👟 تلميح البداية : نفّذ evaluate(t, value) الذي يلحق بـhistory, ويقصّها إلى النافذة, ويعيد False عادةً ، منطق الإطلاق يأتي في الخطوة 3.
# main.py (continued)
def evaluate(self, t, value):
self.history.append(value)
self.history = self.history[-self.window:]
return False # window check lives in Step 3
r = Rule("load", "gt", 5.0, window=4)
for t, v in enumerate([1.0, 2.0, 3.0, 6.0, 4.0, 1.0, 1.0, 9.0]):
r.evaluate(t, v)
print(r.history)
self.history[-self.window:] هو تعبير النافذة المتدحرجة: يبقي آخر window عينة فقط, فتظل الذاكرة مقيدة مهما طال الدفق. قصّ الذيل هو قصة الصواب والكفاءة معًا بضربة واحدة. لاحظ أن evaluate ما زالت تُعيد False هنا ، يحدث حفظ النافذة أولًا, ويأتي القرار في الخطوة 3.
🎯 الناتج المتوقع : [4.0, 1.0, 1.0, 9.0] ، بعد 8 قيم بـwindow=4, احتفظ المحرك بالعيّنات الأربع الأخيرة بالضبط.
🩹 إذا لم يعمل : إن كان r.history أطول من 4, فاستُبدل التقطيع [-self.window:] بـ.append فقط. إن كان أقصر حين يكون الدفق قصيرًا, فهذا سلوك صحيح (لا تملك القاعدة تاريخ 4 عينات حتى ترى 4 عينات) ، لا خطأ.
2.2 أضف فحص الخرق بالعدد
👟 تلميح البداية : استبدل return False بالقرار الحقيقي: max(self.history) > self.threshold لقواعد gt, وmin(...) < self.threshold لـlt ، لكن فقط حين تكن النافذة ممتلئة.
# main.py (continued)
def _window_holds(self):
if len(self.history) < self.window:
return False
if self.op == "gt":
return max(self.history) > self.threshold
return min(self.history) < self.threshold
def evaluate(self, t, value):
self.history.append(value)
self.history = self.history[-self.window:]
return self._window_holds()
r = Rule("load", "gt", 5.0, window=4)
for t, v in enumerate([1.0, 2.0, 3.0, 6.0]):
print(t, v, "window-holds?", r.evaluate(t, v))
تتطلب _window_holds أن تكون النافذة ممتلئة قبل الوثوق بـmax/min ، نافذة من عيّنة واحدة تجاوزت العتبة صدفة ليست حادثة بعد. فقط حين يبلغ history حجم window تعني مقارنة max/min «هذا مستمر عبر النافذة». هذه هي الخطوة التي يتحول فيها «ذروة واحدة» إلى «حادثة حقيقية تؤكدها النافذة».
🎯 الناتج المتوقع :
0 1.0 window-holds? False
1 2.0 window-holds? False
2 3.0 window-holds? False
3 6.0 window-holds? True
مع أن 6.0 تتجاوز 5.0, ينتظر الطاقم حتى يكتمل الجيران في النافذة ليعتبرها حادثة.
🩹 إذا لم يعمل : إن كانت window-holds? True في وقت مبكر, فحارس len(history) < window مفقود. إن كانت False عند t=3 والنافذة [1,2,3,6], فليست max أكبر من 5 لأن القيمة المغذّاة 6 لا 6.0, أو أن مقارنة العتبة معكوسة.
2.3 تحقّق من النافذة
✅ قائمة التحقق
- ✅ تبقى
historyعلىwindowعيّنة بالضبط حالما يتجاوزها الدفق. - ✅ تُعيد
_window_holdsFalseحتى تمتلئ النافذة. - ✅ نافذة ممتلئة يعبر max/min بها العتبة تُعيد
True, والتي لا تعبر تُعيدFalse.
🤔 سؤال (أسئلة) سقراطي(ة) :
- النافذة حصريًا عن «هل العيّنة وسط جيران فوق العتبة». ماذا يحدث لقاعدة
gtتراقب مقياسًا مرتفعًا دائمًا لكنه يزحف ببطء؟ هل ستنطلق_window_holds, وهل نافذة قائمة على max هي الأداة الصحيحة لانجراف بطيء؟ self.history[-self.window:]تُسقط العينات القديمة تمامًا. إن أردت معرفة «كم مرة أطلقت هذه القاعدة في الشهر الماضي», فما الحالة الإضافية التي ستحتفظ بها ، ولماذا يتخلص تصميم المحرك الحالي منها عن قصد؟
الخطوة 3: أضف التبريد ، حادثة واحدة, لا عاصفة
تقول النافذة إن العتبة تصمد; يقول التبريد لا تقلها مجددًا بعد أن قلتها لتوّك. هذا هو المقبض الذي يحوّل الانفجار إلى مجموعة حوادث منفصلة.
3.1 افهم التبريد
👟 تلميح البداية : مدّد evaluate بحيث, بعد إطلاق, تبقى القاعدة صامتة cooldown خطوة زمنية ، if t - self.last_fired < self.cooldown: return False.
# main.py (continued)
def evaluate(self, t, value):
self.history.append(value)
self.history = self.history[-self.window:]
if t - self.last_fired < self.cooldown:
return False # still quiet from the last alert
if self._window_holds():
self.last_fired = t # remember when this incident fired
return True
return False
r = Rule("load", "gt", 5.0, window=4, cooldown=3)
seq = [1.0, 2.0, 3.0, 6.0, 4.0, 1.0, 1.0, 9.0]
alerts = [t for t, v in enumerate(seq) if r.evaluate(t, v)]
print("base alerts:", alerts)
التبريد هو قلب المحرك: يُختم last_fired لحظة الإطلاق, ولكل خطوة زمنية من خطوات cooldown التالية تُكبَت كل عيّنة ، حتى التي ما زالت فوق العتبة. النتيجة هي نموذج الحادثة الكلاسيكي: طفرة 6.0 تنطلق مرة, والقيم المرتفعة اللاحقة والانخفاض القصير صامتة, وخرق جديد لاحق ينطلق مجددًا. تنبيهان منفصلان من نافذة 4 عينات, بالضبط.
🎯 الناتج المتوقع : base alerts: [3, 6] ، الخرق الأول عند t=3 والخرق المتجدد عند t=6, مع كبت عيّنتي t=4 وt=5 بالتبريد. (t=5 مكبوتة لأن 5 - 3 = 2 < 3.)
🩹 إذا لم يعمل : إن أظهر التنبيهات [3, 4, 5, 6, 7], فإما أن last_fired لا يُضبط (سطر self.last_fired = t مفقود) أو أن فحص التبريد ليس t - self.last_fired < self.cooldown (زلة < مقابل <= تغيّر الحدود). إن لم تكن تنبيهات إطلاقًا, فـlast_fired يُعاد ضبطه على كل عيّنة غير مُطلقة.
3.2 قاعدة lt تعكسها
👟 تلميح البداية : تراقب قاعدة lt عبر min(self.history) < self.threshold ، منطق التبريد متطابق; ينقلب المسند فقط.
# main.py (continued)
r = Rule("mem", "lt", 20.0, window=3, cooldown=2)
alerts_lt = [t for t, v in enumerate([90.0, 85.0, 88.0, 12.0, 18.0, 40.0, 30.0])
if r.evaluate(t, v)]
print("low-mem alerts:", alerts_lt)
حين تهبط الذاكرة الحرة تحت 20, فهذه حادثة ذاكرة منخفضة. يعمل التبريد بالطريقة ذاتها: 12.0 الأولى تنطلق, و18.0 التي تليها مباشرة مكبوتة, وخرب لاحق (نزول ثانٍ بعد التعافي, أو قراءة جديدة) يصبح تنبيهًا منفصلًا.
🎯 الناتج المتوقع : low-mem alerts: [3, 5] ، خرق عند t=3 (12.0), تُكبَت t=4 (التبريد: 4 − 3 = 1 < 2), وعند t=5 انتهى التبريد والنافذة [40, 18, 12] ما زالت تحمل 12.0 دون 20 فتنطلق ، 40.0 نفسها ليست دون 20, لكن بقاء 12.0 في النافذة هو ما يشعل. جرّب يدويًا خطوة بخطوة إن اختلف ناتجك.
🩹 إذا لم يعمل : إن اختلف alerts_lt عن تتبّعك اليدوي, فخطِّ القاعدة عيّنة بعيّنة واطبع history وmin(history) وlast_fired ، يتفاعل تقليم النافذة مع التبريد, وطباعة الاثنين تكشف أين يتباينان بالضبط.
3.3 تحقّق من التبريد
✅ قائمة التحقق
- ✅ تنتج قاعدة
gtعلى دفق 8 عينات[3, 6]بالضبط ، حادثتان. - ✅ بين إطلاقين, تمر على الأقل
cooldownعيّنة بصمت. - ✅ تتشارك قاعدتا
gtوltنفس آليات التبريد, ولا تختلفان إلا في المسند.
🤔 سؤال (أسئلة) سقراطي(ة) :
- يكبُت التبريد كل عيّنة لخطوات
cooldown, حتى ذروة جديدة مضاعفة ×10 فعلًا. هل هذا هو المقايضة الصحيحة لجهاز تنبيه حقيقي, أم تريد «أكبر تنبيه يربح» بدل ذلك ، وأين يعيش هذا المنطق؟ - يُختم
last_firedبالزمنt, لا بفهرس العيّنة. في نظام يعالج حزم عينات دفعة واحدة (يقفز t بمقدار 100), كيف يسيئ فحصt - last_firedالتصرف, وماذا ستخزّن بدل ذلك؟
الخطوة 4: ثبّت الحالة واستعدها
محرك ينسى أنه أطلق فعلًا خلال إعادة تشغيل يعيد التنبيه على نفس الحادثة. تسلسل الخطوة 4 حالة كل قاعدة المتعلَّمة ، لا إعدادها فقط ، كي يصمد الاستمرار.
4.1 التقط لقطة لقاعدة
👟 تلميح البداية : أضف snapshot() تُعيد قاموس الإعداد زائد history وlast_fired, وfrom_snapshot يستعدهما.
# main.py (continued)
def snapshot(self):
return {"metric": self.metric, "op": self.op, "threshold": self.threshold,
"window": self.window, "cooldown": self.cooldown,
"history": self.history, "last_fired": self.last_fired}
@classmethod
def from_snapshot(cls, snap):
r = cls(snap["metric"], snap["op"], snap["threshold"],
snap["window"], snap["cooldown"])
r.history = snap["history"]
r.last_fired = snap["last_fired"]
return r
r = Rule("load", "gt", 5.0, window=4, cooldown=3)
for t, v in enumerate([1.0, 2.0, 3.0, 6.0, 4.0, 1.0, 1.0, 9.0]):
r.evaluate(t, v)
snap = json.dumps(r.snapshot())
print("saved", snap)
json.dumps للقطة هو عقد الاستمرار: كل حقل يلزم لاستئناف القاعدة أصبح الآن قاموسًا قابلًا للتسلسل إلى JSON. يعيد from_snapshot بناء صف Rule جديد وينسخ الحقلين المتعلَّمين, فساعة تبريد القاعدة المستعادة ونافذتها في الموضع الذي تركتهما العملية بالضبط.
🎯 الناتج المتوقع : سلسلة JSON تحوي "metric": "load" و"window": 4 و"history" و"last_fired": 6.
🩹 إذا لم يعمل : إن فشل json.dumps على قيمة غير قابلة للتسلسل, فأصبح last_fired أو history نوع numpy ، لفّها بـint(...)/float(...) قبل الإغراق. إن أغفل الناتج history, فمفتاح القاموس ليس في snapshot().
4.2 استعد ولا تُعد التنبيه على نفس الحادثة
👟 تلميح البداية : ألغِ التسلسل, وأعد البناء, وغذِّ استمرار الدفق ، يجب أن تبقى القاعدة المستعادة صامتة على العيّنات التي ما زالت ضمن تبريد آخر زمن إطلاق.
# main.py (continued)
import json
restored = Rule.from_snapshot(json.loads(snap))
print("history carried:", restored.history, "last_fired:", restored.last_fired)
for t, v in enumerate([6.0, 7.0, 8.0, 5.0], start=6):
print("t", t, "v", v, "->", "ALERT" if restored.evaluate(t, v) else "quiet")
استعادة القاعدة والاستمرار عند t=6 يعيد إنجاب الحالة الحية: 6.0, 7.0, 8.0 من الدفق كلها ضمن تبريد إطلاق t=6 (أو تُطلفه مرة ثم تصمت), ونزول جديد فعلًا يُطلق طازجًا. الخاصية المفتاحية: لا يعيد المحرك التنبيه على الحادثة التي أبلغ عنها فعلًا قبل إعادة التشغيل.
🎯 الناتج المتوقع : history carried: [4.0, 1.0, 1.0, 9.0] last_fired: 6 يتبعها أثر استمرار يُطلق مرة واحدة على الأكثر في نافذة التبريد.
🩹 إذا لم يعمل : إن أطلقت القاعدة المستعادة على أول عيّنة متواصلة, فلن يُنسخ last_fired عبر from_snapshot (عاد إلى -10**9). إن لم تنطلق أبدًا على النزول الطازج, فـhistory نُسخت بإفراط والنافذة ما زالت تحمل قيمة مرتفعة قديمة ، تحقق من طول النافذة بعد الاستعادة.
4.3 تحقّق من الاستمرار
✅ قائمة التحقق
- ✅ يُتمم
json.dumps(r.snapshot())رحلة ذهاب وإياب عبرloadsوfrom_snapshot. - ✅ تحمل الحالة المستعادة
historyوlast_firedمعًا; ويطابقhistoryذيل ما قبل الحفظ. - ✅
Rule.from_snapshot(json.loads(snap)) == Rule.from_snapshot(json.loads(snap))سلوكيًا ، استعادتان من نفس الكتلة تتصرفان بنفس الطريقة.
🤔 سؤال (أسئلة) سقراطي(ة) :
- ينسخ
from_snapshotlast_firedلكن لا شيء آخر يتحور بين إعادة التشغيلين. ماذا يحدث لو غيّرت نسخة كود جديدة قيمةwindowواستعدت كتلة قديمة تاريخُها بطول مختلف؟ هل هذا قلق مخطط بيانات أم قلق نسخة كود؟ - اللقطة قاموس واحد. لو كان لديك 50 قاعدة, أتخزن 50 ملفًا, أم مصفوفة JSON واحدة, أم قاموسًا بمفاتيح؟ ما الذي يجعل كل خيار صحيحًا لمحرك صغير خاطئًا عند المقياس؟
الخطوة 5: شغّل الدفق ولخّص
اكتمل المحرك. تدمج الخطوة 5 قواعد متعددة بدفق مُقيَّد وتطبع حكم السطر الواحد الذي يقرؤه مشغّل متعب فعلًا: أي مقياس أطلق, وكم مرة.
5.1 مرّر الدفق عبر كل القواعد
👟 تلميح البداية : احتفظ بقائمة قواعد, وغذِّ كل عيّنة لكل قاعدة, واجمع نتائج الإطلاق مفاتيحها المقاييس.
# main.py (continued)
rules = [
Rule("load", "gt", 5.0, window=4, cooldown=3),
Rule("mem_free", "lt", 20.0, window=3, cooldown=2),
]
stream = [
{"t": 0, "load": 1.0, "mem_free": 90.0},
{"t": 1, "load": 2.0, "mem_free": 85.0},
{"t": 2, "load": 3.0, "mem_free": 88.0},
{"t": 3, "load": 6.0, "mem_free": 12.0},
{"t": 4, "load": 4.0, "mem_free": 18.0},
{"t": 5, "load": 1.0, "mem_free": 40.0},
{"t": 6, "load": 1.0, "mem_free": 30.0},
{"t": 7, "load": 9.0, "mem_free": 28.0},
]
alerts = {}
for sample in stream:
for rule in rules:
if rule.evaluate(sample["t"], sample[rule.metric]):
alerts.setdefault(rule.metric, []).append(sample["t"])
print(alerts)
sample[rule.metric] هو توجيه المقياس: تسحب كل قاعدة قيمتها من عيّنة الدفق المشتركة, فتمريرة واحدة عبر الدفق تقود كل قاعدة. alerts.setdefault(rule.metric, []).append(...) يبني قائمة أزمنة إطلاق لكل مقياس دون فحصٍ صريح «هل بدأت هذه القائمة فعلًا».
🎯 الناتج المتوقع : {'load': [3, 6], 'mem_free': [3, 5]} ، حادثتا قاعدة الحِمل وحادثتا قاعدة الذاكرة, كلها من دفق واحد من 8 عينات.
🩹 إذا لم يعمل : إن غابت قائمة مقياس, فقاعدته لم تُطلق قط (تحقق من عتبتها/مسندها ضد الدفق) أو لم يحصل مفتاح setdefault على أول إلحاق. إن أطلق مقياس أكثر من المتوقع, فالتبريد أو النافذة عليه متعطل.
5.2 اطبع الملخص
👟 تلميح البداية : أضف summarize(alerts) مجهريًا كي يرى المشغّل عدّادات لا أزمنة خام.
# main.py (continued)
def summarize(alerts):
return {metric: len(times) for metric, times in alerts.items()}
print("OPERATOR SUMMARY:", summarize(alerts))
len(times) هو الضغط الودِّي للمشغّل: دفق 200 عيّنة أنتج 3 حوادث لـload وواحدة لـmem_free يلخصه بـ{'load': 3, 'mem_free': 1} في سطر واحد. كل عدّ مشتق من قائمة الإطلاقات الفعلية, فالملخص لا يستطيع الكذب بشأن ما أبلغ عنه المحرك.
🎯 الناتج المتوقع : OPERATOR SUMMARY: {'load': 2, 'mem_free': 2}.
🩹 إذا لم يعمل : إن أظهر الملخص مقياسًا بـ0 مع أنه أطلق, فبُني alerts بـsetdefault طازج على متغير مختلف. إن أظهر أكثر من المتوقع, فغذّى الدفق قيم t مكررة وحسبها التبريد حوادث منفصلة.
5.3 تحقّق من المحرك من طرفٍ إلى طرف
✅ قائمة التحقق
- ✅ ينتج دفق 8 عينات
{'load': [3, 6], 'mem_free': [3, 5]}، أربعة تنبيهات بالضبط. - ✅ يطبع الملخص عدّادات مشتقة من تلك القوائم.
- ✅ يعمل الدفق والقواعد دون تعديل في دفتر أو طرفية.
🤔 سؤال (أسئلة) سقراطي(ة) :
- يعدّ الملخص
len(times). إن تمددت نفس الحادثة عبر إعادة تشغيل, فلن تُطلق القاعدة المستعادة (بصواب) مجددًا, فالعدّ أدنى مما توحي به العيّنات الخام. هل «عدّ الإطلاقات» هو نفسه «عدّ الحوادث» ، وماذا ستضيف كي يميّز الملخص بينهما؟ - ينطلق
mem_freeمن الدفق عندt=3وt=5. تتبّع هلt=5حادثة جديدة (ذاكرة تتعافى ثم تهبط مجددًا) أم الحلقة نفسها تسطح عبر تبريد أقصر ، واذكر أي فرضية يرمز إليها قيمة التبريد2.
⚠️ مآزق شائعة
- نسيان حارس النافذة الكاملة. تقييم
max(history)على نافذة من 2 عيّنة قبل بلوغهاwindowيعامل ذروة صغيرة كحادثة. اشترطlen(history) == window(أو>=) قبل الوثوق بـmax/min. - تبريد لا يُطلق أبدًا مجددًا. إذا ضُبط
last_firedعلى كل عيّنة (لا عند الإطلاق فقط), تبقى القاعدة صامتة إلى الأبد. اختمlast_firedداخل فرعif self._window_holds():فقط. <مقابل<=في التبريد. يكبُتt - last_fired < cooldownلخطواتcooldownبالضبط; يكبُت<=عيّنة أقل. اختر واحدًا وافهم طرف النافذة الخاص بك.- تقطيع الطرف الخاطئ.
self.history[-self.window:]يُبقي الذيل;[:self.window]يُبقي الرأس وسيراقب ماضي الدفق بعد زوال أهميته بزمن طويل. - عدم معالجة الأنواع غير القابلة للتسلسل.
json.dumpsللقطة بها عدد صحيح numpy (last_firedمنnp.arange) يرفع. اجعلهاint/floatعادية قبل التثبيت. - إعادة التنبيه بعد إعادة تشغيل. استعادة قاعدة مع نسيان نسخ
last_firedتجعل المحرك المستعاد يعيد إطلاق الحادثة التي قد أبلغ عنها. استعد دائمًاhistoryوlast_firedمعًا.
ما بنيته للتو
محرك مراقبة بحالة: قواعد تراقب نوافذ متدحرجة, وتبريد يحوّل الانفجارات إلى حوادث منفصلة, واستمرار JSON ينجو من إعادة التشغيل, وملخص مشغّل من سطر واحد. البصيرة الجوهرية هي أن التنبيه قرار بحالة, لا مقارنة ، تجيب النافذة عن «هل هذا مستمر؟», ويجيب التبريد عن «ألم أقله فعلًا؟», وlast_fired هي الذاكرة التي تربطهما. الانقسام الثلاثي ذاك ينتقل إلى محددات المعدل, وتراجع إعادة المحاولة, وارتداد الزر (debouncing), وأي كود يجب أن يقرر متى يتكلم مقابل متى يبقى صامتًا.
شغّل نسخة أكمل دون أي إعداد محلي
examples/alerting-engine/ في مستودع الدورة هو المحرك كاملًا كدفتر ، نفس صف القاعدة, والدفق, والتبريد, والاستمرار, والملخص, قابلة للتشغيل في Colab/Kaggle/Binder. استنسخ المستودع أو افتحه في Codespace.
إلى أين تذهب من هنا
- أضف تأثيرًا جانبيًا
email()يطبع سطر التنبيه فقط حين تُطلق قاعدة, وخُبّه خلف نفس التبريد كي تنتج الحادثة بريدًا واحدًا لا واحدًا لكل عيّنة. - مدّد
Ruleبحقلseverityواجعلsummarizeتعدّ حوادث الحرِجة منفصلة عن التحذيرية. - ثبّت قائمة
rulesكلها معalertsمعًا في كتلة JSON واحدة كي تستعيد إعادة تشغيل كاملة الإعداد ولوحة المشغّل معًا. - غذِّ المحرك بيانات CPU/ذاكرة حية من
psutilوقارن عدّاد تنبيهاته عبر يوم بدفق الاصطناع.
شارك مشروعك مع الصف
بنيت شيئًا تفخر به؟ examples/student-projects/ معرض لمشاريع قدّمها طلاب آخرون ، وREADME الخاص به يحوي شرحًا كاملًا وودودًا للمبتدئين لإضافة مشروعك عبر pull request, حتى لو لم تستخدم git من قبل قط: عمل fork للمستودع, وإنشاء فرع, وتثبيت ملفاتك, وفتح الـ PR, خطوة بخطوة. لا يُفترض أي خبرة سابقة بـ git.
مرحبًا بك في كتابة Python خارج المتصفح. 🎓
أكمل كل خطوة ثم حدد المشروع كمكتمل لجمع نقاطه.