first commit

This commit is contained in:
2026-07-27 00:05:12 +03:00
commit 80980bbf16
19 changed files with 1912 additions and 0 deletions
+40
View File
@@ -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.
+76
View File
@@ -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 النهائي أمام
المستخدم.
+83
View File
@@ -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 واضح، ما يتطلب اتفاقية شراكة بحثية/تجريبية بدل
عقد تجاري تقليدي في المرحلة الأولى.
+1
View File
@@ -0,0 +1 @@
https://qabilah.com/profile/omar7ussein/professional-profile
+8
View File
@@ -0,0 +1,8 @@
__pycache__/
*.pyc
.git
.gitignore
*.log
status.json
.venv/
README.md
+30
View File
@@ -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"]
+97
View File
@@ -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 <registry>/<namespace>/sample-api:1.0 .
docker push <registry>/<namespace>/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 دون تعديل الكود.
+83
View File
@@ -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)
+232
View File
@@ -0,0 +1,232 @@
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>لوحة مراقبة التطبيق</title>
<style>
:root{
--bg:#0b0f14;
--panel:#111823;
--panel-border:#1e2a38;
--text:#dbe4ee;
--muted:#7d8ba0;
--ok:#33d17a;
--bad:#f2495c;
--warn:#f2b632;
--accent:#3ea6ff;
--mono: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
}
*{box-sizing:border-box;}
body{
margin:0;
background:var(--bg);
color:var(--text);
font-family: var(--mono);
min-height:100vh;
padding:28px 20px 60px;
}
.wrap{max-width:960px;margin:0 auto;}
header{
display:flex;justify-content:space-between;align-items:flex-end;
margin-bottom:22px;flex-wrap:wrap;gap:12px;
}
h1{font-size:1.25rem;margin:0;letter-spacing:.02em;font-weight:600;}
.subtitle{color:var(--muted);font-size:.8rem;margin-top:4px;}
.config{display:flex;gap:8px;align-items:center;}
.config input{
background:#0f151d;border:1px solid var(--panel-border);color:var(--text);
padding:8px 10px;border-radius:6px;font-family:var(--mono);font-size:.8rem;width:280px;
}
.config button{
background:var(--accent);border:none;color:#04101c;font-weight:700;
padding:8px 14px;border-radius:6px;cursor:pointer;font-family:var(--mono);font-size:.8rem;
}
.config button:hover{filter:brightness(1.1);}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;margin-bottom:16px;}
.card{
background:var(--panel);border:1px solid var(--panel-border);border-radius:10px;
padding:16px 18px;position:relative;overflow:hidden;
}
.label{color:var(--muted);font-size:.72rem;text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px;}
.value{font-size:1.9rem;font-weight:700;line-height:1.1;}
.value.small{font-size:1.1rem;}
.status-row{display:flex;align-items:center;gap:10px;}
.dot{width:12px;height:12px;border-radius:50%;background:var(--muted);flex:0 0 auto;}
.dot.ok{background:var(--ok);box-shadow:0 0 0 0 rgba(51,209,122,.6);animation:pulse 2s infinite;}
.dot.bad{background:var(--bad);}
@keyframes pulse{
0%{box-shadow:0 0 0 0 rgba(51,209,122,.55);}
70%{box-shadow:0 0 0 8px rgba(51,209,122,0);}
100%{box-shadow:0 0 0 0 rgba(51,209,122,0);}
}
.chart-card{grid-column:1/-1;}
canvas{width:100%;height:140px;display:block;}
.log{
background:var(--panel);border:1px solid var(--panel-border);border-radius:10px;
padding:14px 18px;font-size:.76rem;color:var(--muted);max-height:170px;overflow-y:auto;
}
.log div{padding:2px 0;border-bottom:1px dashed #1a2431;}
.log .fail{color:var(--bad);}
.log .ok-line{color:var(--ok);}
footer{margin-top:18px;color:var(--muted);font-size:.7rem;text-align:center;}
</style>
</head>
<body>
<div class="wrap">
<header>
<div>
<h1>لوحة مراقبة التطبيق</h1>
<div class="subtitle">فحص كل <span id="intervalLabel">5</span> ثوانٍ · Ghaymah Containers</div>
</div>
<div class="config">
<input id="apiUrl" type="text" placeholder="https://your-app.ghaymah.systems" />
<button id="connectBtn">اتصال</button>
</div>
</header>
<div class="grid">
<div class="card">
<div class="label">الحالة</div>
<div class="status-row">
<div class="dot" id="statusDot"></div>
<div class="value small" id="statusText">غير متصل</div>
</div>
</div>
<div class="card">
<div class="label">زمن الاستجابة</div>
<div class="value" id="latencyValue">--</div>
</div>
<div class="card">
<div class="label">عدد الطلبات</div>
<div class="value" id="requestsValue">--</div>
</div>
<div class="card">
<div class="label">مدة التشغيل</div>
<div class="value small" id="uptimeValue">--</div>
</div>
</div>
<div class="card chart-card">
<div class="label">زمن الاستجابة (آخر 30 قراءة)</div>
<canvas id="chart"></canvas>
</div>
<div class="log" id="log"></div>
<footer>يعتمد على نقاط الوصول /health و /metrics · يعمل بالكامل من المتصفح</footer>
</div>
<script>
const POLL_MS = 5000;
const HISTORY_LEN = 30;
let latencyHistory = [];
let timer = null;
const els = {
dot: document.getElementById('statusDot'),
statusText: document.getElementById('statusText'),
latency: document.getElementById('latencyValue'),
requests: document.getElementById('requestsValue'),
uptime: document.getElementById('uptimeValue'),
log: document.getElementById('log'),
apiUrl: document.getElementById('apiUrl'),
connectBtn: document.getElementById('connectBtn'),
};
function fmtUptime(sec){
if(sec == null) return '--';
const h = Math.floor(sec/3600), m = Math.floor((sec%3600)/60), s = Math.floor(sec%60);
return `${h}س ${m}د ${s}ث`;
}
function addLog(msg, ok){
const line = document.createElement('div');
line.className = ok ? 'ok-line' : 'fail';
const time = new Date().toLocaleTimeString('ar-EG');
line.textContent = `[${time}] ${msg}`;
els.log.prepend(line);
while(els.log.children.length > 40) els.log.removeChild(els.log.lastChild);
}
function drawChart(){
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const dpr = window.devicePixelRatio || 1;
const w = canvas.clientWidth, h = canvas.clientHeight;
canvas.width = w * dpr; canvas.height = h * dpr;
ctx.scale(dpr, dpr);
ctx.clearRect(0,0,w,h);
if(latencyHistory.length < 2){
ctx.fillStyle = '#3a4a5c';
ctx.font = '12px monospace';
ctx.fillText('بانتظار بيانات كافية...', 10, h/2);
return;
}
const max = Math.max(...latencyHistory, 10) * 1.2;
const stepX = w / (HISTORY_LEN - 1);
ctx.strokeStyle = '#3ea6ff';
ctx.lineWidth = 2;
ctx.beginPath();
latencyHistory.forEach((v,i) => {
const x = i * stepX;
const y = h - (v / max) * (h - 10) - 5;
i === 0 ? ctx.moveTo(x,y) : ctx.lineTo(x,y);
});
ctx.stroke();
ctx.lineTo((latencyHistory.length-1)*stepX, h);
ctx.lineTo(0, h);
ctx.closePath();
ctx.fillStyle = 'rgba(62,166,255,0.12)';
ctx.fill();
}
async function poll(){
const base = els.apiUrl.value.trim().replace(/\/$/, '');
if(!base) return;
const start = performance.now();
try{
const res = await fetch(base + '/metrics', {cache:'no-store'});
const elapsed = Math.round(performance.now() - start);
if(!res.ok) throw new Error('HTTP ' + res.status);
const data = await res.json();
els.dot.className = 'dot ok';
els.statusText.textContent = 'يعمل';
els.latency.textContent = (data.avg_response_time_ms ?? elapsed) + ' ms';
els.requests.textContent = data.requests_total ?? '--';
els.uptime.textContent = fmtUptime(data.uptime_seconds);
latencyHistory.push(elapsed);
if(latencyHistory.length > HISTORY_LEN) latencyHistory.shift();
drawChart();
addLog(`الفحص ناجح - زمن الاستجابة ${elapsed}ms`, true);
}catch(err){
els.dot.className = 'dot bad';
els.statusText.textContent = 'متوقف / لا يستجيب';
addLog('فشل الفحص: ' + err.message, false);
}
}
els.connectBtn.addEventListener('click', () => {
if(timer) clearInterval(timer);
latencyHistory = [];
poll();
timer = setInterval(poll, POLL_MS);
});
window.addEventListener('resize', drawChart);
drawChart();
</script>
</body>
</html>
+104
View File
@@ -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.")
+42
View File
@@ -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 <base_url> [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
+2
View File
@@ -0,0 +1,2 @@
flask==3.0.3
gunicorn==22.0.0
+180
View File
@@ -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 أفقي + تنبيهات استباقية) تعالج السبب الجذري مباشرة، وتقلّل احتمال تكرار حادثة مشابهة من "شبه مؤكد" إلى "نادر جدًا" إذا نُفّذت جميعها خلال الجدول الزمني المحدد أعلاه.
+108
View File
@@ -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 <service-name> --image <registry>/<namespace>/<image>:<tag> --env <staging|production> --wait
# متابعة سجلات الخدمة بعد النشر
ghaymah logs --service <service-name> --follow
# التحقق من حالة الخدمة والنسخ الحالية
ghaymah status --service <service-name>
# التراجع عن نشر فاشل (Rollback) إلى آخر نسخة مستقرة
ghaymah rollback --service <service-name>
```
### هـ) أفضل الممارسات المطبّقة في هذا الـ Pipeline
- **صورة واحدة تنتقل عبر كل البيئات**: تُبنى مرة واحدة في `build-and-push` وتُنشر بنفس الوسم (tag) على staging ثم production — لا إعادة بناء بين البيئتين، لضمان أن ما يُختبر هو نفسه ما يُنشر.
- **فصل الأسرار حسب البيئة**: أسرار `staging` منفصلة تمامًا عن أسرار `production` عبر GitHub Environments، فحتى لو تم اختراق بيئة staging لا يتأثر الإنتاج.
- **بوابة جودة قبل الرفع**: فحص `/health` يتم داخل الـ runner *قبل* دفع الصورة فعليًا إلى السجل، لتفادي رفع صور معطوبة.
- **موافقة يدوية = حاجز بشري واحد واضح**: بدل انتشار موافقات متفرقة في أدوات متعددة، الموافقة مركزية في نفس واجهة GitHub Actions التي يراقبها الفريق أصلًا.
+158
View File
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

+105
View File
@@ -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.
+243
View File
@@ -0,0 +1,243 @@
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>لوحة مراقبة mithal.space</title>
<style>
:root{
--bg:#0a0e14;
--panel:#10161f;
--border:#1c2530;
--text:#dde5ee;
--muted:#7b8a9e;
--ok:#2fd480;
--bad:#ef4f60;
--warn:#f2b632;
--accent:#5aa9ff;
--mono:'SFMono-Regular',Consolas,'Liberation Mono',Menlo,monospace;
}
*{box-sizing:border-box;}
body{
margin:0;background:var(--bg);color:var(--text);font-family:var(--mono);
padding:26px 18px 50px;
}
.wrap{max-width:1000px;margin:0 auto;}
header{display:flex;justify-content:space-between;align-items:flex-end;flex-wrap:wrap;gap:10px;margin-bottom:20px;}
h1{font-size:1.2rem;margin:0;}
.target{color:var(--muted);font-size:.78rem;margin-top:4px;}
.refresh{color:var(--muted);font-size:.72rem;}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;margin-bottom:14px;}
.card{background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:16px 18px;}
.label{color:var(--muted);font-size:.7rem;text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px;}
/* Uptime gauge */
.gauge-card{display:flex;align-items:center;gap:16px;}
.gauge{width:82px;height:82px;flex:0 0 auto;}
.gauge-value{font-size:1.6rem;font-weight:700;}
.gauge-sub{color:var(--muted);font-size:.72rem;}
.ssl-value{font-size:1.5rem;font-weight:700;}
.ssl-badge{display:inline-block;padding:2px 8px;border-radius:5px;font-size:.68rem;margin-top:6px;}
.ssl-badge.good{background:rgba(47,212,128,.15);color:var(--ok);}
.ssl-badge.warn{background:rgba(242,182,50,.15);color:var(--warn);}
.ssl-badge.bad{background:rgba(239,79,96,.15);color:var(--bad);}
.stat-row{display:flex;justify-content:space-between;font-size:.78rem;padding:4px 0;color:var(--muted);}
.stat-row b{color:var(--text);}
.chart-card canvas{width:100%;height:160px;display:block;}
table{width:100%;border-collapse:collapse;font-size:.76rem;}
th{text-align:right;color:var(--muted);font-weight:600;padding:8px 6px;border-bottom:1px solid var(--border);}
td{padding:7px 6px;border-bottom:1px dashed var(--border);}
.pill{display:inline-block;width:8px;height:8px;border-radius:50%;margin-left:6px;}
.pill.up{background:var(--ok);}
.pill.down{background:var(--bad);}
footer{text-align:center;color:var(--muted);font-size:.68rem;margin-top:18px;}
</style>
</head>
<body>
<div class="wrap">
<header>
<div>
<h1>لوحة مراقبة mithal.space</h1>
<div class="target">Latency · Uptime · SSL · DNS · Search Response</div>
</div>
<div class="refresh">آخر تحديث: <span id="lastUpdate">--</span></div>
</header>
<div class="grid">
<div class="card gauge-card">
<svg class="gauge" viewBox="0 0 42 42">
<circle cx="21" cy="21" r="18" fill="none" stroke="#1c2530" stroke-width="4"></circle>
<circle id="uptimeArc" cx="21" cy="21" r="18" fill="none" stroke="var(--ok)" stroke-width="4"
stroke-dasharray="0 113" stroke-linecap="round" transform="rotate(-90 21 21)"></circle>
</svg>
<div>
<div class="label">Uptime (24 ساعة)</div>
<div class="gauge-value" id="uptimeValue">--%</div>
<div class="gauge-sub" id="uptimeChecks">-- فحص</div>
</div>
</div>
<div class="card">
<div class="label">شهادة SSL</div>
<div class="ssl-value" id="sslDays">-- يوم</div>
<div id="sslBadge" class="ssl-badge">--</div>
<div class="stat-row"><span>تنتهي في</span><b id="sslExpiry">--</b></div>
</div>
<div class="card">
<div class="label">آخر قياس</div>
<div class="stat-row"><span>Latency</span><b id="latestLatency">--</b></div>
<div class="stat-row"><span>DNS</span><b id="latestDns">--</b></div>
<div class="stat-row"><span>Search</span><b id="latestSearch">--</b></div>
<div class="stat-row"><span>HTTP Status</span><b id="latestStatus">--</b></div>
</div>
</div>
<div class="card chart-card" style="margin-bottom:14px;">
<div class="label">زمن الاستجابة (آخر ساعة)</div>
<canvas id="latencyChart"></canvas>
</div>
<div class="card">
<div class="label">آخر 10 فحوصات</div>
<table>
<thead>
<tr><th>الوقت</th><th>الحالة</th><th>Latency</th><th>DNS</th><th>Search</th><th>SSL (أيام)</th></tr>
</thead>
<tbody id="logBody"></tbody>
</table>
</div>
<footer>يقرأ من /api/metrics · يُحدَّث كل 30 ثانية</footer>
</div>
<script>
const API = '/api/metrics';
const POLL_MS = 30000;
function fmtMs(v){ return (v == null) ? '--' : Math.round(v) + ' ms'; }
function fmtTime(iso){
if(!iso) return '--';
const d = new Date(iso);
return d.toLocaleTimeString('ar-EG', {hour:'2-digit', minute:'2-digit', second:'2-digit'});
}
function setUptimeGauge(percent){
const circumference = 2 * Math.PI * 18; // ~113
const arc = document.getElementById('uptimeArc');
const p = percent == null ? 0 : percent;
const filled = (p / 100) * circumference;
arc.setAttribute('stroke-dasharray', `${filled} ${circumference}`);
arc.style.stroke = p >= 99 ? 'var(--ok)' : (p >= 95 ? 'var(--warn)' : 'var(--bad)');
}
function drawLatencyChart(series){
const canvas = document.getElementById('latencyChart');
const ctx = canvas.getContext('2d');
const dpr = window.devicePixelRatio || 1;
const w = canvas.clientWidth, h = canvas.clientHeight;
canvas.width = w * dpr; canvas.height = h * dpr;
ctx.scale(dpr, dpr);
ctx.clearRect(0,0,w,h);
if(!series || series.length < 2){
ctx.fillStyle = '#3a4a5c';
ctx.font = '12px monospace';
ctx.fillText('بيانات غير كافية بعد...', 10, h/2);
return;
}
const values = series.map(p => p.latency_ms);
const max = Math.max(...values, 10) * 1.2;
const stepX = w / (series.length - 1);
ctx.strokeStyle = '#5aa9ff';
ctx.lineWidth = 2;
ctx.beginPath();
values.forEach((v,i) => {
const x = i * stepX;
const y = h - (v / max) * (h - 10) - 5;
i === 0 ? ctx.moveTo(x,y) : ctx.lineTo(x,y);
});
ctx.stroke();
ctx.lineTo((values.length-1)*stepX, h);
ctx.lineTo(0, h);
ctx.closePath();
ctx.fillStyle = 'rgba(90,169,255,0.12)';
ctx.fill();
}
function renderSSL(ssl){
const badge = document.getElementById('sslBadge');
const days = ssl.days_remaining;
document.getElementById('sslDays').textContent = (days == null) ? '--' : `${days} يوم`;
document.getElementById('sslExpiry').textContent = ssl.expires_at ? new Date(ssl.expires_at).toLocaleDateString('ar-EG') : '--';
if(ssl.valid === false){
badge.className = 'ssl-badge bad'; badge.textContent = 'غير صالحة';
} else if(days == null){
badge.className = 'ssl-badge'; badge.textContent = 'بلا بيانات';
} else if(days > 14){
badge.className = 'ssl-badge good'; badge.textContent = 'صالحة';
} else if(days > 3){
badge.className = 'ssl-badge warn'; badge.textContent = 'تقترب من الانتهاء';
} else {
badge.className = 'ssl-badge bad'; badge.textContent = 'خطر - قرب الانتهاء';
}
}
function renderLog(checks){
const body = document.getElementById('logBody');
body.innerHTML = '';
checks.forEach(c => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${fmtTime(c.timestamp)}</td>
<td><span class="pill ${c.up ? 'up' : 'down'}"></span>${c.up ? 'يعمل' : 'متوقف'} (${c.status_code ?? '--'})</td>
<td>${fmtMs(c.latency_ms)}</td>
<td>${fmtMs(c.dns_ms)}</td>
<td>${fmtMs(c.search_response_ms)}</td>
<td>${c.ssl_days_remaining ?? '--'}</td>
`;
body.appendChild(tr);
});
}
async function poll(){
try{
const res = await fetch(API, {cache:'no-store'});
const data = await res.json();
document.getElementById('uptimeValue').textContent = (data.uptime_24h_percent ?? '--') + '%';
document.getElementById('uptimeChecks').textContent = `${data.checks_in_24h} فحص`;
setUptimeGauge(data.uptime_24h_percent);
renderSSL(data.ssl || {});
drawLatencyChart(data.latency_series);
renderLog(data.last_checks || []);
const latest = data.latest || {};
document.getElementById('latestLatency').textContent = fmtMs(latest.latency_ms);
document.getElementById('latestDns').textContent = fmtMs(latest.dns_ms);
document.getElementById('latestSearch').textContent = fmtMs(latest.search_response_ms);
document.getElementById('latestStatus').textContent = latest.status_code ?? '--';
document.getElementById('lastUpdate').textContent = fmtTime(data.generated_at);
}catch(err){
document.getElementById('lastUpdate').textContent = 'خطأ في الاتصال';
}
}
poll();
setInterval(poll, POLL_MS);
window.addEventListener('resize', () => poll());
</script>
</body>
</html>
+320
View File
@@ -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تم إيقاف المراقبة بواسطة المستخدم.")