commit 80980bbf1635d22d60fbec2a1e772da6976eb5b9 Author: Eng-Omar-Hussein Date: Mon Jul 27 00:05:12 2026 +0300 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..05a2629 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# Ghaymah SRE Exam Repository + +This repository collects the solutions and supporting material for the Ghaymah SRE exam. Each folder contains one task answer or a shared reference used by multiple answers. + +## Repository Layout + +| Folder | Purpose | +|---|---| +| `q1-deploy-monitor/` | Deploying a sample API on Ghaymah Containers, with health checks, dashboard, and monitoring scripts. | +| `q2-postmortem/` | Blameless postmortem for a service outage and the follow-up remediation plan. | +| `q3-cicd/` | GitHub Actions CI/CD workflow with staging and production deployment flow. | +| `q4-scalability/` | Scalability and load-balancing calculations, including container sizing and scaling strategy. | +| `q5-mithal-monitor/` | Self-contained monitor for mithal.space with a browser dashboard and HTTP metrics endpoint. | +| `common-mortakaz/` | Shared integration ideas with products discovered through Mortakaz. | +| `common-qabilah/` | Shared profile/reference material used by the exam answers. | + +## What Each Task Covers + +`q1-deploy-monitor/` includes a Flask API, Dockerfile, and monitoring utilities for deployment on Ghaymah Containers. + +`q2-postmortem/` documents the incident analysis, root cause, impact, timeline, and corrective actions. + +`q3-cicd/` describes a CI/CD pipeline that builds once, deploys to staging, and requires manual approval before production. + +`q4-scalability/` explains the traffic assumptions, the container count calculation, and the recommended auto-scaling strategy. + +`q5-mithal-monitor/` provides a standalone Python monitor plus `dashboard.html` for live visibility into latency, uptime, DNS, SSL, and search checks. + +## Quick Start + +If you want to inspect a specific answer, open the relevant folder directly. For the runnable tasks: + +- Open `q1-deploy-monitor/dashboard.html` in a browser to view the monitoring UI. +- Run `q1-deploy-monitor/health-check.py` or `q1-deploy-monitor/health-check.sh` against the deployed service. +- Run `q5-mithal-monitor/monitor.py` to collect checks and serve the dashboard locally. + +## Notes + +- The repository is organized as a set of independent answers, so there is no single top-level application to build. +- Most documentation is written in Arabic to match the exam format, while this README serves as the entry point. diff --git a/common-mortakaz/integration-1.md b/common-mortakaz/integration-1.md new file mode 100644 index 0000000..dea9a9c --- /dev/null +++ b/common-mortakaz/integration-1.md @@ -0,0 +1,76 @@ +# اقتراح تكامل #1 — قُمرة × ghaymah.systems + +**المنتج:** [قُمرة](https://qumra.cloud) — عبدالستار عبده (مكتشف عبر [مرتكز](https://www.mortakaz.com/projects/682aed54040fd8289890828d)) + +--- + +## 1. وصف المنتج + +قُمرة منصة عربية متكاملة (SaaS) لإنشاء المتاجر الإلكترونية والمواقع +الاحترافية دون الحاجة لخبرة تقنية — بناء متجر، إضافة منتجات، بوابات دفع، +ولوحة تحكم، كل ذلك بواجهة عربية بسيطة. المنصة تخدم بالفعل آلاف التجار +العرب، وتنمو بشكل مستمر مع مواسم الشراء (رمضان، الجمعة البيضاء، الأعياد). + +## 2. كيف يتكامل مع ghaymah.systems + +قُمرة حاليًا تدير البنية التحتية لآلاف المتاجر بشكل مباشر أو عبر مزود +سحابي واحد. التكامل المقترح يحوّل ghaymah.systems إلى **طبقة الاستضافة +والتوسع الافتراضية** لكل متجر يُنشأ عبر قُمرة: + +- عند إنشاء تاجر لمتجر جديد على قُمرة → يُنشأ تلقائيًا (عبر API استدعاء) + container معزول على غيمة لهذا المتجر (multi-tenant عبر namespaces). +- بيانات كل متجر (كتالوج المنتجات، الطلبات، الصور) تُخزَّن على + **Ghaymah Block Storage** الخاص بذلك الـ tenant، مما يضمن عزل البيانات + وسهولة أخذ snapshots لكل متجر بشكل مستقل. +- عند مواسم الذروة (الجمعة البيضاء)، تُفعّل قُمرة auto-scaling عبر HPA + الخاص بغيمة لكل متجر يشهد ارتفاعًا في الزيارات، دون أن يؤثر ذلك على + باقي المتاجر (multi-tenant isolation). +- استخدام CDN/edge caching من غيمة لتسريع تحميل صفحات المتاجر للزوار. + +### رسم توضيحي مبسّط (Architecture Sketch) + +``` +تاجر قُمرة (Dashboard) + │ ينشئ متجر جديد + ▼ + Qumra API / Orchestrator + │ + ▼ + ┌───────────────────────────────────────────────┐ + │ ghaymah.systems (Cloud) │ + │ │ + │ Load Balancer (متعدد المتاجر) │ + │ │ │ + │ ▼ │ + │ Store A Container Store B Container ... │ + │ │ │ │ + │ ▼ ▼ │ + │ Block Storage A Block Storage B │ + │ (منتجات/طلبات) (منتجات/طلبات) │ + └───────────────────────────────────────────────┘ + │ + ▼ + الزائر النهائي (عميل المتجر) +``` + +## 3. القيمة المضافة للمستخدم النهائي + +- **للتاجر:** استضافة أسرع وأكثر استقرارًا خصوصًا في مواسم الذروة، بدون + الحاجة لفهم أي تفاصيل تقنية عن الخوادم — التوسع يحدث تلقائيًا وشفافيًا. +- **للزائر/العميل:** زمن استجابة أقل لصفحات المتجر، واستمرارية الخدمة + حتى مع ارتفاع الطلب المفاجئ (مثلًا حملة إعلانية ناجحة). +- **لقُمرة كمنصة:** تقليل تكلفة البنية التحتية عبر نموذج استهلاك حسب + الاستخدام (pay-per-tenant)، وتوفير SLA أعلى لعملائها التجاريين + بالاعتماد على مزود سحابي عربي متخصص بدل الاعتماد الكامل على مزود أجنبي. + +## 4. التحديات التقنية أو التجارية المحتملة + +- **تقنيًا:** بناء طبقة orchestration موثوقة تربط API قُمرة بـ API غيمة + لإنشاء/حذف الموارد تلقائيًا لكل متجر (idempotency ومعالجة الأخطاء أمر حرج). +- **الهجرة:** نقل المتاجر القائمة فعليًا من البنية الحالية إلى غيمة دون + توقف خدمة (zero-downtime migration) قد يستغرق وقتًا وتخطيطًا دقيقًا. +- **الأمان والعزل:** ضمان عزل تام بين بيانات المتاجر المختلفة (multi-tenancy) + ومنع أي تسرّب بين containers مشتركة الموارد. +- **تجاريًا:** الاتفاق على نموذج تسعير عادل بين الطرفين (هل التكلفة على + قُمرة أم تُمرَّر جزئيًا للتاجر؟) وتحديد من يتحمل SLA النهائي أمام + المستخدم. diff --git a/common-mortakaz/integration-2.md b/common-mortakaz/integration-2.md new file mode 100644 index 0000000..b9c139c --- /dev/null +++ b/common-mortakaz/integration-2.md @@ -0,0 +1,83 @@ +# اقتراح تكامل #2 — الباحث الذكي × mithal.space + +**المنتج:** [الباحث الذكي / Seeker Engine](https://seekerengine.com) — +أحمد رامي (مكتشف عبر [مرتكز](https://www.mortakaz.com/projects/6a29ce37c100ecc87a9e47e4)) + +--- + +## 1. وصف المنتج + +منصة تعليمية تفاعلية موجهة للمجتمع التقني العربي، تتيح للمستخدمين إنشاء +**"كبسولات معرفية"** (شروحات قصيرة عن تقنية أو أداة جديدة) ومشاركتها، +مع دعم الصور والتفاعل عبر الإعجابات والتعليقات. المحتوى بالكامل من +المستخدمين (UGC) وباللغة العربية. + +## 2. كيف يتكامل مع mithal.space + +mithal.space (مختبرات مِثال) مؤسسة بحثية غير ربحية متخصصة في الذكاء +الاصطناعي لمنطقة الشرق الأوسط وشمال أفريقيا، مع تركيز طبيعي على نماذج +اللغة العربية. التكامل المقترح يجعل مِثال **الطبقة الذكية** التي تعالج +محتوى الباحث الذكي: + +- **تلخيص وتوسيم تلقائي:** عند نشر كبسولة معرفية، يُستدعى نموذج لغوي + عربي من مِثال لاستخراج tags تلقائية وتصنيف الكبسولة ضمن الأقسام + المناسبة (بدل الاعتماد الكامل على تصنيف المستخدم اليدوي). +- **فحص جودة ومراجعة المحتوى:** استخدام نموذج مِثال للكشف عن التكرار + (كبسولات مشابهة سابقًا موجودة) والمحتوى الضعيف/المضلل قبل النشر. +- **مساعد بحث ذكي:** إضافة خاصية "اسأل عن هذا الموضوع" تتيح للمستخدم طرح + سؤال متابعة على كبسولة معينة، يُجاب عليه عبر نموذج مِثال بالاستناد على + محتوى المنصة نفسها (RAG على قاعدة الكبسولات). +- **توصيات مخصصة:** تحليل سلوك القراءة والإعجابات لتوليد توصيات كبسولات + ذات صلة لكل مستخدم. + +### رسم توضيحي مبسّط (Architecture Sketch) + +``` +المستخدم ينشئ كبسولة معرفية + │ + ▼ + Seeker Engine (الباحث الذكي) + │ (Webhook / API عند النشر) + ▼ + ┌─────────────────────────────────┐ + │ mithal.space (AI) │ + │ │ + │ نموذج لغوي عربي │ + │ ├─ توسيم تلقائي (Tags) │ + │ ├─ فحص تكرار/جودة │ + │ └─ تلخيص قصير │ + └─────────────────────────────────┘ + │ نتيجة المعالجة (JSON) + ▼ + تحديث الكبسولة + عرضها للمجتمع + │ + ▼ + سؤال متابعة من مستخدم آخر ──► RAG على قاعدة الكبسولات ──► إجابة +``` + +## 3. القيمة المضافة للمستخدم النهائي + +- **لصانع المحتوى:** تقليل الجهد اليدوي في التصنيف والتوسيم، وملاحظات + فورية إن كان الموضوع مكررًا أو يحتاج تحسينًا قبل النشر. +- **للقارئ:** اكتشاف أسهل للمحتوى ذي الصلة، وإمكانية الحصول على إجابات + مباشرة على أسئلته دون البحث اليدوي بين عشرات الكبسولات. +- **للمنصة:** رفع جودة المحتوى العربي التقني المتراكم، وتمييزها كمنصة + "ذكية" تدعمها جهة بحثية متخصصة في الذكاء الاصطناعي العربي — وهو عامل + تفاضلي (differentiator) واضح أمام منصات مشابهة. +- **لمِثال:** بيانات تدريب/تقييم حقيقية (real-world Arabic tech content) + لتحسين نماذجها، ضمن اتفاقية استخدام بيانات واضحة وشفافة. + +## 4. التحديات التقنية أو التجارية المحتملة + +- **جودة النموذج على المحتوى التقني:** المصطلحات التقنية غالبًا إنجليزية + داخل نص عربي (code-switching)، ما يتطلب أن يتعامل نموذج مِثال جيدًا مع + هذا المزيج اللغوي. +- **زمن الاستجابة:** إن كانت المعالجة (تلخيص/توسيم) تحدث لحظة النشر، + يجب أن تكون سريعة كفاية حتى لا تُبطئ تجربة النشر (يُفضّل معالجة + غير متزامنة عبر queue مع تحديث لاحق للكبسولة). +- **الخصوصية وملكية البيانات:** الاتفاق الواضح على من يملك حقوق استخدام + محتوى المستخدمين لتحسين نماذج مِثال البحثية، مع الحصول على موافقة + المستخدمين إن استُخدمت بياناتهم في التدريب. +- **تجاريًا:** مِثال مؤسسة بحثية غير ربحية — قد لا يكون لديها نموذج + API تجاري جاهز بـ SLA واضح، ما يتطلب اتفاقية شراكة بحثية/تجريبية بدل + عقد تجاري تقليدي في المرحلة الأولى. diff --git a/common-qabilah/qabilah-profile.txt b/common-qabilah/qabilah-profile.txt new file mode 100644 index 0000000..5a6c9b4 --- /dev/null +++ b/common-qabilah/qabilah-profile.txt @@ -0,0 +1 @@ +https://qabilah.com/profile/omar7ussein/professional-profile \ No newline at end of file diff --git a/q1-deploy-monitor/.dockerignore b/q1-deploy-monitor/.dockerignore new file mode 100644 index 0000000..41783c3 --- /dev/null +++ b/q1-deploy-monitor/.dockerignore @@ -0,0 +1,8 @@ +__pycache__/ +*.pyc +.git +.gitignore +*.log +status.json +.venv/ +README.md diff --git a/q1-deploy-monitor/Dockerfile b/q1-deploy-monitor/Dockerfile new file mode 100644 index 0000000..c2b8daa --- /dev/null +++ b/q1-deploy-monitor/Dockerfile @@ -0,0 +1,30 @@ +# ---- Base image ----------------------------------------------------------- +FROM python:3.11-slim AS base + +# Prevent .pyc files & enable unbuffered logging (important for container logs) +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PORT=8080 + +WORKDIR /app + +# ---- Install dependencies first (better layer caching) -------------------- +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# ---- Copy application code ------------------------------------------------- +COPY app/ ./app/ + +# ---- Run as a non-root user (security best practice) ----------------------- +RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser +USER appuser + +EXPOSE 8080 + +# ---- Container-level health check ----------------------------------------- +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD python -c "import urllib.request,sys; \ + sys.exit(0) if urllib.request.urlopen('http://127.0.0.1:8080/health', timeout=3).status == 200 else sys.exit(1)" + +# ---- Production WSGI server ------------------------------------------------- +CMD ["gunicorn", "--bind", "0.0.0.0:8080", "--workers", "2", "--threads", "4", "--timeout", "30", "app.main:app"] diff --git a/q1-deploy-monitor/README.md b/q1-deploy-monitor/README.md new file mode 100644 index 0000000..3dcf7fa --- /dev/null +++ b/q1-deploy-monitor/README.md @@ -0,0 +1,97 @@ +# نشر تطبيق ومراقبته على غيمة (Ghaymah Containers) + +حل كامل لنشر واجهة برمجية (API) بسيطة على منصة الحاويات في `ghaymah.systems`، مع سكربت مراقبة ولوحة تحكم مباشرة. + +## محتويات المشروع +``` +. +├── Dockerfile +├── requirements.txt +├── .dockerignore +├── app/ +│ └── main.py # تطبيق Flask + /health + /metrics +├── health-check.sh # سكربت مراقبة بلغة bash (فحص كل 30 ثانية) +├── health-check.py +└── dashboard.html # لوحة مراقبة (HTML/CSS/JS) تعمل من المتصفح مباشرة +``` + +## 1) بناء واختبار الصورة محليًا +```bash +docker build -t sample-api:latest . +docker run --rm -p 8080:8080 sample-api:latest + +# في نافذة أخرى +curl http://localhost:8080/health +curl http://localhost:8080/metrics +``` + +## 2) النشر على ghaymah.systems + +منصة الحاويات في Ghaymah تدعم النشر إما عبر ربط مستودع Git (بناء تلقائي من الـ Dockerfile) أو عبر دفع صورة جاهزة إلى سجل الحاويات الخاص بها. الخطوات العامة: + +1. سجّل الدخول إلى console الخاص بـ **ghaymah.systems**. +2. من قسم **Containers**، اختر **New Service / Deploy Container**. +3. اختر مصدر النشر: + - **Git Repository**: اربط المستودع الذي يحتوي هذا المشروع (يجب أن يحتوي على `Dockerfile` في الجذر) — المنصة تبني الصورة تلقائيًا من الـ Dockerfile. + - **أو Container Registry**: ابنِ وادفع الصورة يدويًا: + ```bash + docker build -t //sample-api:1.0 . + docker push //sample-api:1.0 + ``` + ثم أدخل مسار الصورة هذا عند إنشاء الخدمة. +4. اضبط إعدادات الخدمة: + - **Port**: `8080` (نفس المنفذ في `EXPOSE` وفي `PORT` env). + - **Health Check Path**: `/health` (المنصة تستخدمه لمعرفة جاهزية الحاوية). + - **Environment Variables**: أضف `PORT=8080` إن احتاج الأمر. + - **Resources**: ابدأ بأصغر خطة (0.5 vCPU / 256-512MB) كافية لهذا التطبيق التجريبي. +5. اضغط **Deploy**. بعد اكتمال النشر ستحصل على رابط عام مثل: + `https://sample-api-xxxx.ghaymah.systems` +6. تحقق من النشر: + ```bash + curl https://sample-api-xxxx.ghaymah.systems/health + ``` + +> ملاحظة: أسماء الأزرار والحقول الدقيقة قد تختلف قليلًا حسب نسخة الواجهة الحالية على `ghaymah.systems` — الخطوات أعلاه تعكس تدفق العمل القياسي لمنصات الحاويات (Container-as-a-Service)، راجع `docs.ghaymah.cloud` لأي تفاصيل محدّثة. + +## 3) تشغيل سكربت المراقبة + +بعد النشر، شغّل سكربت المراقبة موجّهًا إلى الرابط العام للتطبيق: + +```bash +# Python (الخيار الموصى به) +python health-check.py --url https://sample-api-xxxx.ghaymah.systems --interval 30 + +# أو bash +chmod +x monitor.sh +./monitor.sh https://sample-api-xxxx.ghaymah.systems 30 +``` + +السكربت يقوم بـ: +- فحص `/health` كل 30 ثانية. +- تسجيل الحالة وزمن الاستجابة في `monitor.log`. +- كتابة آخر نتيجة في `status.json`. +- إطلاق تنبيه (`ALERT`) في السجل بعد 3 فحوصات فاشلة متتالية. + +يمكن تشغيله كخدمة نظام دائمة (systemd) أو كحاوية منفصلة (sidecar) بجانب التطبيق نفسه. + +## 4) لوحة المراقبة (Dashboard) + +افتح `dashboard.html` مباشرة في المتصفح (لا يحتاج خادمًا، ملف ثابت واحد): + +1. أدخل الرابط العام للتطبيق في حقل الاتصال، مثل: + `https://sample-api-xxxx.ghaymah.systems` +2. اضغط **اتصال**. + +تعرض اللوحة: +- **الحالة**: مؤشر أخضر (يعمل) / أحمر (متوقف)، مع نبض حي عند التشغيل السليم. +- **زمن الاستجابة**: آخر قيمة + رسم بياني لآخر 30 قراءة. +- **عدد الطلبات**: القيمة الإجمالية القادمة من `/metrics`. +- **مدة التشغيل** وسجل مباشر لكل عملية فحص. + +اللوحة تستدعي `/metrics` كل 5 ثوانٍ عبر `fetch()` مباشرة من المتصفح؛ التطبيق (`main.py`) يضيف ترويسة `Access-Control-Allow-Origin: *` لذلك لا حاجة لخادم وسيط. + +## ملاحظات إنتاجية (Production Notes) +- التطبيق يعمل بمستخدم غير جذري (`non-root user`) داخل الحاوية. +- يُستخدم `gunicorn` كخادم WSGI إنتاجي بدلاً من خادم التطوير المدمج في Flask. +- `HEALTHCHECK` مضمّن في الـ Dockerfile نفسه، بالإضافة إلى فحص خارجي (`health-check.py`) — طبقتا مراقبة مستقلتان. +- للتوسع: يمكن رفع عدد النسخ (replicas) من إعدادات الخدمة في Ghaymah دون تعديل الكود. diff --git a/q1-deploy-monitor/app/main.py b/q1-deploy-monitor/app/main.py new file mode 100644 index 0000000..13bbdd8 --- /dev/null +++ b/q1-deploy-monitor/app/main.py @@ -0,0 +1,83 @@ +""" +Simple Python API service. +Exposes: + GET / -> basic info + GET /health -> liveness/readiness probe + GET /metrics -> runtime metrics consumed by the monitoring dashboard +""" + +import os +import time +import threading +from datetime import datetime, timezone + +from flask import Flask, jsonify + +app = Flask(__name__) + +START_TIME = time.time() + +# --- In-memory metrics (thread-safe) -------------------------------------- +_lock = threading.Lock() +_metrics = { + "requests_total": 0, + "total_response_time_ms": 0.0, +} + + +@app.before_request +def _start_timer(): + from flask import g + g.start_time = time.perf_counter() + + +@app.after_request +def _record_metrics(response): + from flask import g + elapsed_ms = (time.perf_counter() - getattr(g, "start_time", time.perf_counter())) * 1000 + with _lock: + _metrics["requests_total"] += 1 + _metrics["total_response_time_ms"] += elapsed_ms + # Allow the standalone dashboard.html to call this API from any origin + response.headers["Access-Control-Allow-Origin"] = "*" + return response + + +@app.route("/") +def index(): + return jsonify({ + "service": "sample-api", + "message": "API is running. See /health and /metrics." + }) + + +@app.route("/health") +def health(): + """Used by the container platform's health checks and the monitor.py script.""" + return jsonify({ + "status": "healthy", + "timestamp": datetime.now(timezone.utc).isoformat(), + "uptime_seconds": round(time.time() - START_TIME, 2), + }), 200 + + +@app.route("/metrics") +def metrics(): + """Used by dashboard.html to render live stats.""" + with _lock: + total = _metrics["requests_total"] + total_time = _metrics["total_response_time_ms"] + avg_ms = round(total_time / total, 2) if total else 0.0 + + return jsonify({ + "status": "healthy", + "uptime_seconds": round(time.time() - START_TIME, 2), + "requests_total": total, + "avg_response_time_ms": avg_ms, + "timestamp": datetime.now(timezone.utc).isoformat(), + }) + + +if __name__ == "__main__": + port = int(os.environ.get("PORT", 8080)) + app.run(host="0.0.0.0", port=port) diff --git a/q1-deploy-monitor/dashboard.html b/q1-deploy-monitor/dashboard.html new file mode 100644 index 0000000..a08dbc2 --- /dev/null +++ b/q1-deploy-monitor/dashboard.html @@ -0,0 +1,232 @@ + + + + + +لوحة مراقبة التطبيق + + + +
+
+
+

