Ghaymah SRE exam submission - Mohamed Adel
هذا الالتزام موجود في:
33
README.md
Normal file
33
README.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Ghaymah Internship Exam — SRE Track
|
||||
|
||||
**المتقدم:** Mohamed Adel
|
||||
**Qabilah Profile:** https://qabilah.com/profile/muhammedadelofficial01/professional-profile
|
||||
|
||||
## الروابط المنشورة فعليًا على Ghaymah
|
||||
|
||||
| السؤال | الرابط | الحالة |
|
||||
|---|---|---|
|
||||
| Q1 — Deploy & Monitor (API) | https://ghaymah-exam-app-06b532d0a81b.hosted.ghaymah.systems | ✅ منشور، `/health` و `/metrics` يعملان بنجاح |
|
||||
| Q1 — Dashboard | `q1-deploy-monitor/dashboard.html` (مربوط تلقائيًا بالرابط أعلاه) | ✅ |
|
||||
| Q5 — Mithal Monitor + Dashboard | https://ghaymah-mithal-monitor-de4c1ce11f85.hosted.ghaymah.systems/dashboard.html | ✅ منشور، البيانات تُجمع كل دقيقة في `metrics.json` |
|
||||
|
||||
---
|
||||
|
||||
## هيكل التسليم
|
||||
|
||||
| المجلد | المحتوى |
|
||||
|---|---|
|
||||
| `q1-deploy-monitor/` | Dockerfile + app.py (Flask API مع `/health`) + health-check.sh (فحص كل 30 ثانية) + dashboard.html |
|
||||
| `q2-postmortem/` | تقرير Postmortem كامل: ملخص، timeline، root cause، سياسة auto-scaling، اكتشاف مبكر |
|
||||
| `q3-cicd/` | GitHub Actions workflow (build → push → staging → manual approval → production) + شرح الفرق بين البيئتين وربط Ghaymah CLI |
|
||||
| `q4-scalability/` | architecture.png + حسابات عدد الحاويات (39 حاوية لـ 15,000 req/s) + استراتيجية cold start + شرح Block Storage |
|
||||
| `q5-mithal-monitor/` | monitor.py (latency, uptime, SSL, DNS, search) + dashboard.html |
|
||||
| `common-mortakaz/` | اقتراحي تكامل: منصة "مقابلة" (Mock Interviews) ومنصة السيرة الذاتية |
|
||||
| `common-qabilah/` | رابط الحساب الشخصي على قبيلة |
|
||||
|
||||
## ملاحظات مهمة قبل التسليم
|
||||
|
||||
- **النشر الفعلي على ghaymah.systems (Q1, Q5):** تم النشر فعليًا (انظر جدول الروابط أعلاه) وتم التحقق يدويًا من عمل `/health` و `/metrics` (Q1) و `/dashboard.html` + `metrics.json` (Q5) بنجاح.
|
||||
- **متطلبات Q3 (Secrets):** الـ workflow يفترض وجود 3 secrets في إعدادات المستودع: `GHAYMAH_REGISTRY_USER`, `GHAYMAH_REGISTRY_TOKEN`, `GHAYMAH_API_TOKEN` — لازم تضيفهم من حسابك الفعلي قبل تشغيل الـ pipeline.
|
||||
- **Q5 — رابط بحث mithal.space:** الكود يفترض endpoint افتراضي `/search` — لو الموقع الفعلي له مسار بحث مختلف، عدّل المتغير `search_path` في `monitor.py`.
|
||||
- **رفع المستودع على GitPasha:** كل الملفات جاهزة هنا؛ الخطوة المتبقية هي إنشاء المستودع `ghaymah-exam-mohamed-adel-sre` على GitPasha ورفع هذا الهيكل كما هو.
|
||||
34
common-mortakaz/integration-1.md
Normal file
34
common-mortakaz/integration-1.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# اقتراح تكامل #1 — منصة "مقابلة" (Moqabala) × Ghaymah
|
||||
|
||||
## وصف المنتج
|
||||
**مقابلة** منصة عربية على مرتكز متخصصة في محاكاة المقابلات التقنية الوظيفية (Mock Interviews)، تساعد المتقدمين على الاستعداد الحقيقي لسوق العمل من خلال محاكاة مقابلات فعلية بثقة واحترافية.
|
||||
|
||||
## كيف تتكامل مع ghaymah.systems
|
||||
|
||||
المشكلة التقنية المتوقعة لمنصة زي "مقابلة": جلسات المحاكاة (خاصة لو فيها فيديو/صوت حي أو AI يجاوب فورًا) تحتاج بنية تحتية قابلة للتوسع بشكل متغير جدًا — عدد المستخدمين يزيد بشكل حاد في أوقات معينة (بعد إعلانات توظيف كبيرة، أو مواسم التخرج) وينخفض في أوقات أخرى.
|
||||
|
||||
**التكامل المقترح:**
|
||||
- استضافة backend المنصة (معالجة الصوت/الفيديو، تحليل إجابات المرشح، توليد أسئلة المقابلة) على **Ghaymah Containers** مع auto-scaling، بحيث تتوسع المنصة تلقائيًا وقت الذروة (مثل موسم التقديم على وظائف Junior بعد التخرج) بدون تدخل يدوي.
|
||||
- استخدام **Ghaymah Block Storage** لتخزين تسجيلات جلسات المحاكاة (فيديو/صوت) بشكل دائم ومنفصل عن الحاويات، بحيث يقدر المستخدم يرجع يشوف أداءه في مقابلات سابقة.
|
||||
- استخدام أدوات **مراقبة غيمة** لضمان أن زمن استجابة الذكاء الاصطناعي أثناء المقابلة الحية يبقى منخفضًا (latency-sensitive)، لأن أي تأخير هيكسر إحساس "المقابلة الحقيقية".
|
||||
|
||||
```
|
||||
[متصفح المستخدم] --(WebRTC/HTTP)--> [Ghaymah LB]
|
||||
|
|
||||
[Container Pool: AI Interview Engine]
|
||||
|
|
||||
-----------------------------------
|
||||
| |
|
||||
[Ghaymah Block Storage] [Monitoring: latency alerts]
|
||||
(تسجيلات المقابلات) (لضمان استجابة فورية أثناء الجلسة)
|
||||
```
|
||||
|
||||
## القيمة المضافة للمستخدم النهائي
|
||||
- استقرار المنصة وقت الضغط (مثل مواسم التوظيف) بدل تعليق أو بطء الخدمة.
|
||||
- استجابة سريعة أثناء المحاكاة الحية، وهو أهم عامل في تجربة "المقابلة الواقعية".
|
||||
- أرشيف موثوق لتسجيلات المقابلات السابقة يقدر المستخدم يرجعله في أي وقت لتتبع تحسّنه.
|
||||
|
||||
## التحديات التقنية أو التجارية المحتملة
|
||||
- **تكلفة التخزين**: تسجيلات الفيديو/الصوت تستهلك مساحة كبيرة مع نمو عدد المستخدمين، فلازم سياسة أرشفة/حذف تلقائي للتسجيلات القديمة.
|
||||
- **زمن الاستجابة الحرج (latency-sensitive)**: أي تأخر في استجابة الذكاء الاصطناعي أثناء المحاكاة الحية يفسد التجربة، فيحتاج التصميم استضافة قريبة جغرافيًا من قاعدة المستخدمين الأساسية.
|
||||
- **الخصوصية**: تسجيلات المقابلات تحتوي بيانات شخصية وصوت/صورة المستخدم، فلازم ضمان تشفير وسياسات وصول واضحة على Block Storage.
|
||||
43
common-mortakaz/integration-2.md
Normal file
43
common-mortakaz/integration-2.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# اقتراح تكامل #2 — منصة إنشاء السيرة الذاتية العربية × Ghaymah / mithal.space
|
||||
|
||||
## وصف المنتج
|
||||
منصة مجانية على مرتكز لإنشاء سيرة ذاتية احترافية متوافقة مع أنظمة التوظيف (ATS-friendly)، توفر قوالب متعددة وتقييمًا فوريًا للسيرة الذاتية، وتدعم اللغتين العربية والإنجليزية.
|
||||
|
||||
## كيف تتكامل مع ghaymah.systems أو mithal.space
|
||||
|
||||
**التكامل المقترح مع ghaymah.systems:**
|
||||
- نشر backend المنصة (توليد ملفات PDF، محرك التقييم الفوري للسيرة) كحاويات على Ghaymah، مع CI/CD pipeline (زي اللي بنيناه في Q3) لنشر تحديثات القوالب والتقييم بأمان عبر staging قبل production.
|
||||
- كل عملية "تقييم فوري" للسيرة الذاتية هي عملية معالجة نصية قد تكون ثقيلة نسبيًا (تحليل NLP) — فهي مرشح جيد للتوسع الأفقي التلقائي (auto-scaling) الموضح في Q4، خصوصًا وقت الذروة (بداية كل سنة دراسية أو موسم التوظيف).
|
||||
|
||||
**التكامل المقترح مع mithal.space:**
|
||||
- محرك البحث في mithal.space (زي اللي بنينا مراقبته في Q5) ممكن يُستخدم كطبقة بحث داخل منصة السيرة الذاتية نفسها — مثلاً: المستخدم يبحث عن "كلمات مفتاحية مطلوبة لوظيفة DevOps" أو "أمثلة صياغة لمهارات Kubernetes"، ومحرك mithal.space يرجع اقتراحات فورية تُدمج في السيرة الذاتية أثناء الكتابة.
|
||||
|
||||
```
|
||||
[المستخدم يكتب سيرته الذاتية]
|
||||
|
|
||||
[منصة السيرة الذاتية - Ghaymah Containers]
|
||||
| \
|
||||
[محرك التقييم الفوري] [استعلام بحث -> mithal.space Search API]
|
||||
| |
|
||||
[توليد PDF] [اقتراحات كلمات مفتاحية للوظيفة]
|
||||
|
|
||||
[Ghaymah Block Storage: حفظ نسخ السيرة الذاتية]
|
||||
```
|
||||
|
||||
## القيمة المضافة للمستخدم النهائي
|
||||
- سيرة ذاتية أقوى وأكثر توافقًا مع متطلبات الوظيفة الفعلية، بفضل دمج اقتراحات بحث حية من mithal.space بدل الاعتماد على قالب ثابت فقط.
|
||||
- استقرار الخدمة وسرعة توليد PDF حتى وقت الضغط (بداية السنة الدراسية / مواسم التقديم)، بفضل auto-scaling على غيمة.
|
||||
- حفظ آمن لنسخ متعددة من السيرة الذاتية عبر الوقت (تجربة كتب لها Mohamed Adel نفسه كباحث عن عمل — نوع الميزة اللي بيحتاجها فعليًا).
|
||||
|
||||
## التحديات التقنية أو التجارية المحتملة
|
||||
- **جودة اقتراحات البحث**: لازم يكون محرك mithal.space مدرَّب/مضبوط على مصطلحات سوق العمل تحديدًا، وإلا الاقتراحات هتكون عامة وغير مفيدة.
|
||||
- **زمن التكامل بين خدمتين منفصلتين**: أي استدعاء لـ mithal.space أثناء الكتابة الحية للسيرة الذاتية لازم يكون سريع جدًا (تحت ثانية)، وإلا هيبطئ تجربة الكتابة.
|
||||
- **نموذج العمل (Business Model)**: المنصة مجانية حاليًا — التكامل مع بنية تحتية مدفوعة (Ghaymah) يحتاج تفكير في كيفية تغطية التكلفة (خطة مدفوعة اختيارية، أو رعاية).
|
||||
|
||||
## أي المنتجين أكثر قابلية للتطبيق؟
|
||||
|
||||
**منصة السيرة الذاتية (اقتراح #2) أكثر قابلية للتطبيق فورًا**، للأسباب التالية:
|
||||
1. الحمل الحسابي أخف بكثير من محاكاة مقابلات بالفيديو/الصوت الحي، فتكلفة البنية التحتية على غيمة أقل والمخاطر التقنية أبسط.
|
||||
2. لا تحتاج معالجة حساسة بزمن استجابة فوري حرج (real-time latency) بنفس درجة "مقابلة"، فهامش الخطأ الهندسي أكبر.
|
||||
3. التكامل مع mithal.space (بحث نصي بسيط) أبسط تقنيًا من تكامل الذكاء الاصطناعي الصوتي/المرئي المطلوب في مقابلة.
|
||||
4. القيمة واضحة وقابلة للقياس فورًا (جودة سيرة ذاتية أفضل = فرصة توظيف أعلى)، وهي مشكلة يعيشها أي باحث عن عمل يوميًا.
|
||||
7
common-qabilah/qabilah-profile.txt
Normal file
7
common-qabilah/qabilah-profile.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Qabilah Profile — Mohamed Adel
|
||||
================================
|
||||
|
||||
Profile URL:
|
||||
https://qabilah.com/profile/muhammedadelofficial01/professional-profile
|
||||
|
||||
Followed: Ghaymah official account on Qabilah (@Ghaymah)
|
||||
30
q1-deploy-monitor/Dockerfile
Normal file
30
q1-deploy-monitor/Dockerfile
Normal file
@@ -0,0 +1,30 @@
|
||||
# ---- Base image ----
|
||||
FROM python:3.12-slim
|
||||
|
||||
# ---- Metadata ----
|
||||
LABEL maintainer="Mohamed Adel"
|
||||
LABEL description="Simple Flask API with /health endpoint, deployed on Ghaymah Containers"
|
||||
|
||||
# ---- Working directory ----
|
||||
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.py .
|
||||
|
||||
# ---- Create non-root user for security ----
|
||||
RUN useradd -m appuser
|
||||
USER appuser
|
||||
|
||||
# ---- Expose the app port ----
|
||||
EXPOSE 8080
|
||||
|
||||
# ---- Container-level healthcheck (used by Ghaymah / Docker runtime) ----
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')" || exit 1
|
||||
|
||||
# ---- Run the app with a production WSGI server ----
|
||||
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "--workers", "2", "app:app"]
|
||||
65
q1-deploy-monitor/app.py
Normal file
65
q1-deploy-monitor/app.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
Simple demo API for the Ghaymah SRE exam (Q1).
|
||||
|
||||
Endpoints:
|
||||
GET / -> basic welcome message
|
||||
GET /health -> liveness/readiness probe used by Ghaymah + monitoring script
|
||||
GET /metrics -> lightweight JSON metrics used by the dashboard
|
||||
(uptime, request count, avg response time)
|
||||
"""
|
||||
|
||||
import time
|
||||
from flask import Flask, jsonify
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
START_TIME = time.time()
|
||||
REQUEST_COUNT = 0
|
||||
TOTAL_RESPONSE_TIME = 0.0
|
||||
|
||||
|
||||
@app.before_request
|
||||
def _start_timer():
|
||||
global REQUEST_COUNT
|
||||
REQUEST_COUNT += 1
|
||||
app.config["_req_start"] = time.time()
|
||||
|
||||
|
||||
@app.after_request
|
||||
def _record_timing(response):
|
||||
global TOTAL_RESPONSE_TIME
|
||||
elapsed = time.time() - app.config.get("_req_start", time.time())
|
||||
TOTAL_RESPONSE_TIME += elapsed
|
||||
response.headers["X-Response-Time-ms"] = f"{elapsed * 1000:.2f}"
|
||||
return response
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return jsonify(message="Ghaymah SRE exam demo API is running", status="ok")
|
||||
|
||||
|
||||
@app.route("/health")
|
||||
def health():
|
||||
"""Used by: Ghaymah container platform health checks, and the
|
||||
external monitoring script (health-check.sh)."""
|
||||
return jsonify(status="healthy", uptime_seconds=round(time.time() - START_TIME, 2)), 200
|
||||
|
||||
|
||||
@app.route("/metrics")
|
||||
def metrics():
|
||||
"""Used by the dashboard.html to render status / avg response time / request count."""
|
||||
avg_response_ms = (
|
||||
(TOTAL_RESPONSE_TIME / REQUEST_COUNT) * 1000 if REQUEST_COUNT else 0
|
||||
)
|
||||
return jsonify(
|
||||
status="healthy",
|
||||
uptime_seconds=round(time.time() - START_TIME, 2),
|
||||
request_count=REQUEST_COUNT,
|
||||
avg_response_time_ms=round(avg_response_ms, 2),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# For local testing only; production uses gunicorn (see Dockerfile CMD)
|
||||
app.run(host="0.0.0.0", port=8080)
|
||||
212
q1-deploy-monitor/dashboard.html
Normal file
212
q1-deploy-monitor/dashboard.html
Normal file
@@ -0,0 +1,212 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ar" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>لوحة مراقبة التطبيق — Ghaymah SRE Exam</title>
|
||||
<style>
|
||||
:root{
|
||||
--bg:#0b0f0d;
|
||||
--panel:#101512;
|
||||
--line:#1e2a24;
|
||||
--amber:#ffb454;
|
||||
--green:#5fd88a;
|
||||
--red:#ff6b6b;
|
||||
--text:#d7e0da;
|
||||
--dim:#7c8f85;
|
||||
--mono: "IBM Plex Mono","SFMono-Regular",Consolas,monospace;
|
||||
}
|
||||
*{box-sizing:border-box;}
|
||||
body{
|
||||
margin:0;
|
||||
background:
|
||||
radial-gradient(circle at 20% -10%, #142019 0%, transparent 60%),
|
||||
var(--bg);
|
||||
color:var(--text);
|
||||
font-family:var(--mono);
|
||||
padding:28px 20px 60px;
|
||||
direction:rtl;
|
||||
}
|
||||
.wrap{max-width:980px;margin:0 auto;}
|
||||
header{
|
||||
display:flex;justify-content:space-between;align-items:flex-end;
|
||||
border-bottom:1px solid var(--line);
|
||||
padding-bottom:16px;margin-bottom:22px;flex-wrap:wrap;gap:10px;
|
||||
}
|
||||
h1{font-size:20px;margin:0;font-weight:600;letter-spacing:.5px;}
|
||||
h1 span{color:var(--green);}
|
||||
.sub{color:var(--dim);font-size:12px;margin-top:4px;}
|
||||
.controls{display:flex;gap:8px;align-items:center;}
|
||||
input[type=text]{
|
||||
background:var(--panel);border:1px solid var(--line);color:var(--text);
|
||||
padding:8px 10px;border-radius:6px;font-family:var(--mono);font-size:12px;width:260px;
|
||||
}
|
||||
button{
|
||||
background:var(--green);color:#04150a;border:none;border-radius:6px;
|
||||
padding:8px 14px;font-family:var(--mono);font-weight:600;font-size:12px;cursor:pointer;
|
||||
}
|
||||
button:hover{filter:brightness(1.08);}
|
||||
.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-bottom:20px;}
|
||||
@media(max-width:760px){.grid{grid-template-columns:1fr;}}
|
||||
.card{
|
||||
background:var(--panel);border:1px solid var(--line);border-radius:10px;
|
||||
padding:18px;position:relative;overflow:hidden;
|
||||
}
|
||||
.card .label{color:var(--dim);font-size:11px;text-transform:uppercase;letter-spacing:1px;}
|
||||
.card .value{font-size:30px;font-weight:700;margin-top:8px;}
|
||||
.status-up{color:var(--green);}
|
||||
.status-down{color:var(--red);}
|
||||
.dot{display:inline-block;width:9px;height:9px;border-radius:50%;margin-left:8px;}
|
||||
.dot.up{background:var(--green);box-shadow:0 0 8px var(--green);}
|
||||
.dot.down{background:var(--red);box-shadow:0 0 8px var(--red);}
|
||||
.panel{
|
||||
background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:18px;margin-bottom:16px;
|
||||
}
|
||||
.panel h2{font-size:13px;color:var(--dim);text-transform:uppercase;letter-spacing:1px;margin:0 0 14px;}
|
||||
#chart{width:100%;height:160px;display:block;}
|
||||
table{width:100%;border-collapse:collapse;font-size:12px;}
|
||||
th,td{text-align:right;padding:8px 6px;border-bottom:1px solid var(--line);}
|
||||
th{color:var(--dim);font-weight:500;}
|
||||
.pill{padding:2px 8px;border-radius:20px;font-size:11px;font-weight:600;}
|
||||
.pill.up{background:rgba(95,216,138,.12);color:var(--green);}
|
||||
.pill.down{background:rgba(255,107,107,.12);color:var(--red);}
|
||||
footer{color:var(--dim);font-size:11px;text-align:center;margin-top:30px;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
|
||||
<header>
|
||||
<div>
|
||||
<h1>لوحة <span>مراقبة</span> التطبيق</h1>
|
||||
<div class="sub">Ghaymah SRE Exam — Q1 · يقرأ من /metrics و /health كل 5 ثوانٍ</div>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<input type="text" id="apiUrl" placeholder="https://your-app.ghaymah.systems" value="https://ghaymah-exam-app-06b532d0a81b.hosted.ghaymah.systems">
|
||||
<button onclick="setUrl()">اتصال</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<div class="label">الحالة</div>
|
||||
<div class="value" id="statusValue"><span class="dot up"></span><span id="statusText">جاري التحميل...</span></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>
|
||||
|
||||
<div class="panel">
|
||||
<h2>زمن الاستجابة — آخر 20 فحص</h2>
|
||||
<svg id="chart" viewBox="0 0 900 160" preserveAspectRatio="none"></svg>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>سجل الفحوصات</h2>
|
||||
<table>
|
||||
<thead><tr><th>الوقت</th><th>الحالة</th><th>زمن الاستجابة</th></tr></thead>
|
||||
<tbody id="logBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<footer>يعمل الفحص كل 5 ثوانٍ في المتصفح · مصدر البيانات: نفس تطبيق Q1 (endpoint: /metrics)</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let API_URL = "";
|
||||
let history = []; // {t, ok, ms}
|
||||
const MAX_POINTS = 20;
|
||||
|
||||
function setUrl(){
|
||||
API_URL = document.getElementById('apiUrl').value.trim().replace(/\/$/, '');
|
||||
history = [];
|
||||
poll();
|
||||
}
|
||||
|
||||
function fmtTime(d){
|
||||
return d.toLocaleTimeString('ar-EG', {hour:'2-digit', minute:'2-digit', second:'2-digit'});
|
||||
}
|
||||
|
||||
async function poll(){
|
||||
const statusText = document.getElementById('statusText');
|
||||
const statusValue = document.getElementById('statusValue');
|
||||
const latencyValue = document.getElementById('latencyValue');
|
||||
const requestsValue = document.getElementById('requestsValue');
|
||||
|
||||
if(!API_URL){
|
||||
statusText.textContent = "أدخل رابط التطبيق فوق";
|
||||
return;
|
||||
}
|
||||
|
||||
const t0 = performance.now();
|
||||
let ok = false, ms = 0, reqCount = '—';
|
||||
try{
|
||||
const res = await fetch(API_URL + '/metrics', {cache:'no-store'});
|
||||
ms = Math.round(performance.now() - t0);
|
||||
if(res.ok){
|
||||
const data = await res.json();
|
||||
ok = true;
|
||||
reqCount = data.request_count ?? '—';
|
||||
}
|
||||
}catch(e){
|
||||
ms = Math.round(performance.now() - t0);
|
||||
ok = false;
|
||||
}
|
||||
|
||||
statusValue.innerHTML = `<span class="dot ${ok?'up':'down'}"></span><span class="${ok?'status-up':'status-down'}">${ok?'يعمل':'متوقف'}</span>`;
|
||||
latencyValue.textContent = ms + ' ms';
|
||||
requestsValue.textContent = reqCount;
|
||||
|
||||
const now = new Date();
|
||||
history.push({t: now, ok, ms});
|
||||
if(history.length > MAX_POINTS) history.shift();
|
||||
|
||||
drawChart();
|
||||
drawLog();
|
||||
|
||||
setTimeout(poll, 5000);
|
||||
}
|
||||
|
||||
function drawChart(){
|
||||
const svg = document.getElementById('chart');
|
||||
if(history.length < 2){ svg.innerHTML=''; return; }
|
||||
const w = 900, h = 160, pad = 10;
|
||||
const maxMs = Math.max(...history.map(p=>p.ms), 50);
|
||||
const step = (w - pad*2) / (MAX_POINTS - 1);
|
||||
let points = history.map((p,i)=>{
|
||||
const x = pad + i*step;
|
||||
const y = h - pad - ((p.ms / maxMs) * (h - pad*2));
|
||||
return `${x},${y}`;
|
||||
}).join(' ');
|
||||
svg.innerHTML = `
|
||||
<polyline fill="none" stroke="#5fd88a" stroke-width="2" points="${points}" />
|
||||
${history.map((p,i)=>{
|
||||
const x = pad + i*step;
|
||||
const y = h - pad - ((p.ms / maxMs) * (h - pad*2));
|
||||
return `<circle cx="${x}" cy="${y}" r="3" fill="${p.ok?'#5fd88a':'#ff6b6b'}" />`;
|
||||
}).join('')}
|
||||
`;
|
||||
}
|
||||
|
||||
function drawLog(){
|
||||
const body = document.getElementById('logBody');
|
||||
body.innerHTML = history.slice().reverse().slice(0,10).map(p=>`
|
||||
<tr>
|
||||
<td>${fmtTime(p.t)}</td>
|
||||
<td><span class="pill ${p.ok?'up':'down'}">${p.ok?'UP':'DOWN'}</span></td>
|
||||
<td>${p.ms} ms</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Auto-connect on load using the pre-filled deployed URL
|
||||
window.addEventListener('DOMContentLoaded', setUrl);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
66
q1-deploy-monitor/health-check.sh
Normal file
66
q1-deploy-monitor/health-check.sh
Normal file
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# health-check.sh
|
||||
# Polls the app's /health endpoint every 30 seconds, logs status + response
|
||||
# time to a CSV file that the dashboard (dashboard.html) can read, and
|
||||
# alerts (stderr + log) if the app is unhealthy for 3 consecutive checks.
|
||||
#
|
||||
# Usage:
|
||||
# ./health-check.sh https://your-app.ghaymah.systems
|
||||
#
|
||||
# Run in the background on the host, or as a sidecar container, or as a
|
||||
# systemd service (recommended for production on Ghaymah).
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
APP_URL="${1:-http://localhost:8080}"
|
||||
HEALTH_ENDPOINT="${APP_URL%/}/health"
|
||||
CHECK_INTERVAL=30 # seconds
|
||||
LOG_FILE="./metrics.csv"
|
||||
FAIL_THRESHOLD=3
|
||||
consecutive_failures=0
|
||||
|
||||
# Create CSV header if the file doesn't exist yet
|
||||
if [[ ! -f "$LOG_FILE" ]]; then
|
||||
echo "timestamp,status,http_code,response_time_ms" > "$LOG_FILE"
|
||||
fi
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
|
||||
}
|
||||
|
||||
log "Starting health monitor for: $HEALTH_ENDPOINT (interval: ${CHECK_INTERVAL}s)"
|
||||
|
||||
while true; do
|
||||
start_ms=$(date +%s%3N)
|
||||
|
||||
# -o /dev/null discards body, -w prints http_code, -s silent, -m 5 timeout 5s
|
||||
http_code=$(curl -s -o /dev/null -w "%{http_code}" -m 5 "$HEALTH_ENDPOINT")
|
||||
curl_exit=$?
|
||||
|
||||
end_ms=$(date +%s%3N)
|
||||
response_time_ms=$((end_ms - start_ms))
|
||||
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
if [[ $curl_exit -eq 0 && "$http_code" == "200" ]]; then
|
||||
status="UP"
|
||||
consecutive_failures=0
|
||||
log "OK - ${HEALTH_ENDPOINT} - ${http_code} - ${response_time_ms}ms"
|
||||
else
|
||||
status="DOWN"
|
||||
consecutive_failures=$((consecutive_failures + 1))
|
||||
log "FAIL - ${HEALTH_ENDPOINT} - http_code=${http_code:-none} curl_exit=${curl_exit} - failure #${consecutive_failures}"
|
||||
|
||||
if [[ $consecutive_failures -ge $FAIL_THRESHOLD ]]; then
|
||||
log "ALERT: ${consecutive_failures} consecutive failures! Application appears DOWN." >&2
|
||||
# In production: send this to Slack/Email/PagerDuty/Ghaymah alerting webhook, e.g.:
|
||||
# curl -s -X POST -H "Content-Type: application/json" \
|
||||
# -d "{\"text\":\"ALERT: ${APP_URL} is down (${consecutive_failures} consecutive failures)\"}" \
|
||||
# "$ALERT_WEBHOOK_URL"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "${timestamp},${status},${http_code:-0},${response_time_ms}" >> "$LOG_FILE"
|
||||
|
||||
sleep "$CHECK_INTERVAL"
|
||||
done
|
||||
2
q1-deploy-monitor/requirements.txt
Normal file
2
q1-deploy-monitor/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
flask==3.0.3
|
||||
gunicorn==22.0.0
|
||||
96
q2-postmortem/postmortem-report.md
Normal file
96
q2-postmortem/postmortem-report.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# تقرير حادثة (Postmortem) — انقطاع خدمة بسبب OOMKilled متكرر
|
||||
|
||||
**الحالة:** مغلق (Resolved)
|
||||
**التأثير:** انقطاع كامل للخدمة لمدة 45 دقيقة
|
||||
**الخطورة:** SEV-2
|
||||
|
||||
---
|
||||
|
||||
## 1. الملخص التنفيذي
|
||||
|
||||
تعطّل التطبيق المستضاف على منصة غيمة عن العمل لمدة **45 دقيقة**. السبب المباشر هو دخول الحاوية (Container) في حلقة تعطّل متكررة (**CrashLoop**) ناتجة عن **OOMKilled** — أي أن نظام التشغيل أنهى العملية (Process) بالقوة لأنها تجاوزت حد الذاكرة (Memory Limit) المخصص لها. كل مرة كانت المنصة تعيد تشغيل الحاوية تلقائيًا، كانت تعود لتستهلك نفس القدر من الذاكرة خلال دقائق وتتعطل من جديد، وهو ما جعل الخدمة غير متاحة بشكل شبه مستمر طوال فترة الحادثة.
|
||||
|
||||
---
|
||||
|
||||
## 2. الجدول الزمني (Timeline)
|
||||
|
||||
| الوقت (تقريبي) | الحدث |
|
||||
|---|---|
|
||||
| T+0 دقيقة | بدء ارتفاع غير طبيعي في استهلاك الذاكرة (Memory) للحاوية، غير ملحوظ بعد من الفريق |
|
||||
| T+3 دقيقة | استهلاك الذاكرة يتجاوز الحد المخصص (Memory Limit) → kernel OOM Killer يوقف العملية |
|
||||
| T+3 دقيقة | منصة غيمة تكتشف توقف الحاوية وتعيد تشغيلها تلقائيًا (Auto-restart) |
|
||||
| T+3 إلى T+40 دقيقة | حلقة متكررة: الحاوية تبدأ، تستهلك الذاكرة بسرعة، تُقتل (OOMKilled)، تُعاد تلقائيًا — والتطبيق غير مستجيب لمعظم الطلبات خلال هذه الفترة |
|
||||
| T+12 دقيقة | أول تنبيه (Alert) يصل لفريق العمليات بسبب فشل متكرر في health check |
|
||||
| T+15 دقيقة | بدء التحقيق: مراجعة السجلات (Logs) ولوحة المراقبة على غيمة |
|
||||
| T+25 دقيقة | تحديد السبب: تسريب ذاكرة (Memory Leak) تراكمي في مسار معالجة الطلبات الكبيرة (لم يكن هناك تحرير صحيح للـ cache بعد كل طلب) |
|
||||
| T+35 دقيقة | إجراء مؤقت (Mitigation): رفع حد الذاكرة (Memory Limit) للحاوية مؤقتًا + إعادة نشر (Redeploy) نسخة سابقة مستقرة (Rollback) |
|
||||
| T+45 دقيقة | استقرار الخدمة، عودة معدل الاستجابة الطبيعي، إغلاق الحادثة (Incident Resolved) |
|
||||
| T+1 يوم | إصلاح جذري: تعديل الكود لتحرير الـ cache بشكل صحيح، ونشره بعد اختبار في staging |
|
||||
|
||||
---
|
||||
|
||||
## 3. السبب الجذري (Root Cause)
|
||||
|
||||
- كان هناك **تسريب ذاكرة تدريجي (gradual memory leak)** في التطبيق: كائنات (objects) خاصة بمعالجة الطلبات الكبيرة كانت تبقى محتفظًا بها في الذاكرة (cache/in-memory buffer) دون تحرير بعد انتهاء الطلب.
|
||||
- مع تزايد عدد الطلبات، تراكم استهلاك الذاكرة تدريجيًا حتى تجاوز الحد الأقصى المحدد للحاوية (Memory Limit)، فقام نظام Linux (OOM Killer) بإنهاء العملية بالقوة — وهو ما يظهر في منصة غيمة كحالة **OOMKilled**.
|
||||
- **العامل المُضاعِف (contributing factor):** لم يكن هناك auto-scaling أو حد أدنى/أقصى مضبوط بذكاء (فقط نسخة واحدة Single Replica)، لذلك أي إعادة تشغيل كانت تعني توقف الخدمة بالكامل بدلاً من أن تستوعبها نسخة أخرى سليمة.
|
||||
- **عامل مُضاعِف آخر:** غياب تنبيه مبكر على اتجاه استهلاك الذاكرة (Memory Trend Alert) — التنبيه الوحيد كان بعد فشل health check، أي بعد وقوع المشكلة فعليًا وليس قبلها.
|
||||
|
||||
---
|
||||
|
||||
## 4. الإجراءات التصحيحية (Action Items)
|
||||
|
||||
| # | الإجراء | الأولوية | المسؤول |
|
||||
|---|---|---|---|
|
||||
| 1 | إصلاح تسريب الذاكرة في كود معالجة الطلبات (تحرير الـ cache/buffers بعد كل طلب) | عالية | فريق التطوير |
|
||||
| 2 | ضبط `resources.requests` و `resources.limits` بشكل واقعي بناءً على قياس فعلي لاستهلاك التطبيق تحت حمل حقيقي (Load Test) | عالية | SRE |
|
||||
| 3 | تفعيل **Horizontal Auto-scaling** (2 نسخ كحد أدنى) حتى لا تعتمد الخدمة على نسخة واحدة (انظر السياسة في القسم 5) | عالية | SRE |
|
||||
| 4 | إضافة تنبيه استباقي عند وصول استهلاك الذاكرة إلى 80% من الحد المخصص (قبل الوصول لـ OOM) | متوسطة | SRE |
|
||||
| 5 | إضافة اختبار حمل (Load/Soak Test) دوري في CI قبل كل نشر يفحص استقرار استهلاك الذاكرة مع الوقت | متوسطة | فريق التطوير |
|
||||
| 6 | توثيق runbook لحالة OOMKilled ليستخدمه أي مهندس مناوب مستقبلًا لتقليل زمن التحقيق (MTTR) | منخفضة | SRE |
|
||||
|
||||
---
|
||||
|
||||
## 5. سياسة Auto-Scaling المقترحة لمنصة غيمة (لمنع التكرار)
|
||||
|
||||
**الهدف:** ألا يعتمد استقرار الخدمة على حاوية واحدة، وأن يتم استيعاب أي ضغط ذاكرة/معالج تلقائيًا قبل أن يتحول لانقطاع كامل.
|
||||
|
||||
```yaml
|
||||
# مثال توضيحي لسياسة auto-scaling على منصة غيمة
|
||||
scaling_policy:
|
||||
min_replicas: 2 # لا تقل عن نسختين دائمًا (لا يوجد Single Point of Failure)
|
||||
max_replicas: 6
|
||||
metrics:
|
||||
- type: memory
|
||||
target_utilization: 70% # التوسع عند وصول استهلاك الذاكرة لـ 70% من الحد المخصص
|
||||
- type: cpu
|
||||
target_utilization: 75%
|
||||
scale_up:
|
||||
cooldown_seconds: 60 # استجابة سريعة عند الضغط
|
||||
scale_down:
|
||||
cooldown_seconds: 300 # تقليل النسخ بحذر لتفادي "flapping"
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "250m"
|
||||
limits:
|
||||
memory: "512Mi" # حد واقعي مبني على قياس فعلي، وليس تخمينًا
|
||||
cpu: "500m"
|
||||
restart_policy:
|
||||
max_restarts_before_alert: 3 # تنبيه فوري (لا انتظار) بعد 3 إعادة تشغيل متتالية لنفس الحاوية
|
||||
```
|
||||
|
||||
**لماذا هذا يمنع تكرار الحادثة:**
|
||||
- `min_replicas: 2` يعني أن فشل حاوية واحدة بـ OOMKilled لا يوقف الخدمة بالكامل — الحاويات الأخرى تستمر في خدمة الطلبات.
|
||||
- التوسع على أساس استهلاك الذاكرة (وليس فقط CPU) يستوعب زيادة الحمل قبل أن تصل أي حاوية لحد OOM.
|
||||
- `max_restarts_before_alert` يحوّل "إعادة التشغيل الصامتة والمتكررة" (اللي حصلت في هذا الحادث) إلى تنبيه فوري بدل ما تستمر لمدة 45 دقيقة قبل ما حد يلاحظ.
|
||||
|
||||
---
|
||||
|
||||
## 6. كيفية اكتشاف هذه المشكلة مبكرًا باستخدام أدوات مراقبة غيمة
|
||||
|
||||
1. **Memory Trend Alerting:** ضبط تنبيه على لوحة مراقبة غيمة عند وصول استهلاك الذاكرة لنسبة 80% من الـ limit لمدة تزيد عن دقيقتين متتاليتين (وليس فقط عند الفشل الفعلي). هذا كان سيعطي إنذارًا مبكرًا قبل أول OOMKilled بعدة دقائق.
|
||||
2. **Restart Count Alerting:** تنبيه فوري عند تجاوز عدد إعادة التشغيل التلقائي (auto-restart) لحاوية واحدة أكثر من مرتين خلال 10 دقائق — بدل الاعتماد فقط على فشل health check.
|
||||
3. **Container Event Logs:** مراجعة دورية لسجل أحداث المنصة (Container Events) اللي يسجل صراحة سبب إعادة التشغيل (OOMKilled / Crash / Manual)، بدل الاعتماد فقط على سجل التطبيق نفسه.
|
||||
4. **Dashboard واحد يجمع:** Memory usage % + Restart count + Response time + Error rate في نفس الشاشة، حتى يقدر المهندس المناوب يربط بين الأعراض بسرعة (استجابة بطيئة + ارتفاع ذاكرة + إعادة تشغيل = إشارة قوية على OOM قادم).
|
||||
5. **Synthetic health checks خارجية** (مثل سكريبت `health-check.sh` في السؤال الأول) تعمل من خارج المنصة، لأن أدوات المراقبة الداخلية أحيانًا لا تكتشف أن الخدمة "بطيئة جدًا" حتى لو لم تكن "متوقفة تمامًا".
|
||||
51
q3-cicd/README.md
Normal file
51
q3-cicd/README.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Q3 — CI/CD Pipeline على غيمة
|
||||
|
||||
يشرح هذا الملف `workflow.yml` بالإضافة للفرق بين staging و production وطريقة الربط بـ Ghaymah CLI.
|
||||
|
||||
## 1. الفرق بين Staging و Production
|
||||
|
||||
| | Staging | Production |
|
||||
|---|---|---|
|
||||
| **الغرض** | بيئة تحقق قبل الإطلاق: نشر أوتوماتيكي لأي تعديل على `main` للتأكد أن الصورة (image) الجديدة تعمل فعليًا | البيئة الحقيقية التي يستخدمها المستخدم النهائي |
|
||||
| **من يوافق؟** | لا يوجد — نشر تلقائي بمجرد نجاح البناء | يتطلب موافقة يدوية (Manual Approval) من مراجع معتمد قبل التنفيذ |
|
||||
| **البيانات** | بيانات تجريبية/مصغّرة، منفصلة تمامًا عن بيانات المستخدمين الحقيقيين | بيانات حقيقية وحساسة |
|
||||
| **الموارد** | نسخة واحدة أو نسختين، موارد أقل (توفير تكلفة) | حسب سياسة auto-scaling (انظر Q4/Q2)، موارد كافية لتحمل الحمل الحقيقي |
|
||||
| **التنبيهات عند الفشل** | إشعار للفريق فقط | تنبيه فوري (on-call) لأن أي عطل يؤثر على المستخدمين مباشرة |
|
||||
| **الدومين المستخدم في هذا المثال** | `staging.ghaymah-exam-app.ghaymah.systems` | `ghaymah-exam-app.ghaymah.systems` |
|
||||
|
||||
الفكرة الأساسية: نفس الـ image بالضبط (نفس الـ tag، نفس commit SHA) يمر من staging إلى production بدون إعادة بناء، لضمان أن ما تم اختباره هو نفسه ما يُنشر فعليًا.
|
||||
|
||||
## 2. كيف تعمل بوابة الموافقة اليدوية (Manual Approval)
|
||||
|
||||
الموافقة اليدوية **لا تُكتب كخطوة داخل الـ workflow نفسه**، بل تُفعَّل من إعدادات المستودع:
|
||||
|
||||
1. `Settings` → `Environments` → إنشاء بيئة اسمها `production`
|
||||
2. تفعيل `Required reviewers` واختيار الأشخاص المسموح لهم بالموافقة (مثلاً: قائد الفريق أو مهندس SRE أول)
|
||||
3. أي job في الـ workflow يستخدم `environment: name: production` (كما في `deploy-production` أعلاه) سيتوقف تلقائيًا وينتظر ضغط "Approve" من أحد المراجعين المحددين قبل أن يبدأ التنفيذ فعليًا.
|
||||
|
||||
## 3. طريقة الربط الفعلية (Docker Hub + Ghaymah Dashboard)
|
||||
|
||||
**ملاحظة مهمة:** واجهة Ghaymah الحالية (deploy.ghaymah.systems) بتدعم النشر عن طريق **ربط حساب Docker Hub** مباشرة من الداشبورد (زر "Deploy Application" → "Docker Hub")، مش عن طريق CLI مخصص. لذلك التدفق الفعلي للـ pipeline هو:
|
||||
|
||||
```bash
|
||||
# 1. بناء الصورة محليًا أو عبر GitHub Actions (خطوة build-and-push في workflow.yml)
|
||||
docker build -t mohamedadel777/ghaymah-exam-app:latest .
|
||||
|
||||
# 2. تسجيل الدخول لـ Docker Hub
|
||||
docker login
|
||||
|
||||
# 3. رفع الصورة
|
||||
docker push mohamedadel777/ghaymah-exam-app:latest
|
||||
```
|
||||
|
||||
ثم من داشبورد Ghaymah:
|
||||
`Deploy Application → Docker Hub → اختيار الصورة والـ tag → تحديد Port 8080 → Deploy Now`
|
||||
|
||||
**في CI (GitHub Actions)** الخطوة `build-and-push` في `workflow.yml` بتعمل بالضبط نفس الأمرين (build + push) تلقائيًا، وبعدين خطوات `deploy-staging` و `deploy-production` بترجع رسالة تفيدك إن فيه صورة جديدة جاهزة للـ redeploy — لأن Ghaymah لسه ما بتوفرش webhook/CLI عام لتفعيل الـ redeploy تلقائيًا وقت كتابة هذا الملف، فالخطوة الأخيرة (اختيار الـ tag الجديد والضغط Redeploy) بتتم يدويًا من الداشبورد. لو Ghaymah ضافت لاحقًا CLI أو Webhook رسمي للنشر، تُستبدل هذه الخطوة بأمر فعلي.
|
||||
|
||||
الـ Secrets المطلوبة في إعدادات المستودع (`Settings → Secrets and variables → Actions`):
|
||||
|
||||
| الاسم | القيمة |
|
||||
|---|---|
|
||||
| `DOCKERHUB_USERNAME` | يوزر Docker Hub (`mohamedadel777`) |
|
||||
| `DOCKERHUB_TOKEN` | Access Token من Docker Hub (Account Settings → Security → New Access Token) |
|
||||
103
q3-cicd/workflow.yml
Normal file
103
q3-cicd/workflow.yml
Normal file
@@ -0,0 +1,103 @@
|
||||
# .github/workflows/deploy.yml
|
||||
#
|
||||
# CI/CD pipeline: build Docker image -> push to Ghaymah Container Registry
|
||||
# -> deploy to staging automatically -> manual approval -> deploy to production.
|
||||
|
||||
name: Build and Deploy to Ghaymah
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch: {}
|
||||
|
||||
env:
|
||||
DOCKERHUB_USER: mohamedadel777
|
||||
IMAGE_NAME: ghaymah-exam-app
|
||||
|
||||
jobs:
|
||||
# ---------------------------------------------------------------------
|
||||
# 1) Build the image once and push it to Docker Hub, tagged with the
|
||||
# commit SHA. Ghaymah pulls the image from Docker Hub when you
|
||||
# deploy/redeploy from the dashboard (Deploy App -> Docker Hub).
|
||||
# ---------------------------------------------------------------------
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
image_tag: ${{ steps.meta.outputs.tag }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set image tag (short commit SHA)
|
||||
id: meta
|
||||
run: echo "tag=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKERHUB_USER }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.tag }}
|
||||
${{ env.DOCKERHUB_USER }}/${{ env.IMAGE_NAME }}:latest
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# 2) Deploy to STAGING automatically — no approval needed.
|
||||
# NOTE: Ghaymah's current dashboard doesn't expose a public
|
||||
# redeploy-by-API step, so this job posts a reminder / calls a
|
||||
# webhook if you set one up. If Ghaymah adds a CLI/API later,
|
||||
# replace the "Trigger redeploy" step below with the real command.
|
||||
# ---------------------------------------------------------------------
|
||||
deploy-staging:
|
||||
needs: build-and-push
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: staging
|
||||
url: https://ghaymah-mithal-monitor-de4c1ce11f85.hosted.ghaymah.systems
|
||||
steps:
|
||||
- name: New image pushed — ready for staging redeploy
|
||||
run: |
|
||||
echo "New image pushed: ${{ env.DOCKERHUB_USER }}/${{ env.IMAGE_NAME }}:${{ needs.build-and-push.outputs.image_tag }}"
|
||||
echo "Go to the Ghaymah dashboard -> your app -> Redeploy, and pick this tag from Docker Hub."
|
||||
# If/when Ghaymah exposes a redeploy webhook or CLI, call it here, e.g.:
|
||||
# curl -X POST "$GHAYMAH_REDEPLOY_WEBHOOK_URL"
|
||||
|
||||
- name: Smoke test staging /health
|
||||
run: |
|
||||
sleep 10
|
||||
curl -f https://ghaymah-exam-app-06b532d0a81b.hosted.ghaymah.systems/health
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# 3) Deploy to PRODUCTION — gated behind a manual approval.
|
||||
# The "environment: production" + protection rule configured in the
|
||||
# repo settings (Settings -> Environments -> production -> Required
|
||||
# reviewers) is what forces a human to click "Approve" in the
|
||||
# Actions tab before this job runs.
|
||||
# ---------------------------------------------------------------------
|
||||
deploy-production:
|
||||
needs: [build-and-push, deploy-staging]
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: production # <-- manual approval gate lives here (repo settings)
|
||||
url: https://ghaymah-exam-app-06b532d0a81b.hosted.ghaymah.systems
|
||||
steps:
|
||||
- name: New image ready for production redeploy
|
||||
run: |
|
||||
echo "Approved. Image ready: ${{ env.DOCKERHUB_USER }}/${{ env.IMAGE_NAME }}:${{ needs.build-and-push.outputs.image_tag }}"
|
||||
echo "Go to the Ghaymah dashboard -> your production app -> Redeploy, and pick this tag from Docker Hub."
|
||||
|
||||
- name: Smoke test production /health
|
||||
run: |
|
||||
sleep 10
|
||||
curl -f https://ghaymah-exam-app-06b532d0a81b.hosted.ghaymah.systems/health
|
||||
ثنائية
q4-scalability/architecture.png
Normal file
ثنائية
q4-scalability/architecture.png
Normal file
ملف ثنائي غير معروض.
|
بعد العرض: | الارتفاع: | الحجم: 141 KiB |
79
q4-scalability/calculations.md
Normal file
79
q4-scalability/calculations.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Q4 — قابلية التوسع وتوزيع الأحمال
|
||||
|
||||
## 1. Architecture Diagram
|
||||
|
||||
انظر `architecture.png` في نفس المجلد. المكونات الأساسية:
|
||||
|
||||
- **Clients → DNS → Load Balancer (Ghaymah LB):** توزيع الطلبات على الحاويات المتاحة بالتساوي (round-robin / least-connections).
|
||||
- **Container Pool (Auto-scaled):** مجموعة حاويات متطابقة بلا حالة (stateless)، يتحكم فيها Autoscaler بناءً على معدل الطلبات (RPS) واستهلاك CPU.
|
||||
- **Cache Layer (Redis):** يمتص جزءًا كبيرًا من الحمل عن قاعدة البيانات للبيانات المتكررة القراءة.
|
||||
- **Ghaymah Block Storage:** لتخزين أي بيانات يجب أن تبقى (stateful) بمعزل عن دورة حياة الحاوية القصيرة.
|
||||
- **Managed DB (Primary + Replica):** طبقة البيانات الدائمة.
|
||||
- **Monitoring & Health Checks:** يراقب كل حاوية ويغذي قرار التوسع (autoscaler) وينبه عند الأعطال.
|
||||
|
||||
## 2. حساب عدد الحاويات المطلوبة
|
||||
|
||||
**المعطيات:**
|
||||
- الحمل المطلوب: 15,000 req/s
|
||||
- سعة الحاوية الواحدة: 500 req/s
|
||||
- هامش أمان (Safety Margin): 30%
|
||||
|
||||
**الحساب:**
|
||||
|
||||
```
|
||||
عدد الحاويات الأساسي = 15,000 / 500 = 30 حاوية
|
||||
|
||||
مع هامش أمان 30%:
|
||||
عدد الحاويات النهائي = 30 × 1.30 = 39 حاوية
|
||||
```
|
||||
|
||||
**النتيجة: 39 حاوية (container replicas)**
|
||||
|
||||
**لماذا الهامش مهم:**
|
||||
- يمتص أي تذبذب مفاجئ في الحمل (spike) دون انتظار دورة scale-up كاملة.
|
||||
- يحافظ على أداء مستقر إذا كانت إحدى الحاويات تحت صيانة أو أعيد تشغيلها (rolling restart / deployment).
|
||||
- يمنع الوصول لحافة السعة القصوى للحاوية، وهو ما يقلل احتمال تكرار مشاكل مثل OOMKilled الموضحة في Q2.
|
||||
|
||||
**توصية عملية للإعداد على غيمة:**
|
||||
```yaml
|
||||
min_replicas: 15 # يغطي حمل أساسي منخفض في أوقات الهدوء
|
||||
max_replicas: 45 # يغطي 39 المطلوبة + هامش إضافي لأي spike غير متوقع
|
||||
target_rps_per_replica: 500
|
||||
scale_up_cooldown: 60s
|
||||
scale_down_cooldown: 300s
|
||||
```
|
||||
البدء بـ `min_replicas` أقل من الرقم النهائي (39) مع autoscaler سريع الاستجابة أفضل من تثبيت 39 حاوية دائمًا، لأنه يوفر التكلفة في أوقات الحمل المنخفض ويتوسع تلقائيًا وقت الذروة.
|
||||
|
||||
## 3. استراتيجية Cold Start للحاويات الجديدة
|
||||
|
||||
المشكلة: عندما يضيف الـ Autoscaler حاوية جديدة فجأة، تحتاج وقتًا لتصبح "جاهزة" (تحميل التطبيق، فتح اتصالات DB، تسخين الـ cache المحلي)؛ خلال هذا الوقت قد تستقبل طلبات وهي غير جاهزة فعليًا فتفشل أو تكون بطيئة.
|
||||
|
||||
**الاستراتيجية المقترحة:**
|
||||
|
||||
1. **Readiness Probe منفصلة عن Liveness Probe:**
|
||||
الحاوية لا تدخل ضمن قائمة الـ Load Balancer إلا بعد نجاح `/ready` (وليس فقط `/health`)، بحيث لا تستقبل طلبات وهي لسه بتشتغل.
|
||||
|
||||
2. **Pre-warming تدريجي (Gradual Traffic Ramp-up):**
|
||||
بدل ما الحاوية الجديدة تستقبل حصتها الكاملة من الطلبات فورًا، الـ Load Balancer يزيد نسبة الطلبات الموجهة لها تدريجيًا على مدار 30-60 ثانية (weighted routing) حتى تثبت جاهزتها تمامًا.
|
||||
|
||||
3. **Predictive / Proactive Scaling بدل Reactive فقط:**
|
||||
الاعتماد على نمط الحمل التاريخي (مثلاً: ذروة يومية معروفة الوقت) لبدء إضافة حاويات قبل الذروة الفعلية بدقائق، بدلاً من الانتظار حتى يرتفع الحمل فعليًا ثم البدء في cold start وقت الضغط.
|
||||
|
||||
4. **Minimum warm pool:**
|
||||
الاحتفاظ بعدد صغير من الحاويات "دافئة" فوق الحد الأدنى الفعلي (buffer بسيط)، بحيث أي زيادة مفاجئة تُستوعب فورًا من حاويات جاهزة بينما الـ Autoscaler يبدأ حاويات جديدة بالتوازي لتعويض الـ buffer.
|
||||
|
||||
5. **تقليل زمن الإقلاع نفسه (Application-level):**
|
||||
صور Docker خفيفة (multi-stage build، slim base image)، تأجيل تحميل أي بيانات غير ضرورية عند بدء التشغيل، واستخدام connection pooling بدل فتح اتصال جديد لكل حاوية عند الإقلاع.
|
||||
|
||||
## 4. استخدام Ghaymah Block Storage للـ Stateful Workloads
|
||||
|
||||
الحاويات في `Container Pool` بلا حالة (stateless) بتصميم — أي حاوية ممكن تتوقف أو تتجدد في أي وقت بدون فقدان بيانات، لأن أي بيانات يجب أن تبقى تُخزَّن خارج الحاوية نفسها.
|
||||
|
||||
**أين يُستخدم Ghaymah Block Storage هنا:**
|
||||
|
||||
- **ملفات يرفعها المستخدمون** (uploads، صور، مرفقات) — تُخزَّن على Block Storage مشترك بدل القرص المحلي للحاوية، بحيث أي حاوية (أيًا كانت) تقدر توصل لنفس الملف.
|
||||
- **بيانات تحتاج استمرارية بين إعادة التشغيل**: مثل قواعد بيانات محلية صغيرة، أو أنظمة قوائم انتظار (queues) تحتفظ بحالتها على القرص.
|
||||
- **Logs مؤقتة أو ملفات cache كبيرة** لا تناسب حجمها الذاكرة (RAM) لكن يجب أن تبقى بين عمليات إعادة التشغيل لتحليلها لاحقًا.
|
||||
|
||||
**لماذا هذا التصميم مهم لقابلية التوسع تحديدًا:**
|
||||
عندما يزيد الـ Autoscaler عدد الحاويات من 15 إلى 39 (كما في القسم 2)، كل حاوية جديدة تحتاج "ترى" نفس البيانات المشتركة فورًا دون نسخها يدويًا — وهو بالضبط ما يوفره Block Storage كطبقة تخزين مستقلة عن دورة حياة الحاوية، بعكس تخزين البيانات على القرص المحلي للحاوية والذي يختفي بمجرد إعادة تشغيلها أو استبدالها.
|
||||
17
q5-mithal-monitor/Dockerfile
Normal file
17
q5-mithal-monitor/Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
||||
# ---- Base image ----
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# ---- Copy app files ----
|
||||
COPY monitor.py .
|
||||
COPY dashboard.html .
|
||||
COPY entrypoint.sh .
|
||||
|
||||
RUN chmod +x entrypoint.sh
|
||||
|
||||
# ---- Expose the dashboard port ----
|
||||
EXPOSE 8080
|
||||
|
||||
# ---- Run monitor.py in the background + serve dashboard.html on 8080 ----
|
||||
CMD ["./entrypoint.sh"]
|
||||
164
q5-mithal-monitor/dashboard.html
Normal file
164
q5-mithal-monitor/dashboard.html
Normal file
@@ -0,0 +1,164 @@
|
||||
<!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:#0b0f0d; --panel:#101512; --line:#1e2a24;
|
||||
--green:#5fd88a; --red:#ff6b6b; --amber:#ffb454;
|
||||
--text:#d7e0da; --dim:#7c8f85;
|
||||
--mono:"IBM Plex Mono","SFMono-Regular",Consolas,monospace;
|
||||
}
|
||||
*{box-sizing:border-box;}
|
||||
body{margin:0;background:radial-gradient(circle at 20% -10%, #142019 0%, transparent 60%), var(--bg);
|
||||
color:var(--text);font-family:var(--mono);padding:28px 20px 60px;direction:rtl;}
|
||||
.wrap{max-width:1040px;margin:0 auto;}
|
||||
header{border-bottom:1px solid var(--line);padding-bottom:16px;margin-bottom:22px;}
|
||||
h1{font-size:20px;margin:0;font-weight:600;}
|
||||
h1 span{color:var(--green);}
|
||||
.sub{color:var(--dim);font-size:12px;margin-top:4px;}
|
||||
.grid{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:20px;}
|
||||
@media(max-width:820px){.grid{grid-template-columns:repeat(2,1fr);}}
|
||||
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:16px;}
|
||||
.card .label{color:var(--dim);font-size:11px;text-transform:uppercase;letter-spacing:1px;}
|
||||
.card .value{font-size:24px;font-weight:700;margin-top:8px;}
|
||||
.ok{color:var(--green);} .bad{color:var(--red);} .warn{color:var(--amber);}
|
||||
.panel{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:18px;margin-bottom:16px;}
|
||||
.panel h2{font-size:13px;color:var(--dim);text-transform:uppercase;letter-spacing:1px;margin:0 0 14px;}
|
||||
#chart{width:100%;height:160px;display:block;}
|
||||
table{width:100%;border-collapse:collapse;font-size:12px;}
|
||||
th,td{text-align:right;padding:8px 6px;border-bottom:1px solid var(--line);}
|
||||
th{color:var(--dim);font-weight:500;}
|
||||
.pill{padding:2px 8px;border-radius:20px;font-size:11px;font-weight:600;}
|
||||
.pill.up{background:rgba(95,216,138,.12);color:var(--green);}
|
||||
.pill.down{background:rgba(255,107,107,.12);color:var(--red);}
|
||||
.note{color:var(--dim);font-size:11px;margin-top:6px;}
|
||||
footer{color:var(--dim);font-size:11px;text-align:center;margin-top:30px;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header>
|
||||
<h1>لوحة مراقبة <span>mithal.space</span></h1>
|
||||
<div class="sub">Ghaymah SRE Exam — Q5 · يقرأ بيانات من metrics.json (ينتجها monitor.py كل دقيقة)</div>
|
||||
</header>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<div class="label">Uptime (24 ساعة)</div>
|
||||
<div class="value" id="uptimeValue">—</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">آخر زمن استجابة</div>
|
||||
<div class="value" id="latencyValue">—</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">شهادة SSL</div>
|
||||
<div class="value" id="sslValue">—</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">زمن DNS</div>
|
||||
<div class="value" id="dnsValue">—</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>زمن الاستجابة — آخر ساعة</h2>
|
||||
<svg id="chart" viewBox="0 0 1000 160" preserveAspectRatio="none"></svg>
|
||||
<div class="note">كل نقطة = فحص واحد (كل دقيقة تقريبًا)</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>سجل آخر 10 فحوصات</h2>
|
||||
<table>
|
||||
<thead><tr><th>الوقت</th><th>الحالة</th><th>Status Code</th><th>زمن الاستجابة</th><th>DNS</th><th>Search</th></tr></thead>
|
||||
<tbody id="logBody"><tr><td colspan="6">جاري تحميل البيانات من metrics.json...</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<footer>مصدر البيانات: metrics.json الناتج عن monitor.py (نفس المجلد) · حدّث الصفحة لتحديث البيانات، أو استضف monitor.py على Ghaymah كمهمة مجدولة (cron/scheduled task)</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function loadData(){
|
||||
let data = [];
|
||||
try{
|
||||
const res = await fetch('./metrics.json', {cache:'no-store'});
|
||||
data = await res.json();
|
||||
}catch(e){
|
||||
document.getElementById('logBody').innerHTML =
|
||||
'<tr><td colspan="6">تعذر تحميل metrics.json — تأكد أن monitor.py يعمل وأنه بجانب هذا الملف على نفس الاستضافة.</td></tr>';
|
||||
return;
|
||||
}
|
||||
if(!data.length) return;
|
||||
|
||||
// Uptime % over last 24h (or all available data if less)
|
||||
const last24h = data.slice(-1440);
|
||||
const upCount = last24h.filter(d => d.up).length;
|
||||
const uptimePct = ((upCount / last24h.length) * 100).toFixed(2);
|
||||
const uptimeEl = document.getElementById('uptimeValue');
|
||||
uptimeEl.textContent = uptimePct + '%';
|
||||
uptimeEl.className = 'value ' + (uptimePct >= 99.9 ? 'ok' : uptimePct >= 99 ? 'warn' : 'bad');
|
||||
|
||||
const latest = data[data.length - 1];
|
||||
const latencyEl = document.getElementById('latencyValue');
|
||||
latencyEl.textContent = (latest.latency_ms ?? '—') + ' ms';
|
||||
latencyEl.className = 'value ' + (latest.up ? 'ok' : 'bad');
|
||||
|
||||
const sslEl = document.getElementById('sslValue');
|
||||
if(latest.ssl_valid){
|
||||
sslEl.textContent = latest.ssl_days_remaining + ' يوم متبقي';
|
||||
sslEl.className = 'value ' + (latest.ssl_days_remaining < 14 ? 'warn' : 'ok');
|
||||
} else {
|
||||
sslEl.textContent = 'غير صالحة';
|
||||
sslEl.className = 'value bad';
|
||||
}
|
||||
|
||||
document.getElementById('dnsValue').textContent = (latest.dns_ms ?? '—') + ' ms';
|
||||
|
||||
drawChart(data.slice(-60));
|
||||
drawLog(data.slice(-10).reverse());
|
||||
}
|
||||
|
||||
function drawChart(points){
|
||||
const svg = document.getElementById('chart');
|
||||
if(points.length < 2){ svg.innerHTML=''; return; }
|
||||
const w = 1000, h = 160, pad = 10;
|
||||
const vals = points.map(p => p.latency_ms || 0);
|
||||
const maxMs = Math.max(...vals, 50);
|
||||
const step = (w - pad*2) / (points.length - 1);
|
||||
const line = points.map((p,i) => {
|
||||
const x = pad + i*step;
|
||||
const y = h - pad - (((p.latency_ms||0) / maxMs) * (h - pad*2));
|
||||
return `${x},${y}`;
|
||||
}).join(' ');
|
||||
svg.innerHTML = `
|
||||
<polyline fill="none" stroke="#5fd88a" stroke-width="2" points="${line}" />
|
||||
${points.map((p,i)=>{
|
||||
const x = pad + i*step;
|
||||
const y = h - pad - (((p.latency_ms||0) / maxMs) * (h - pad*2));
|
||||
return `<circle cx="${x}" cy="${y}" r="2.5" fill="${p.up?'#5fd88a':'#ff6b6b'}" />`;
|
||||
}).join('')}
|
||||
`;
|
||||
}
|
||||
|
||||
function drawLog(rows){
|
||||
document.getElementById('logBody').innerHTML = rows.map(r => `
|
||||
<tr>
|
||||
<td>${new Date(r.timestamp).toLocaleTimeString('ar-EG')}</td>
|
||||
<td><span class="pill ${r.up?'up':'down'}">${r.up?'UP':'DOWN'}</span></td>
|
||||
<td>${r.status_code ?? '—'}</td>
|
||||
<td>${r.latency_ms ?? '—'} ms</td>
|
||||
<td>${r.dns_ms ?? '—'} ms</td>
|
||||
<td>${r.search_response_ms ?? '—'} ms</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
loadData();
|
||||
setInterval(loadData, 60000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
10
q5-mithal-monitor/entrypoint.sh
Normal file
10
q5-mithal-monitor/entrypoint.sh
Normal file
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
# Starts monitor.py in the background (checks mithal.space every 60s,
|
||||
# writes metrics.csv + metrics.json into /app), then serves the current
|
||||
# directory (including dashboard.html and metrics.json) over HTTP on 8080
|
||||
# so the dashboard can fetch('./metrics.json') directly.
|
||||
set -e
|
||||
|
||||
python3 monitor.py --url "${MONITOR_TARGET_URL:-https://mithal.space}" --interval 60 &
|
||||
|
||||
exec python3 -m http.server 8080
|
||||
172
q5-mithal-monitor/monitor.py
Normal file
172
q5-mithal-monitor/monitor.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""
|
||||
monitor.py — Monitoring script for mithal.space (Q5)
|
||||
|
||||
Collects every 60 seconds:
|
||||
- Latency: HTTP request response time
|
||||
- Uptime: is the site reachable? (status code)
|
||||
- SSL: certificate validity + days until expiry
|
||||
- DNS: DNS resolution time
|
||||
- Search Response: time to send a query and get a response
|
||||
|
||||
Stores results in both CSV and JSON so dashboard.html can read either.
|
||||
|
||||
Usage:
|
||||
python3 monitor.py --url https://mithal.space --interval 60
|
||||
python3 monitor.py --once # single run, useful for cron/testing
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import socket
|
||||
import ssl
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
CSV_FILE = Path(__file__).parent / "metrics.csv"
|
||||
JSON_FILE = Path(__file__).parent / "metrics.json"
|
||||
CSV_FIELDS = [
|
||||
"timestamp", "up", "status_code", "latency_ms",
|
||||
"dns_ms", "ssl_valid", "ssl_days_remaining",
|
||||
"search_ok", "search_response_ms", "error",
|
||||
]
|
||||
|
||||
|
||||
def check_dns(hostname: str):
|
||||
"""Returns DNS resolution time in ms, or None on failure."""
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
socket.gethostbyname(hostname)
|
||||
return round((time.perf_counter() - start) * 1000, 2)
|
||||
except socket.gaierror:
|
||||
return None
|
||||
|
||||
|
||||
def check_ssl(hostname: str, port: int = 443):
|
||||
"""Returns (is_valid, days_remaining) for the site's SSL certificate."""
|
||||
try:
|
||||
ctx = ssl.create_default_context()
|
||||
with socket.create_connection((hostname, port), timeout=5) as sock:
|
||||
with ctx.wrap_socket(sock, server_hostname=hostname) as ssock:
|
||||
cert = ssock.getpeercert()
|
||||
expiry = datetime.strptime(cert["notAfter"], "%b %d %H:%M:%S %Y %Z")
|
||||
expiry = expiry.replace(tzinfo=timezone.utc)
|
||||
days_remaining = (expiry - datetime.now(timezone.utc)).days
|
||||
return True, days_remaining
|
||||
except Exception:
|
||||
return False, None
|
||||
|
||||
|
||||
def check_http(url: str, timeout: int = 10):
|
||||
"""Returns (up, status_code, latency_ms, error)."""
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Ghaymah-SRE-Monitor/1.0"})
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
latency_ms = round((time.perf_counter() - start) * 1000, 2)
|
||||
return True, resp.status, latency_ms, None
|
||||
except urllib.error.HTTPError as e:
|
||||
latency_ms = round((time.perf_counter() - start) * 1000, 2)
|
||||
# site responded, just with an error status (e.g. 404/500)
|
||||
return e.code < 500, e.code, latency_ms, str(e)
|
||||
except Exception as e:
|
||||
latency_ms = round((time.perf_counter() - start) * 1000, 2)
|
||||
return False, None, latency_ms, str(e)
|
||||
|
||||
|
||||
def check_search(base_url: str, query: str = "test", timeout: int = 10):
|
||||
"""
|
||||
Simulates sending a search query to the site and measuring response time.
|
||||
Adjust `search_path` below to match mithal.space's actual search endpoint
|
||||
once known; falls back to hitting the homepage with a query string.
|
||||
"""
|
||||
search_path = "/search"
|
||||
url = f"{base_url.rstrip('/')}{search_path}?q={query}"
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Ghaymah-SRE-Monitor/1.0"})
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
elapsed_ms = round((time.perf_counter() - start) * 1000, 2)
|
||||
return resp.status < 400, elapsed_ms
|
||||
except Exception:
|
||||
elapsed_ms = round((time.perf_counter() - start) * 1000, 2)
|
||||
return False, elapsed_ms
|
||||
|
||||
|
||||
def run_check(url: str):
|
||||
parsed = urlparse(url)
|
||||
hostname = parsed.hostname
|
||||
|
||||
dns_ms = check_dns(hostname)
|
||||
up, status_code, latency_ms, error = check_http(url)
|
||||
ssl_valid, ssl_days = check_ssl(hostname)
|
||||
search_ok, search_ms = check_search(url)
|
||||
|
||||
return {
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"up": up,
|
||||
"status_code": status_code,
|
||||
"latency_ms": latency_ms,
|
||||
"dns_ms": dns_ms,
|
||||
"ssl_valid": ssl_valid,
|
||||
"ssl_days_remaining": ssl_days,
|
||||
"search_ok": search_ok,
|
||||
"search_response_ms": search_ms,
|
||||
"error": error,
|
||||
}
|
||||
|
||||
|
||||
def append_csv(row: dict):
|
||||
is_new = not CSV_FILE.exists()
|
||||
with open(CSV_FILE, "a", newline="") as f:
|
||||
writer = csv.DictWriter(f, fieldnames=CSV_FIELDS)
|
||||
if is_new:
|
||||
writer.writeheader()
|
||||
writer.writerow(row)
|
||||
|
||||
|
||||
def append_json(row: dict, keep_last: int = 1440):
|
||||
"""Keeps a rolling window of the last N checks (1440 = 24h at 1/min)."""
|
||||
data = []
|
||||
if JSON_FILE.exists():
|
||||
try:
|
||||
data = json.loads(JSON_FILE.read_text())
|
||||
except json.JSONDecodeError:
|
||||
data = []
|
||||
data.append(row)
|
||||
data = data[-keep_last:]
|
||||
JSON_FILE.write_text(json.dumps(data, indent=2))
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Monitor mithal.space")
|
||||
parser.add_argument("--url", default="https://mithal.space")
|
||||
parser.add_argument("--interval", type=int, default=60, help="seconds between checks")
|
||||
parser.add_argument("--once", action="store_true", help="run a single check and exit")
|
||||
args = parser.parse_args()
|
||||
|
||||
while True:
|
||||
result = run_check(args.url)
|
||||
append_csv(result)
|
||||
append_json(result)
|
||||
|
||||
status = "UP" if result["up"] else "DOWN"
|
||||
print(
|
||||
f"[{result['timestamp']}] {status} | "
|
||||
f"latency={result['latency_ms']}ms | dns={result['dns_ms']}ms | "
|
||||
f"ssl_days_left={result['ssl_days_remaining']} | "
|
||||
f"search={result['search_response_ms']}ms"
|
||||
)
|
||||
|
||||
if args.once:
|
||||
break
|
||||
time.sleep(args.interval)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
المرجع في مشكلة جديدة
حظر مستخدم