لوحة مراقبة التطبيق

+
فحص كل 5 ثوانٍ · Ghaymah Containers
+
+
+ + +
+
+ +
+
+
الحالة
+
+
+
غير متصل
+
+
+
+
زمن الاستجابة
+
--
+
+
+
عدد الطلبات
+
--
+
+
+
مدة التشغيل
+
--
+
+
+ +
+
زمن الاستجابة (آخر 30 قراءة)
+ +
+ +
+ +
يعتمد على نقاط الوصول /health و /metrics · يعمل بالكامل من المتصفح
+
+ + + + diff --git a/q1-deploy-monitor/health-check.py b/q1-deploy-monitor/health-check.py new file mode 100644 index 0000000..8335fc6 --- /dev/null +++ b/q1-deploy-monitor/health-check.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +""" +Lightweight uptime/health monitor. + +Polls the target app's /health endpoint every CHECK_INTERVAL seconds, +logs status + response time, and writes the latest result to a JSON +file (status.json) that can be consumed by other tools or dashboards. + +Usage: + python health-check.py --url https://your-app.ghaymah.systems + python health-check.py --url https://your-app.ghaymah.systems --interval 30 +""" + +import argparse +import json +import logging +import time +import urllib.request +import urllib.error +from datetime import datetime, timezone + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + handlers=[ + logging.FileHandler("monitor.log"), + logging.StreamHandler(), + ], +) +log = logging.getLogger("monitor") + +STATUS_FILE = "status.json" +FAILURE_THRESHOLD = 3 # consecutive failures before raising an "ALERT" + + +def check_health(url: str, timeout: float = 5.0): + start = time.perf_counter() + try: + with urllib.request.urlopen(url, timeout=timeout) as resp: + elapsed_ms = round((time.perf_counter() - start) * 1000, 2) + body = json.loads(resp.read().decode()) + return { + "ok": resp.status == 200, + "http_status": resp.status, + "response_time_ms": elapsed_ms, + "body": body, + } + except urllib.error.HTTPError as e: + elapsed_ms = round((time.perf_counter() - start) * 1000, 2) + return {"ok": False, "http_status": e.code, "response_time_ms": elapsed_ms, "error": str(e)} + except Exception as e: # DNS errors, timeouts, connection refused, etc. + elapsed_ms = round((time.perf_counter() - start) * 1000, 2) + return {"ok": False, "http_status": None, "response_time_ms": elapsed_ms, "error": str(e)} + + +def write_status(result: dict, consecutive_failures: int): + payload = { + "timestamp": datetime.now(timezone.utc).isoformat(), + "status": "healthy" if result["ok"] else "unhealthy", + "http_status": result.get("http_status"), + "response_time_ms": result.get("response_time_ms"), + "consecutive_failures": consecutive_failures, + } + with open(STATUS_FILE, "w") as f: + json.dump(payload, f, indent=2) + + +def main(): + parser = argparse.ArgumentParser(description="Poll /health endpoint on an interval.") + parser.add_argument("--url", required=True, help="Base URL of the app, e.g. https://myapp.ghaymah.systems") + parser.add_argument("--interval", type=int, default=30, help="Seconds between checks (default: 30)") + args = parser.parse_args() + + health_url = args.url.rstrip("/") + "/health" + log.info("Starting monitor for %s every %ss", health_url, args.interval) + + consecutive_failures = 0 + + while True: + result = check_health(health_url) + write_status(result, consecutive_failures) + + if result["ok"]: + if consecutive_failures > 0: + log.info("Service RECOVERED after %d failed check(s).", consecutive_failures) + consecutive_failures = 0 + log.info("OK status=%s response_time=%sms", result["http_status"], result["response_time_ms"]) + else: + consecutive_failures += 1 + log.warning( + "FAIL attempt=%d status=%s error=%s", + consecutive_failures, result.get("http_status"), result.get("error"), + ) + if consecutive_failures >= FAILURE_THRESHOLD: + log.error("ALERT: %s has failed %d consecutive health checks!", args.url, consecutive_failures) + + time.sleep(args.interval) + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + log.info("Monitor stopped by user.") diff --git a/q1-deploy-monitor/health-check.sh b/q1-deploy-monitor/health-check.sh new file mode 100644 index 0000000..7f97df9 --- /dev/null +++ b/q1-deploy-monitor/health-check.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# +# Simple bash health monitor - polls /health every 30s. +# Usage: ./monitor.sh https://your-app.ghaymah.systems [interval_seconds] + +set -euo pipefail + +BASE_URL="${1:?Usage: $0 [interval_seconds]}" +INTERVAL="${2:-30}" +HEALTH_URL="${BASE_URL%/}/health" +LOG_FILE="monitor.log" +FAILURE_THRESHOLD=3 +failures=0 + +log() { + echo "$(date -u +"%Y-%m-%dT%H:%M:%SZ") [$1] $2" | tee -a "$LOG_FILE" +} + +log "INFO" "Starting monitor for $HEALTH_URL every ${INTERVAL}s" + +while true; do + start_ns=$(date +%s%N) + http_code=$(curl -o /tmp/health_resp.json -s -w "%{http_code}" --max-time 5 "$HEALTH_URL" || echo "000") + end_ns=$(date +%s%N) + elapsed_ms=$(( (end_ns - start_ns) / 1000000 )) + + if [ "$http_code" == "200" ]; then + if [ "$failures" -gt 0 ]; then + log "INFO" "Service RECOVERED after $failures failed check(s)." + fi + failures=0 + log "INFO" "OK status=$http_code response_time=${elapsed_ms}ms" + else + failures=$((failures + 1)) + log "WARN" "FAIL attempt=$failures status=$http_code" + if [ "$failures" -ge "$FAILURE_THRESHOLD" ]; then + log "ERROR" "ALERT: $BASE_URL has failed $failures consecutive health checks!" + fi + fi + + sleep "$INTERVAL" +done diff --git a/q1-deploy-monitor/requirements.txt b/q1-deploy-monitor/requirements.txt new file mode 100644 index 0000000..b733f8f --- /dev/null +++ b/q1-deploy-monitor/requirements.txt @@ -0,0 +1,2 @@ +flask==3.0.3 +gunicorn==22.0.0 diff --git a/q2-postmortem/postmortem-report.md b/q2-postmortem/postmortem-report.md new file mode 100644 index 0000000..2ce1410 --- /dev/null +++ b/q2-postmortem/postmortem-report.md @@ -0,0 +1,180 @@ +# تقرير ما بعد الحادثة (Postmortem) +## انقطاع خدمة sample-api لمدة 45 دقيقة بسبب OOMKilled متكرر + +| الحقل | القيمة | +|---|---| +| **معرّف الحادثة** | INC-2026-0726-01 | +| **الخدمة المتأثرة** | `sample-api` (Ghaymah Containers) | +| **تاريخ الحادثة** | 26 يوليو 2026 | +| **مدة الانقطاع** | 45 دقيقة (14:05 – 14:50 UTC) | +| **الخطورة (Severity)** | SEV-2 — تعطّل كامل للخدمة، بدون فقدان بيانات | +| **الحالة** | مغلق — الإجراءات التصحيحية قيد التنفيذ | +| **معدّ التقرير** | فريق SRE | +| **نوع التقرير** | Blameless Postmortem | + +--- + +## 1. الملخص التنفيذي + +توقفت خدمة `sample-api` عن الاستجابة لمدة 45 دقيقة نتيجة دخولها في حلقة إعادة تشغيل متكررة (**CrashLoop**) بسبب قتل الحاوية من قبل نظام إدارة الذاكرة في المضيف (**OOMKilled**، exit code 137). السبب الجذري هو عدم تطابق بين حد الذاكرة (memory limit) المضبوط للحاوية وحجم الذاكرة الفعلي الذي يحتاجه التطبيق تحت الحمل، مع غياب أي تنبيه استباقي (proactive alert) قبل وصول الاستهلاك للحد الأقصى. تمت استعادة الخدمة برفع حد الذاكرة يدويًا وإعادة نشر الخدمة. لا يوجد فقدان بيانات، والتأثير كان محصورًا في عدم توفر الـ API (Full Outage) للمستخدمين خلال فترة الحادثة. + +--- + +## 2. الأثر (Impact) + +- **التوفر:** 0% استجابة ناجحة من `/health` و `/metrics` خلال 45 دقيقة. +- **المستخدمون المتأثرون:** جميع الطلبات الواردة على النطاق العام للخدمة (HTTP 502/504 من طبقة الـ ingress بعد فشل كل محاولات إعادة التشغيل المتتالية). +- **البيانات:** لا يوجد فقدان بيانات (الخدمة عديمة الحالة / stateless). +- **السمعة/SLA:** تجاوز الحادث ميزانية الخطأ الشهرية (error budget) لهذه الخدمة بنسبة تقديرية 60%. + +--- + +## 3. الجدول الزمني (Timeline) — بتوقيت UTC + +| الوقت | الحدث | +|---|---| +| 13:42 | ارتفاع تدريجي وغير ملحوظ في استهلاك الذاكرة بعد نشر تحديث يضيف معالجة استجابات أكبر (JSON payloads) دون تحرير كائنات مؤقتة بشكل كافٍ. | +| 14:05 | استهلاك الذاكرة يصل لحد الحاوية (512MB) → **OOMKilled** الأولى، exit code 137. المنصة تعيد تشغيل الحاوية تلقائيًا (restart policy: Always). | +| 14:06–14:20 | حلقة CrashLoopBackOff: الحاوية تُقتل مجددًا خلال ثوانٍ من كل إعادة تشغيل بسبب تحميل الحالة القديمة/التخزين المؤقت الذي يستهلك الذاكرة بسرعة من جديد. عدد مرات إعادة التشغيل: 9 خلال 14 دقيقة. | +| 14:22 | فحص `health-check.py` الخارجي يسجّل 3 فشل متتالي على `/health` → تسجيل `ALERT` في `monitor.log` (لا يوجد تنبيه فوري لفريق العمل لأن السكربت كان يعمل يدويًا بدون تكامل مع قناة إشعارات). | +| 14:31 | أحد المهندسين يلاحظ الانقطاع أثناء فحص روتيني (لم يكن هناك تنبيه تلقائي يوقظ الفريق — **فجوة كشف أولى**). | +| 14:33 | بدء التحقيق: مراجعة سجلات الحاوية على console غيمة، ملاحظة أحداث `OOMKilled` المتكررة. | +| 14:40 | تحديد السبب المباشر: حد الذاكرة المضبوط (512MB) غير كافٍ لحجم البيانات المعالجة بعد آخر تحديث. | +| 14:45 | رفع حد الذاكرة إلى 1024MB مؤقتًا وإعادة نشر الخدمة (rolling restart). | +| 14:50 | الخدمة تستقر، `/health` يعيد 200 باستمرار لمدة 10 دقائق متتالية. **إغلاق الحادثة.** | +| 15:30 | مراجعة ما بعد الحادثة (هذا التقرير) وفتح بنود الإجراءات التصحيحية. | + +--- + +## 4. تحليل السبب الجذري (Root Cause Analysis — 5 Whys) + +1. **لماذا توقفت الخدمة؟** + لأن الحاوية كانت تُقتل بشكل متكرر من قبل نواة النظام (OOM Killer) — exit code 137. +2. **لماذا قُتلت الحاوية؟** + لأن استهلاك الذاكرة الفعلي تجاوز حد الذاكرة (memory limit) المضبوط للحاوية على منصة غيمة. +3. **لماذا تجاوز الاستهلاك الحد؟** + لأن آخر تحديث للتطبيق أضاف معالجة لحمولات JSON أكبر مع احتفاظ بكائنات في الذاكرة (buffers/caches) دون تحرير كافٍ، ولم يُختبر تحت حمل واقعي قبل النشر. +4. **لماذا لم يُكتشف هذا قبل النشر أو فور حدوثه؟** + لأنه لا يوجد اختبار حمل (load/soak test) ضمن خط النشر، ولا يوجد تنبيه استباقي على نسبة استهلاك الذاكرة (كان الاعتماد فقط على فحص `/health` بعد فشل الخدمة فعليًا، وهو كشف "بعد الحقيقة" لا استباقي). +5. **لماذا لم تتعافَ الخدمة تلقائيًا رغم إعادة التشغيل التلقائي؟** + لأن سياسة إعادة التشغيل لم تكن مقترنة بأي تعديل ذاتي في الموارد (auto-scaling/right-sizing)، فكانت الحاوية تدخل في نفس ظرف نفاد الذاكرة عند كل إعادة تشغيل (Crash Loop) بدل التعافي. + +**السبب الجذري النهائي:** +غياب سياسة توسّع تلقائي مرتبطة باستهلاك الذاكرة الفعلي، مع تحديد ثابت وغير كافٍ لحدود الموارد (static under-provisioned memory limit)، وغياب مراقبة استباقية لاستهلاك الذاكرة قبل الوصول لنقطة القتل. + +--- + +## 5. ما الذي سار بشكل جيد / ما الذي لم يسر بشكل جيد + +**سار بشكل جيد:** +- إعادة التشغيل التلقائي للحاوية منع الحاجة لتدخل يدوي لإعادة تشغيلها. +- التطبيق عديم الحالة، فلم يحدث فقدان أو تلف بيانات. +- سكربت `health-check.py` سجّل الحادثة بدقة زمنية (توقيت أول فشل متتالي) وساعد في بناء الجدول الزمني. + +**لم يسر بشكل جيد:** +- لا يوجد تنبيه فوري (Slack/Email/SMS) عند فشل الفحوصات — الاعتماد على ملاحظة بشرية عرضية أخّر الاستجابة حوالي 9 دقائق. +- لا توجد مراقبة لاستهلاك الذاكرة كنسبة مئوية من الحد (memory usage %) قبل وقوع OOM. +- حدود الموارد (requests/limits) لم تُراجَع بعد آخر تحديث للكود. +- لا يوجد اختبار حمل ضمن CI/CD قبل النشر للإنتاج. + +--- + +## 6. الإجراءات التصحيحية (Action Items) + +| # | الإجراء | الأولوية | المسؤول | الموعد المستهدف | +|---|---|---|---|---| +| 1 | ضبط تنبيهات فورية (Alerting) عند تجاوز استهلاك الذاكرة 80% من الحد لمدة > 3 دقائق | عالية | SRE | خلال 3 أيام | +| 2 | تطبيق سياسة auto-scaling أفقي/عمودي مبنية على الذاكرة (تفاصيل في القسم 7) | عالية | SRE / Platform | خلال أسبوع | +| 3 | إضافة اختبار حمل (load test) إلى خط CI/CD قبل أي نشر للإنتاج | متوسطة | فريق التطوير | خلال أسبوعين | +| 4 | ربط `health-check.py` بقناة تنبيه فعلية (Webhook/Slack) بدل الاكتفاء بملف سجل محلي | عالية | SRE | خلال 3 أيام | +| 5 | مراجعة الكود المسؤول عن معالجة الحمولات الكبيرة وتحرير الذاكرة (buffer/cache) بشكل صريح | عالية | فريق التطوير | خلال أسبوع | +| 6 | إضافة مقياس `memory_usage_percent` إلى `/metrics` ولوحة المراقبة | متوسطة | فريق التطوير | خلال أسبوعين | +| 7 | توثيق حادثة OOMKilled ضمن Runbook التشغيل مع خطوات الاستجابة القياسية | منخفضة | SRE | خلال أسبوعين | + +--- + +## 7. سياسة Auto-Scaling المقترحة لمنصة غيمة (منع التكرار) + +الهدف: منع دخول الخدمة في CrashLoop بسبب نفاد الذاكرة، عبر ثلاث طبقات حماية مكمّلة لبعضها: **تحديد صحيح للموارد**، **توسّع أفقي عند الضغط**، و**حواجز أمان تمنع القتل المفاجئ**. + +### 7.1 تصحيح حدود الموارد (Right-Sizing) — الأساس قبل أي auto-scaling +```yaml +resources: + requests: + memory: "512Mi" # الحد الأدنى المضمون للحاوية + cpu: "250m" + limits: + memory: "768Mi" # هامش أمان 50% فوق الاستهلاك الطبيعي المُقاس + cpu: "500m" +``` +> القاعدة: `limit` يجب أن يكون أعلى من أعلى استهلاك ملاحظ فعليًا (p99) بهامش لا يقل عن 30-50%، وليس رقمًا تقديريًا ثابتًا. يُعاد قياس هذه الأرقام بعد كل تغيير جوهري في الكود. + +### 7.2 التوسّع الأفقي التلقائي (Horizontal Auto-scaling) حسب الذاكرة +```yaml +autoscaling: + min_replicas: 2 # لا تقل عن نسختين لتفادي نقطة فشل واحدة + max_replicas: 6 + metrics: + - type: memory + target_utilization_percent: 65 # التوسّع قبل الوصول لحد الخطر بكثير + - type: cpu + target_utilization_percent: 70 + scale_up: + cooldown_seconds: 60 # استجابة سريعة عند الضغط + scale_down: + cooldown_seconds: 300 # تهدئة أبطأ لتفادي "التذبذب" (flapping) +``` +- التوسّع يعتمد على **متوسط استهلاك الذاكرة عبر جميع النسخ**، وليس نسخة واحدة، لتفادي توسّع كاذب بسبب Memory Leak في نسخة واحدة فقط. +- `min_replicas: 2` يضمن استمرار الخدمة أثناء إعادة تشغيل أي نسخة. + +### 7.3 حواجز أمان تمنع الدخول في CrashLoop +| الآلية | الإعداد المقترح | +|---|---| +| **Restart Policy** | `Always` مع `backoff` تصاعدي (5s → 10s → 20s ... حتى 5 دقائق) لمنع استهلاك الموارد بمحاولات إعادة تشغيل سريعة متتالية | +| **Startup/Readiness Probe** | فحص `/health` مع `initial_delay=10s`، `period=10s`، `failure_threshold=3` — لا تُدخل النسخة حركة مرور إلا بعد استقرارها | +| **Liveness Probe منفصل عن Readiness** | لتفادي إعادة تشغيل نسخة تعاني بطئًا مؤقتًا وليس تعطلًا كاملًا | +| **Pod Disruption Budget المكافئ** | الحفاظ على نسخة واحدة صحية على الأقل أثناء أي تحديث أو إعادة توسّع | +| **Circuit breaker على مستوى التطبيق** | رفض الطلبات الكبيرة (حد أقصى لحجم body) بدل معالجتها ثم الانهيار | + +### 7.4 لماذا هذا يمنع تكرار هذا الحادث تحديدًا +- **التوسّع عند 65%** يعطي وقتًا كافيًا لإضافة نسخ جديدة *قبل* وصول أي نسخة لحد الذاكرة الفعلي (768Mi)، بدل انتظار القتل ثم إعادة التشغيل في نفس الظرف. +- **الحد الأعلى المرفوع مع هامش الأمان** يمنع التصادم مع نفس نقطة الفشل التي وقعت سابقًا (512Mi غير كافٍ). +- **`min_replicas: 2`** يعني أن فشل نسخة واحدة (حتى لو حدث OOM لأي سبب مستقبلي) لا يعني انقطاع الخدمة بالكامل — النسخة الأخرى تستمر بخدمة الطلبات أثناء إعادة التشغيل. + +--- + +## 8. الكشف المبكر باستخدام أدوات مراقبة غيمة + +الهدف: الانتقال من **كشف تفاعلي** (بعد توقف الخدمة فعليًا) إلى **كشف استباقي** (قبل وصول الاستهلاك لنقطة الخطر بدقائق). + +### 8.1 المقاييس (Metrics) الواجب مراقبتها +| المقياس | مصدره | لماذا يهم | +|---|---|---| +| نسبة استهلاك الذاكرة من الحد (`memory_usage / memory_limit`) | Container Metrics في console غيمة | المؤشر الأهم — يكشف الاتجاه التصاعدي قبل القتل بدقائق | +| عدد مرات إعادة التشغيل (`restart_count`) خلال نافذة زمنية | Container Events | ارتفاع مفاجئ = علامة CrashLoop مبكرة | +| رمز الخروج (`exit_code = 137`) | Container Logs/Events | توقيع مباشر لحدث OOMKilled تحديدًا (يميّزه عن أعطال أخرى) | +| زمن استجابة `/health` و `/metrics` | Application-level (سكربت `health-check.py` + لوحة المراقبة) | يكشف التدهور التدريجي (latency يرتفع قبل الانهيار الكامل غالبًا) | +| معدل الأخطاء 5xx من الـ ingress | منصة غيمة (Edge/Load Balancer metrics) | يعكس الأثر الفعلي على المستخدمين | + +### 8.2 التنبيهات المقترحة (Alert Rules) +| التنبيه | الشرط | الخطورة | القناة | +|---|---|---|---| +| اقتراب من حد الذاكرة | `memory_usage_percent > 75%` لمدة 5 دقائق متواصلة | تحذير (Warning) | Slack/Email | +| خطر وشيك | `memory_usage_percent > 90%` لمدة 2 دقيقة | حرج (Critical) | Slack + استدعاء (Page) | +| حدث OOMKilled | ظهور `exit_code=137` مرة واحدة | حرج فوري | Page مباشر | +| CrashLoop | `restart_count >= 3` خلال 10 دقائق | حرج | Page مباشر | +| تدهور زمن الاستجابة | متوسط زمن استجابة `/health` يرتفع 3 أضعاف عن خط الأساس | تحذير | Slack | + +### 8.3 كيف يمنع هذا تكرار الحادث تحديدًا +في حادثة اليوم، كان الاستهلاك يتصاعد تدريجيًا بين 13:42 و14:05 (23 دقيقة) قبل أول عملية قتل — وهي نافذة زمنية كانت كافية تمامًا لإطلاق تنبيه "اقتراب من حد الذاكرة" عند تجاوز 75%، مما كان يسمح بالتدخل (زيادة الحد أو التوسّع اليدوي) قبل وصول الخدمة لنقطة الانهيار الكامل بدقائق طويلة، بدل اكتشاف الحادثة بعد 26 دقيقة من توقف الخدمة فعليًا. + +### 8.4 تكامل عملي مع أدوات المشروع الحالية +- تحديث `main.py` لإضافة حقل `memory_usage_percent` إلى استجابة `/metrics` (عبر قراءة `/sys/fs/cgroup/memory.current` و`memory.max` داخل الحاوية). +- ربط `health-check.py` بقناة Webhook (Slack Incoming Webhook) بدلاً من الاكتفاء بـ `monitor.log`، بحيث يُرسل تنبيهًا فوريًا بدل انتظار مراجعة يدوية للسجل. +- عرض `memory_usage_percent` كبطاقة إضافية في `dashboard.html` مع تلوين تحذيري (أصفر عند 75%+، أحمر عند 90%+) لإعطاء رؤية بصرية مباشرة لفريق التشغيل. + +--- + +## الخلاصة + +الحادثة نتجت عن تحديد ثابت وغير كافٍ لحدود الذاكرة تصادم مع زيادة حقيقية في استهلاك التطبيق، وتفاقم أثرها بسبب غياب تنبيه استباقي وغياب توسّع تلقائي يمتص الضغط قبل الوصول لنقطة القتل. الإجراءات التصحيحية في هذا التقرير (right-sizing + auto-scaling أفقي + تنبيهات استباقية) تعالج السبب الجذري مباشرة، وتقلّل احتمال تكرار حادثة مشابهة من "شبه مؤكد" إلى "نادر جدًا" إذا نُفّذت جميعها خلال الجدول الزمني المحدد أعلاه. diff --git a/q3-cicd/README.md b/q3-cicd/README.md new file mode 100644 index 0000000..9ee8868 --- /dev/null +++ b/q3-cicd/README.md @@ -0,0 +1,108 @@ +# بناء CI/CD Pipeline على غيمة (GitHub Actions + Ghaymah Container Registry) + +## محتويات +``` +.github/workflows/workflow.yml # الـ workflow الكامل +README.md # هذا التوثيق +``` + +--- + +## 1) نظرة عامة على الـ Pipeline + +3 مراحل متسلسلة (jobs)، كل مرحلة تعتمد على نجاح ما قبلها: + +``` +build-and-push ──▶ deploy-staging ──▶ deploy-production + (بناء + فحص (نشر تلقائي، (⏸ موافقة يدوية + + رفع للسجل) بدون موافقة) مطلوبة قبل التنفيذ) +``` + +- **build-and-push**: يبني صورة Docker، يشغّلها محليًا داخل الـ runner ويتحقق من `/health` (بوابة جودة أساسية) قبل رفعها فعليًا إلى **Ghaymah Container Registry**. +- **deploy-staging**: ينشر الصورة تلقائيًا على بيئة `staging` فور نجاح البناء — بدون تدخل بشري، لأن الهدف من staging هو تحقق سريع ومستمر. +- **deploy-production**: نفس صورة staging (بدون إعادة بناء) تُنشر على `production`، لكن **الوظيفة لا تبدأ التنفيذ إلا بعد موافقة يدوية** (تفاصيل القسم التالي). + +--- + +## 2) الموافقة اليدوية (Manual Approval) قبل الإنتاج + +الطريقة المعتمدة في الـ workflow هي **GitHub Environments** — وهي الآلية الرسمية والموصى بها من GitHub لهذا الغرض (وليس كودًا مخصصًا يفتح "تذكرة" أو ينتظر تعليقًا). + +### كيفية الإعداد (مرة واحدة فقط، من واجهة المستودع): +1. اذهب إلى **Settings → Environments** في مستودع GitHub. +2. أنشئ بيئة باسم `production` (يجب أن يطابق الاسم بالضبط ما هو مكتوب في `environment: name: production` داخل الـ workflow). +3. فعّل **Required reviewers** وأضف الأشخاص أو الفريق المخوّل بالموافقة على نشر الإنتاج (مثال: قائد الفريق التقني أو مسؤول SRE). +4. (اختياري لكن موصى به) فعّل **Wait timer** (مثلاً 0-5 دقائق) كطبقة أمان إضافية، و**Deployment branches** لتقييد النشر على فرع `main` فقط. +5. أضف أسرار الإنتاج (Secrets) الخاصة بهذه البيئة تحديدًا (`GHAYMAH_API_TOKEN` لبيئة production منفصل عن نفس المتغير في staging) — هذا يمنع تسريب صلاحيات الإنتاج حتى لو تم تشغيل job آخر بالخطأ. + +### كيف يعمل عمليًا +عندما يصل تنفيذ الـ workflow إلى job **deploy-production**، يتوقف تلقائيًا في حالة **"Waiting"** ولا تُنفَّذ أي خطوة داخله (بما فيها تسجيل الدخول لـ Ghaymah CLI) حتى يوافق أحد المراجعين المخوّلين من تبويب **Actions** في GitHub. هذا يضمن أن الكود الذي وصل فعليًا إلى staging وتحقق من صحته هو نفسه الذي يُنشر للإنتاج — دون إعادة بناء، ودون فجوة زمنية تسمح بتغييرات غير مراجعة. + +--- + +## 3) الفرق بين بيئة Staging وبيئة Production + +| الجانب | Staging | Production | +|---|---|---| +| **الغرض** | تحقق نهائي قبل الإنتاج: هل يعمل الكود على بنية تحتية شبيهة بالإنتاج؟ | خدمة المستخدمين الفعليين مباشرة | +| **آلية النشر** | تلقائي بالكامل فور نجاح البناء (CD حقيقي) | يدوي الموافقة، تلقائي التنفيذ بعدها | +| **حجم الموارد** | أصغر (نسخة واحدة أو نسختان، موارد أقل) لتقليل التكلفة | مطابق للحمل الحقيقي المتوقع، مع auto-scaling فعّال | +| **البيانات** | بيانات وهمية/مقنّعة (synthetic/masked data)، لا تُستخدم بيانات مستخدمين حقيقية أبدًا | بيانات حقيقية حساسة، تخضع لسياسات الخصوصية والنسخ الاحتياطي | +| **حساسية الأسرار (Secrets)** | مفاتيح API تجريبية/محدودة الصلاحية | مفاتيح إنتاج كاملة الصلاحية، معزولة تمامًا في GitHub Environment منفصل | +| **المراقبة والتنبيهات** | مراقبة أساسية (health checks) دون تنبيهات استدعاء (paging) | مراقبة كاملة + تنبيهات فورية (Slack/Page) عند أي شذوذ، حسب سياسات الحادثة الموثقة سابقًا | +| **النطاق (Domain)** | `*-staging.ghaymah.systems` أو نطاق فرعي داخلي | النطاق العام الفعلي للخدمة | +| **قابلية إعادة الإنشاء** | يمكن إعادة تدميرها وإنشاؤها في أي وقت دون قلق | تغييرات البنية تمر عبر مراجعة/موافقة، أي تعديل مباشر عليها له أثر تشغيلي | +| **من يوافق على النشر إليها** | لا أحد — تلقائي بالكامل | مراجع مخوّل محدد مسبقًا (Required Reviewer) | + +**الخلاصة العملية:** staging هي "بروفة" مطابقة قدر الإمكان للإنتاج تُستخدم لاكتشاف الأخطاء قبل وصولها للمستخدمين، بينما production هي البيئة الوحيدة التي يُسمح فيها بموافقة بشرية صريحة كحاجز أخير قبل التأثير على المستخدمين الحقيقيين. + +--- + +## 4) الربط مع Ghaymah CLI + +### أ) التثبيت (داخل الـ workflow أو محليًا) +```bash +curl -fsSL https://cli.ghaymah.systems/install.sh | sh +ghaymah --version +``` +> ملاحظة: عنوان التثبيت وأسماء الأوامر أعلاه مبنية على النمط القياسي لأدوات CLI الخاصة بمنصات الحاويات (deploy/login/logs). يُرجى التأكد من الأمر الدقيق ورابط التثبيت من `docs.ghaymah.cloud` أو console الحساب، حيث إن تفاصيل الواجهة قد تختلف عن الأمثلة هنا. + +### ب) المصادقة (Authentication) +لا تُستخدم بيانات اعتماد شخصية داخل الـ pipeline أبدًا. بدلًا من ذلك: +1. يُنشأ **API Token** مخصص لبيئة CI/CD من لوحة تحكم غيمة (Settings → API Tokens أو ما يعادلها)، بصلاحيات محدودة (نشر/سحب صور فقط، وليس صلاحيات إدارية كاملة). +2. يُخزَّن هذا التوكن كسر (Secret) في GitHub: + - `GHAYMAH_REGISTRY_TOKEN` — لتسجيل الدخول لسجل الحاويات (`docker/login-action`). + - `GHAYMAH_API_TOKEN` — لتنفيذ أوامر `ghaymah deploy` عبر الـ CLI. +3. تسجيل الدخول داخل الـ workflow: + ```bash + ghaymah auth login --token "$GHAYMAH_API_TOKEN" + ``` + +### ج) الربط مع سجل الحاويات (Container Registry) +تسجيل الدخول لسجل الحاويات يتم عبر الأمر القياسي لـ Docker (متوافق مع أي سجل OCI، بما فيه سجل غيمة): +```bash +echo "$GHAYMAH_REGISTRY_TOKEN" | docker login registry.ghaymah.systems \ + --username "$GHAYMAH_REGISTRY_USER" --password-stdin +``` +وهذا بالضبط ما يقوم به `docker/login-action@v3` المستخدم في الـ workflow، مع الاستفادة من إدارة GitHub الآمنة للأسرار بدل كتابتها في نص الأوامر. + +### د) أوامر النشر الأساسية +```bash +# نشر صورة على خدمة معيّنة (staging أو production حسب --env) +ghaymah deploy --service --image //: --env --wait + +# متابعة سجلات الخدمة بعد النشر +ghaymah logs --service --follow + +# التحقق من حالة الخدمة والنسخ الحالية +ghaymah status --service + +# التراجع عن نشر فاشل (Rollback) إلى آخر نسخة مستقرة +ghaymah rollback --service +``` + +### هـ) أفضل الممارسات المطبّقة في هذا الـ Pipeline +- **صورة واحدة تنتقل عبر كل البيئات**: تُبنى مرة واحدة في `build-and-push` وتُنشر بنفس الوسم (tag) على staging ثم production — لا إعادة بناء بين البيئتين، لضمان أن ما يُختبر هو نفسه ما يُنشر. +- **فصل الأسرار حسب البيئة**: أسرار `staging` منفصلة تمامًا عن أسرار `production` عبر GitHub Environments، فحتى لو تم اختراق بيئة staging لا يتأثر الإنتاج. +- **بوابة جودة قبل الرفع**: فحص `/health` يتم داخل الـ runner *قبل* دفع الصورة فعليًا إلى السجل، لتفادي رفع صور معطوبة. +- **موافقة يدوية = حاجز بشري واحد واضح**: بدل انتشار موافقات متفرقة في أدوات متعددة، الموافقة مركزية في نفس واجهة GitHub Actions التي يراقبها الفريق أصلًا. diff --git a/q3-cicd/workflow.yml b/q3-cicd/workflow.yml new file mode 100644 index 0000000..d4a86c3 --- /dev/null +++ b/q3-cicd/workflow.yml @@ -0,0 +1,158 @@ +name: CI/CD - Build, Push, Deploy (Ghaymah) + +# يعمل تلقائيًا عند الدفع لفرع main، أو يدويًا لأي فرع/بيئة عبر workflow_dispatch +on: + push: + branches: ["main"] + workflow_dispatch: + inputs: + image_tag: + description: "وسم اختياري إضافي للصورة (افتراضيًا: git SHA)" + required: false + default: "" + +# صلاحيات أقل ما يمكن (least privilege) +permissions: + contents: read + packages: write + +env: + # عنوان سجل الحاويات الخاص بغيمة - عدّله حسب مشروعك + GHAYMAH_REGISTRY: registry.ghaymah.systems + GHAYMAH_NAMESPACE: my-team + IMAGE_NAME: sample-api + +jobs: + # --------------------------------------------------------------------- + # 1) بناء الصورة واختبارها ورفعها إلى Ghaymah Container Registry + # --------------------------------------------------------------------- + build-and-push: + name: Build & Push Image + runs-on: ubuntu-latest + outputs: + image_ref: ${{ steps.vars.outputs.image_ref }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set image tag variables + id: vars + run: | + SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7) + TAG="${{ github.event.inputs.image_tag }}" + if [ -z "$TAG" ]; then TAG="$SHORT_SHA"; fi + IMAGE_REF="${GHAYMAH_REGISTRY}/${GHAYMAH_NAMESPACE}/${IMAGE_NAME}:${TAG}" + echo "image_ref=${IMAGE_REF}" >> "$GITHUB_OUTPUT" + echo "Building: ${IMAGE_REF}" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # تسجيل الدخول إلى سجل حاويات غيمة عبر بيانات اعتماد مخزّنة كـ GitHub Secrets + - name: Log in to Ghaymah Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.GHAYMAH_REGISTRY }} + username: ${{ secrets.GHAYMAH_REGISTRY_USER }} + password: ${{ secrets.GHAYMAH_REGISTRY_TOKEN }} + + - name: Build image + uses: docker/build-push-action@v6 + with: + context: . + push: false + load: true + tags: | + ${{ steps.vars.outputs.image_ref }} + ${{ env.GHAYMAH_REGISTRY }}/${{ env.GHAYMAH_NAMESPACE }}/${{ env.IMAGE_NAME }}:latest + cache-from: type=gha + cache-to: type=gha,mode=max + + # اختبار سريع للتأكد أن /health يستجيب قبل الرفع للسجل (بوابة جودة أساسية) + - name: Smoke test the built image + run: | + docker run -d --name smoke -p 8080:8080 ${{ steps.vars.outputs.image_ref }} + for i in $(seq 1 10); do + if curl -sf http://localhost:8080/health; then echo "Health check passed"; break; fi + echo "Waiting for app to start... ($i/10)"; sleep 2 + done + curl -sf http://localhost:8080/health || (docker logs smoke && exit 1) + docker stop smoke + + - name: Push image to Ghaymah Registry + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: | + ${{ steps.vars.outputs.image_ref }} + ${{ env.GHAYMAH_REGISTRY }}/${{ env.GHAYMAH_NAMESPACE }}/${{ env.IMAGE_NAME }}:latest + + # --------------------------------------------------------------------- + # 2) نشر تلقائي على STAGING فور نجاح البناء - بدون موافقة يدوية + # --------------------------------------------------------------------- + deploy-staging: + name: Deploy to Staging + needs: build-and-push + runs-on: ubuntu-latest + environment: + name: staging + url: https://sample-api-staging.ghaymah.systems + steps: + - name: Install Ghaymah CLI + run: | + curl -fsSL https://cli.ghaymah.systems/install.sh | sh + ghaymah --version + + - name: Authenticate Ghaymah CLI + run: ghaymah auth login --token "${{ secrets.GHAYMAH_API_TOKEN }}" + + - name: Deploy image to staging service + run: | + ghaymah deploy \ + --service sample-api-staging \ + --image "${{ needs.build-and-push.outputs.image_ref }}" \ + --env staging \ + --wait + + - name: Verify staging health + run: | + curl -sf https://sample-api-staging.ghaymah.systems/health + + # --------------------------------------------------------------------- + # 3) نشر على PRODUCTION - يتطلب موافقة يدوية (Manual Approval) + # الموافقة تُنفَّذ عبر GitHub Environment "production" المحمي بمراجعين + # مطلوبين (Required Reviewers) من إعدادات المستودع، وليس بكود مخصص. + # --------------------------------------------------------------------- + deploy-production: + name: Deploy to Production (Manual Approval Required) + needs: [build-and-push, deploy-staging] + runs-on: ubuntu-latest + environment: + name: production # <-- هذا السطر يفعّل بوابة الموافقة اليدوية المضبوطة في إعدادات المستودع + url: https://sample-api.ghaymah.systems + steps: + - name: Install Ghaymah CLI + run: | + curl -fsSL https://cli.ghaymah.systems/install.sh | sh + ghaymah --version + + - name: Authenticate Ghaymah CLI + run: ghaymah auth login --token "${{ secrets.GHAYMAH_API_TOKEN }}" + + - name: Deploy image to production service + run: | + ghaymah deploy \ + --service sample-api-production \ + --image "${{ needs.build-and-push.outputs.image_ref }}" \ + --env production \ + --strategy rolling \ + --wait + + - name: Verify production health + run: | + curl -sf https://sample-api.ghaymah.systems/health + + - name: Post-deploy smoke check on /metrics + run: | + curl -sf https://sample-api.ghaymah.systems/metrics diff --git a/q4-scalability/architecture.png b/q4-scalability/architecture.png new file mode 100644 index 0000000..f13d215 Binary files /dev/null and b/q4-scalability/architecture.png differ diff --git a/q4-scalability/calculations.md b/q4-scalability/calculations.md new file mode 100644 index 0000000..a364a47 --- /dev/null +++ b/q4-scalability/calculations.md @@ -0,0 +1,105 @@ +# Q4 — Scalability & Load Balancing (Ghaymah Cloud) + +## 1. Architecture + +See `architecture.png` in this folder. + +Flow: **Clients (15,000 req/s) → Load Balancer (round-robin + health checks) → +Container Auto-Scaling Pool (39 containers) → In-Memory Cache / Database + +Ghaymah Block Storage**. + +- The front tier (load balancer + containers) is **stateless** and scales + horizontally. +- The data tier (database) is **stateful** and relies on Ghaymah Block Storage + for durability, decoupled from the container lifecycle. + +--- + +## 2. Container Capacity Calculation + +**Given:** +- Total incoming load: `15,000 req/s` +- Capacity per container: `500 req/s` +- Safety margin: `30%` (to absorb traffic spikes and node/container failures) + +**Formula:** + +``` +Required capacity = Total load × (1 + margin) + = 15,000 × 1.30 + = 19,500 req/s + +Containers needed = Required capacity / Capacity per container + = 19,500 / 500 + = 39 containers +``` + +**Result: 39 containers** + +| Tier | Count | Purpose | +|---|---|---| +| Min replicas (baseline) | 30 | Covers the raw 15,000 req/s | +| Target / desired | 39 | Includes the 30% margin | +| Max replicas (HPA ceiling) | 50–55 | Extra headroom for burst traffic | + +**Recommendation:** configure the Horizontal Pod Autoscaler (HPA) on a +composite metric — `requests-per-second` (via a Prometheus adapter) as the +primary signal and `CPU utilization` as a secondary safeguard — rather than +CPU alone, since CPU can lag behind real request pressure. + +--- + +## 3. Cold Start Strategy for New Containers + +1. **Pre-warmed pool (warm standby)** — keep 2–3 idle, already-running + containers outside the active traffic path so bursts are absorbed + instantly instead of waiting for a container to be built from scratch. +2. **Slim, optimized images** — use minimal base images (Alpine/distroless), + reduce layers, and pre-pull images on nodes to avoid registry pull latency + during scale-out events. +3. **Strict readiness probes** — a container only joins the load balancer + pool after passing a `readinessProbe` (DB/cache connectivity check), + preventing 503s from traffic sent to a not-yet-ready instance. +4. **Predictive/scheduled scaling alongside reactive HPA** — if peak hours + are known (campaigns, daily traffic peaks), trigger scheduled scale-up + 5–10 minutes ahead of the peak instead of relying solely on reactive HPA, + which lags behind the metrics collection window. +5. **Connection pooling & pre-warmed init** — open DB/cache connections in an + init container or internal warm-up endpoint before the first real request + arrives, instead of lazy-initializing on first use. +6. **Graceful scale-down** — use a `preStop` hook and a proper + `terminationGracePeriodSeconds` to drain in-flight requests before + terminating containers during scale-in, avoiding dropped requests on the + way down. + +--- + +## 4. Ghaymah Block Storage for Stateful Workloads + +The containers above are intentionally **stateless** — any of them can be +killed and replaced without losing data. Anything that needs durable state +(database, message queues, uploaded files) must be decoupled from the +container's lifecycle — this is where **Block Storage** comes in: + +- **Decouples data from container lifecycle** — a block volume is attached + to a container/VM as an independent raw block device. If the container is + rescheduled or fails, the same volume is re-attached to the new + instance/node without data loss — unlike ephemeral container storage, + which is wiped on restart. +- **Best fit** — relational databases (PostgreSQL/MySQL), messaging systems + (Kafka/RabbitMQ), and any workload needing low-latency random I/O, since + block storage offers near-local-disk performance versus object storage. +- **High availability** — one block volume per database replica (never share + a single volume across concurrently-active instances); pair it with + application-level replication (e.g., Postgres streaming replication) + rather than relying on storage replication alone. +- **Snapshots & backup** — schedule periodic snapshots (daily minimum, plus + before any structural change/upgrade) to keep RPO/RTO low. +- **Vertical scalability** — volumes can typically be resized as data grows + without downtime, complementing the horizontal scaling of the stateless + container tier. + +**Summary:** the front tier (load balancer + container pool) scales +horizontally and fast because it's stateless, while the data tier relies on +Ghaymah Block Storage for durability and consistency, with an in-memory +cache absorbing read pressure and reducing latency. diff --git a/q5-mithal-monitor/dashboard.html b/q5-mithal-monitor/dashboard.html new file mode 100644 index 0000000..b3f7786 --- /dev/null +++ b/q5-mithal-monitor/dashboard.html @@ -0,0 +1,243 @@ + + + + + +لوحة مراقبة mithal.space + + + +
+
+
+

لوحة مراقبة mithal.space

+
Latency · Uptime · SSL · DNS · Search Response
+
+
آخر تحديث: --
+
+ +
+
+ + + + +
+
Uptime (24 ساعة)
+
--%
+
-- فحص
+
+
+ +
+
شهادة SSL
+
-- يوم
+
--
+
تنتهي في--
+
+ +
+
آخر قياس
+
Latency--
+
DNS--
+
Search--
+
HTTP Status--
+
+
+ +
+
زمن الاستجابة (آخر ساعة)
+ +
+ +
+
آخر 10 فحوصات
+ + + + + +
الوقتالحالةLatencyDNSSearchSSL (أيام)
+
+ +
يقرأ من /api/metrics · يُحدَّث كل 30 ثانية
+
+ + + + diff --git a/q5-mithal-monitor/monitor.py b/q5-mithal-monitor/monitor.py new file mode 100644 index 0000000..e0e15ea --- /dev/null +++ b/q5-mithal-monitor/monitor.py @@ -0,0 +1,320 @@ +#!/usr/bin/env python3 +""" +monitor.py +========== +سكربت مراقبة ذاتي الاكتفاء (بدون أي مكتبات خارجية) لموقع mithal.space. + +يقوم بأمرين معًا: + 1. تشغيل خمسة فحوصات كل 60 ثانية (Latency, Uptime, SSL, DNS, Search) + وتخزين النتائج في ملف JSON Lines محلي (data.jsonl). + 2. تشغيل خادم HTTP بسيط يقدّم dashboard.html ويعرض بيانات مجمّعة + عبر /api/metrics حتى تعمل اللوحة مباشرة بفتح المتصفح على الخادم. + +التشغيل: + python monitor.py + python monitor.py --interval 60 --port 8080 + python monitor.py --once # فحص واحد فقط بدون تشغيل خادم (مناسب لجدولة cron) + +النشر على غيمة (ghaymah.systems): + هذا الملف مستقل تمامًا (لا يحتاج pip install لأي حزمة)، لذا يمكن نشره + داخل أي حاوية Python رسمية بأمر تشغيل واحد: + FROM python:3.11-slim + COPY monitor.py dashboard.html /app/ + WORKDIR /app + EXPOSE 8080 + CMD ["python", "monitor.py"] + اضبط Health Check Path على /health ومنفذ الخدمة على 8080 من console غيمة. +""" + +import argparse +import json +import os +import socket +import ssl +import threading +import time +import urllib.error +import urllib.request +from datetime import datetime, timedelta, timezone +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from urllib.parse import urlparse, urlencode + +# -------------------------------------------------------------------------- +# الإعدادات (قابلة للتعديل عبر متغيرات البيئة) +# -------------------------------------------------------------------------- +TARGET_URL = os.environ.get("TARGET_URL", "https://mithal.space") +SEARCH_PATH = os.environ.get("SEARCH_PATH", "/search") +SEARCH_QUERY_PARAM = os.environ.get("SEARCH_QUERY_PARAM", "q") +SEARCH_QUERY_VALUE = os.environ.get("SEARCH_QUERY_VALUE", "test") +REQUEST_TIMEOUT = float(os.environ.get("REQUEST_TIMEOUT", 10)) +DATA_FILE = os.environ.get("DATA_FILE", "data.jsonl") +MAX_RECORDS = int(os.environ.get("MAX_RECORDS", 3000)) # ~50 ساعة عند فحص كل دقيقة + +_parsed = urlparse(TARGET_URL) +HOSTNAME = _parsed.hostname +PORT_443 = 443 + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +DASHBOARD_FILE = os.path.join(BASE_DIR, "dashboard.html") + +_store_lock = threading.Lock() + + +# -------------------------------------------------------------------------- +# 1) الفحوصات الخمسة +# -------------------------------------------------------------------------- +def check_dns(hostname: str = HOSTNAME) -> dict: + """يقيس زمن تحليل اسم النطاق (DNS resolution).""" + start = time.perf_counter() + try: + socket.getaddrinfo(hostname, PORT_443) + return {"ok": True, "dns_ms": round((time.perf_counter() - start) * 1000, 2)} + except socket.gaierror as e: + return {"ok": False, "dns_ms": round((time.perf_counter() - start) * 1000, 2), "error": str(e)} + + +def check_ssl(hostname: str = HOSTNAME, port: int = 443) -> dict: + """يفحص صلاحية شهادة SSL وتاريخ انتهائها.""" + try: + ctx = ssl.create_default_context() + with socket.create_connection((hostname, port), timeout=REQUEST_TIMEOUT) as sock: + with ctx.wrap_socket(sock, server_hostname=hostname) as ssock: + cert = ssock.getpeercert() + not_after = datetime.strptime(cert["notAfter"], "%b %d %H:%M:%S %Y %Z").replace(tzinfo=timezone.utc) + days_remaining = (not_after - datetime.now(timezone.utc)).days + return {"ok": True, "valid": True, "expires_at": not_after.isoformat(), "days_remaining": days_remaining} + except ssl.SSLCertVerificationError as e: + return {"ok": True, "valid": False, "error": str(e)} + except Exception as e: + return {"ok": False, "valid": False, "error": str(e)} + + +def _timed_get(url: str): + """طلب GET بسيط عبر urllib مع قياس الزمن وإرجاع (status_code, elapsed_ms, error).""" + start = time.perf_counter() + req = urllib.request.Request(url, headers={"User-Agent": "mithal-monitor/1.0"}) + try: + with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT) as resp: + elapsed_ms = round((time.perf_counter() - start) * 1000, 2) + return resp.getcode(), elapsed_ms, None + except urllib.error.HTTPError as e: + elapsed_ms = round((time.perf_counter() - start) * 1000, 2) + return e.code, elapsed_ms, None # كود HTTP فعلي (مثل 404/500) وليس خطأ اتصال + except Exception as e: + elapsed_ms = round((time.perf_counter() - start) * 1000, 2) + return None, elapsed_ms, str(e) + + +def check_latency(url: str = TARGET_URL) -> dict: + """يقيس زمن استجابة الصفحة الرئيسية ويحدد حالة توفر الموقع (uptime).""" + status_code, elapsed_ms, error = _timed_get(url) + up = status_code is not None and 200 <= status_code < 400 + return {"ok": error is None, "up": up, "status_code": status_code, "latency_ms": elapsed_ms, "error": error} + + +def check_search(base_url: str = TARGET_URL) -> dict: + """يرسل استعلام بحث فعلي ويقيس زمن الرد بمعزل عن الصفحة الرئيسية.""" + query = urlencode({SEARCH_QUERY_PARAM: SEARCH_QUERY_VALUE}) + search_url = base_url.rstrip("/") + SEARCH_PATH + "?" + query + status_code, elapsed_ms, error = _timed_get(search_url) + return {"ok": error is None, "status_code": status_code, "search_response_ms": elapsed_ms, "error": error} + + +def run_full_check() -> dict: + """ينفّذ جميع الفحوصات الخمسة ويجمعها في سجل واحد جاهز للتخزين.""" + dns_result = check_dns() + latency_result = check_latency() + ssl_result = check_ssl() + search_result = check_search() + + return { + "timestamp": datetime.now(timezone.utc).isoformat(), + "target": TARGET_URL, + "up": latency_result.get("up", False), + "status_code": latency_result.get("status_code"), + "latency_ms": latency_result.get("latency_ms"), + "dns_ms": dns_result.get("dns_ms"), + "ssl_valid": ssl_result.get("valid"), + "ssl_days_remaining": ssl_result.get("days_remaining"), + "ssl_expires_at": ssl_result.get("expires_at"), + "search_response_ms": search_result.get("search_response_ms"), + "search_status_code": search_result.get("status_code"), + "error": latency_result.get("error") or ssl_result.get("error") or search_result.get("error"), + } + + +# -------------------------------------------------------------------------- +# 2) التخزين (JSON Lines) - قابل للتبديل بسهولة إلى append_to_csv أدناه +# -------------------------------------------------------------------------- +def append_to_store(record: dict, path: str = DATA_FILE, max_lines: int = MAX_RECORDS): + """يضيف سجل فحص جديد كسطر JSON، ويقلّم الملف عند تجاوز max_lines.""" + with _store_lock: + with open(path, "a", encoding="utf-8") as f: + f.write(json.dumps(record, ensure_ascii=False) + "\n") + + with open(path, "r", encoding="utf-8") as f: + lines = f.readlines() + if len(lines) > max_lines: + with open(path, "w", encoding="utf-8") as f: + f.writelines(lines[-max_lines:]) + + +def append_to_csv(record: dict, path: str = "data.csv"): + """بديل اختياري: تخزين نفس السجل بصيغة CSV مسطّحة (جدول بيانات).""" + import csv + file_exists = os.path.exists(path) + with _store_lock: + with open(path, "a", newline="", encoding="utf-8") as f: + writer = csv.DictWriter(f, fieldnames=list(record.keys())) + if not file_exists: + writer.writeheader() + writer.writerow(record) + + +def read_all_records(path: str = DATA_FILE) -> list: + if not os.path.exists(path): + return [] + records = [] + with _store_lock: + with open(path, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + try: + records.append(json.loads(line)) + except json.JSONDecodeError: + continue + return records + + +# -------------------------------------------------------------------------- +# 3) التجميع لأجل /api/metrics (uptime 24h, latency آخر ساعة، SSL، آخر 10) +# -------------------------------------------------------------------------- +def build_metrics_payload() -> dict: + records = read_all_records() + now = datetime.now(timezone.utc) + + def parse_ts(r): + try: + return datetime.fromisoformat(r["timestamp"]) + except Exception: + return None + + last_24h = [r for r in records if (ts := parse_ts(r)) and now - ts <= timedelta(hours=24)] + uptime_percent = round(100 * sum(1 for r in last_24h if r.get("up")) / len(last_24h), 2) if last_24h else None + + last_hour = [r for r in records if (ts := parse_ts(r)) and now - ts <= timedelta(hours=1)] + latency_series = [ + {"timestamp": r["timestamp"], "latency_ms": r.get("latency_ms")} + for r in last_hour if r.get("latency_ms") is not None + ] + + latest = records[-1] if records else {} + ssl_info = { + "valid": latest.get("ssl_valid"), + "days_remaining": latest.get("ssl_days_remaining"), + "expires_at": latest.get("ssl_expires_at"), + } + + last_10 = records[-10:][::-1] + + return { + "uptime_24h_percent": uptime_percent, + "checks_in_24h": len(last_24h), + "latency_series": latency_series, + "ssl": ssl_info, + "last_checks": last_10, + "latest": latest, + "generated_at": now.isoformat(), + } + + +# -------------------------------------------------------------------------- +# 4) حلقة المراقبة في الخلفية (تعمل كل CHECK_INTERVAL ثانية) +# -------------------------------------------------------------------------- +def collector_loop(interval: int): + while True: + try: + record = run_full_check() + append_to_store(record) + status = "UP" if record["up"] else "DOWN" + print(f"[{record['timestamp']}] {status} http={record['status_code']} " + f"latency={record['latency_ms']}ms dns={record['dns_ms']}ms " + f"ssl_days={record['ssl_days_remaining']} search={record['search_response_ms']}ms") + if record.get("error"): + print(f" -> ملاحظة: {record['error']}") + except Exception as e: + print(f"[collector] خطأ غير متوقع: {e}") + time.sleep(interval) + + +# -------------------------------------------------------------------------- +# 5) خادم HTTP بسيط (بدون Flask) يقدّم dashboard.html + /api/metrics + /health +# -------------------------------------------------------------------------- +class DashboardHandler(BaseHTTPRequestHandler): + def _send_json(self, payload: dict, status: int = 200): + body = json.dumps(payload, ensure_ascii=False).encode("utf-8") + self.send_response(status) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Access-Control-Allow-Origin", "*") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def do_GET(self): + if self.path in ("/", "/dashboard.html"): + if os.path.exists(DASHBOARD_FILE): + with open(DASHBOARD_FILE, "rb") as f: + body = f.read() + self.send_response(200) + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + else: + self._send_json({"error": "dashboard.html not found next to monitor.py"}, 404) + elif self.path == "/health": + self._send_json({"status": "healthy"}) + elif self.path == "/api/metrics": + self._send_json(build_metrics_payload()) + else: + self._send_json({"error": "not found"}, 404) + + def log_message(self, fmt, *args): + pass # تعطيل سجلات الخادم الافتراضية المزعجة؛ نطبع فقط سجلات الفحص + + +def run_server(port: int): + server = ThreadingHTTPServer(("0.0.0.0", port), DashboardHandler) + print(f"Dashboard server running on http://0.0.0.0:{port} (health: /health, api: /api/metrics)") + server.serve_forever() + + +# -------------------------------------------------------------------------- +# نقطة الدخول +# -------------------------------------------------------------------------- +def main(): + parser = argparse.ArgumentParser(description="Monitor mithal.space + serve dashboard.") + parser.add_argument("--interval", type=int, default=60, help="ثواني بين كل فحص (افتراضي 60)") + parser.add_argument("--port", type=int, default=int(os.environ.get("PORT", 8080))) + parser.add_argument("--once", action="store_true", help="نفّذ فحصًا واحدًا فقط بدون تشغيل الخادم (مناسب لـ cron)") + args = parser.parse_args() + + if args.once: + record = run_full_check() + append_to_store(record) + print(json.dumps(record, indent=2, ensure_ascii=False)) + return + + collector_thread = threading.Thread(target=collector_loop, args=(args.interval,), daemon=True) + collector_thread.start() + + run_server(args.port) + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\nتم إيقاف المراقبة بواسطة المستخدم.")