Initial Ghaymah tasks setup

هذا الالتزام موجود في:
momenlotfy
2026-07-28 20:41:37 +03:00
التزام 43e034fe64
16 ملفات معدلة مع 1181 إضافات و0 حذوفات

195
.github/workflows/deploy.yml مباع Normal file
عرض الملف

@@ -0,0 +1,195 @@
name: Build, Deploy & Monitor Ghaymah Application
on:
push:
branches:
- main
- develop
workflow_dispatch:
env:
APP_DIR: ./task1-deploy
jobs:
# ==================================
# Build & Validate
# ==================================
build:
name: Build Docker Image
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Build Docker Image
run: |
docker build \
-t training-api:${{ github.sha }} \
${{ env.APP_DIR }}
- name: Test Docker Image
run: |
docker run -d \
--name test-container \
-p 8080:8080 \
training-api:${{ github.sha }}
sleep 5
curl -f http://localhost:8080/health
docker stop test-container
# ==================================
# Deploy Staging
# ==================================
deploy-staging:
name: Deploy to Staging
needs: build
runs-on: ubuntu-latest
environment:
name: staging
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Ghaymah CLI
run: |
curl -sSl https://cli.ghaymah.systems/install.sh | bash
echo "$HOME/ghaymah/bin" >> $GITHUB_PATH
- name: Authenticate with Ghaymah
run: |
gy auth login \
--token "${{ secrets.GHAYMAH_API_TOKEN }}"
- name: Deploy Staging Application
working-directory: task1-deploy
run: |
gy resource app launch
- name: Smoke Test Staging
run: |
sleep 20
curl -f \
https://task1-deploy-b18316770d08.hosted.ghaymah.systems/health
# ==================================
# Manual Approval
# ==================================
approve-production:
name: Production Approval
needs: deploy-staging
runs-on: ubuntu-latest
environment:
name: production-approval
steps:
- name: Waiting for approval
run: |
echo "Production deployment approved"
# ==================================
# Production Deployment
# ==================================
deploy-production:
name: Deploy Production
needs:
- approve-production
runs-on: ubuntu-latest
environment:
name: production
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Ghaymah CLI
run: |
curl -sSl https://cli.ghaymah.systems/install.sh | bash
echo "$HOME/ghaymah/bin" >> $GITHUB_PATH
- name: Authenticate with Ghaymah
run: |
gy auth login \
--token "${{ secrets.GHAYMAH_API_TOKEN }}"
- name: Deploy Production Application
working-directory: task1-deploy
run: |
gy resource app launch
- name: Production Health Check
run: |
sleep 20
curl -f \
https://task1-deploy-b18316770d08.hosted.ghaymah.systems/health

27
README.md Normal file
عرض الملف

@@ -0,0 +1,27 @@
# مشروع تدريب: نشر ومراقبة على Ghaymah — الحل الكامل
هذا المشروع يغطي الأنشطة الخمسة المطلوبة:
| # | المجلد | المهمة |
|---|---|---|
| 1 | `task1-deploy/` | Dockerfile + API + /health + سكريبت مراقبة + dashboard |
| 2 | `task2-postmortem/` | تقرير Postmortem لحادثة OOMKilled + سياسة auto-scaling |
| 3 | `task3-cicd/` | GitHub Actions workflow + شرح staging/production + ghaymah CLI |
| 4 | `task4-scalability/` | Architecture diagram + حسابات 15,000 req/s + Cold start + Block Storage |
| 5 | `task5-mithal-dashboard/` | جامع مقاييس + dashboard لموقع mithal.space |
## ⚠️ ملاحظة مهمة عن أوامر Ghaymah CLI
لم أستطع الوصول لتوثيق تفصيلي حي لأوامر `ghaymah` CLI الدقيقة (الموقع الرسمي
docs.ghaymah.cloud مبني بجافاسكربت ولا يظهر المحتوى الكامل عبر البحث). لذلك
كل أوامر `ghaymah ...` في هذا المشروع هي **بالنمط القياسي المتوقع** لمنصات
الحاويات (login → build/push → deploy → status/logs → rollback)، وتحتاج منك
التأكد من الصياغة الدقيقة من https://docs.ghaymah.cloud أو من CLI نفسه عبر
`ghaymah --help` قبل الاستخدام الفعلي في التسليم.
## ترتيب مقترح للعمل
1. جرّب `task1-deploy` محلياً بـ Docker أولاً، ثم انشرها فعلياً على Ghaymah.
2. شغّل `monitor.sh` على الرابط المنشور وافتح `dashboard.html`.
3. اقرأ `task2-postmortem/postmortem.md` وطبّق سياسة الـ auto-scaling على نفس التطبيق إن أمكن.
4. أعدّ الـ CI/CD (`task3-cicd`) وفعّل الموافقة اليدوية في GitHub Environments.
5. استخدم حسابات `task4-scalability/scalability.md` كجزء من التقرير الكتابي.
6. شغّل `task5-mithal-dashboard/collector.py` وانشر اللوحة كما في المهمة 1.

عرض الملف

@@ -0,0 +1,17 @@
{
"id": "5ba192b0-d1f0-43a6-963d-be133af424b1",
"name": "task1-deploy",
"projectId": "34340b16-24f2-4db9-89e4-bf94bb372e68",
"ports": [
{
"expose": true,
"number": 8080
}
],
"publicAccess": {
"enabled": true,
"domain": "auto"
},
"resourceTier": "t1",
"dockerFileName": "Dockerfile"
}

21
task1-deploy/Dockerfile Normal file
عرض الملف

@@ -0,0 +1,21 @@
FROM python:3.11-slim AS base
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
RUN useradd -m appuser
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD python -c "import urllib.request,sys; sys.exit(0) if urllib.request.urlopen('http://localhost:8080/health').getcode()==200 else sys.exit(1)"
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "--workers", "2", "--timeout", "30", "app:app"]

85
task1-deploy/app.py Normal file
عرض الملف

@@ -0,0 +1,85 @@
import fcntl
import os
import time
from flask import Flask, jsonify
from flask_cors import CORS
app = Flask(__name__)
# السماح للـ dashboard.html بالوصول للـ API من المتصفح
CORS(app)
START_TIME = time.time()
# عداد الطلبات مشترك بين gunicorn workers
COUNTER_FILE = "/tmp/request_count.txt"
def _increment_and_read_count() -> int:
fd = os.open(COUNTER_FILE, os.O_RDWR | os.O_CREAT, 0o644)
try:
fcntl.flock(fd, fcntl.LOCK_EX)
data = os.read(fd, 64).decode().strip()
count = int(data) if data else 0
count += 1
os.lseek(fd, 0, os.SEEK_SET)
os.truncate(fd, 0)
os.write(fd, str(count).encode())
return count
finally:
fcntl.flock(fd, fcntl.LOCK_UN)
os.close(fd)
def _read_count() -> int:
if not os.path.exists(COUNTER_FILE):
return 0
with open(COUNTER_FILE, "r") as f:
data = f.read().strip()
return int(data) if data else 0
@app.before_request
def _count_requests():
_increment_and_read_count()
@app.route("/")
def home():
return jsonify({
"message": "Ghaymah Training API is running",
"status": "ok"
})
@app.route("/health")
def health():
uptime_seconds = round(time.time() - START_TIME, 2)
return jsonify({
"status": "healthy",
"uptime_seconds": uptime_seconds,
"requests_served": _read_count()
}), 200
@app.route("/metrics")
def metrics():
uptime_seconds = round(time.time() - START_TIME, 2)
return jsonify({
"uptime_seconds": uptime_seconds,
"requests_served": _read_count(),
"start_time": START_TIME
})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)

161
task1-deploy/dashboard.html Normal file
عرض الملف

@@ -0,0 +1,161 @@
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<title>لوحة مراقبة التطبيق - Ghaymah</title>
<style>
:root {
--up: #16a34a;
--down: #dc2626;
--bg: #0f172a;
--card: #1e293b;
--text: #e2e8f0;
--muted: #94a3b8;
}
* { box-sizing: border-box; }
body {
background: var(--bg);
color: var(--text);
font-family: "Segoe UI", Tahoma, sans-serif;
margin: 0;
padding: 24px;
}
h1 { margin-bottom: 4px; }
.sub { color: var(--muted); margin-bottom: 24px; }
.controls { margin-bottom: 20px; display: flex; gap: 8px; }
input {
flex: 1;
padding: 10px;
border-radius: 8px;
border: 1px solid #334155;
background: #0b1220;
color: var(--text);
}
button {
padding: 10px 16px;
border-radius: 8px;
border: none;
background: #2563eb;
color: white;
cursor: pointer;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
}
.card {
background: var(--card);
border-radius: 12px;
padding: 20px;
border: 1px solid #334155;
}
.card h3 { margin: 0 0 8px; color: var(--muted); font-size: 14px; font-weight: 500; }
.card .value { font-size: 28px; font-weight: 700; }
.badge {
display: inline-block;
padding: 4px 12px;
border-radius: 999px;
font-size: 14px;
font-weight: 700;
}
.badge.up { background: rgba(22,163,74,.15); color: var(--up); }
.badge.down { background: rgba(220,38,38,.15); color: var(--down); }
#log {
margin-top: 20px;
background: var(--card);
border-radius: 12px;
padding: 16px;
max-height: 260px;
overflow-y: auto;
font-family: monospace;
font-size: 13px;
}
.log-line { padding: 4px 0; border-bottom: 1px solid #334155; color: var(--muted); }
</style>
</head>
<body>
<h1>📊 لوحة مراقبة التطبيق</h1>
<div class="sub">يتم الفحص تلقائياً كل 5 ثوانٍ عبر /health و /metrics</div>
<div class="controls">
<input id="appUrl" value="https://task1-deploy-b18316770d08.hosted.ghaymah.systems" placeholder="رابط التطبيق مثال: https://myapp.ghaymah.systems">
<button onclick="startMonitoring()">ابدأ المراقبة</button>
</div>
<div class="grid">
<div class="card">
<h3>الحالة</h3>
<div class="value"><span id="statusBadge" class="badge down">غير معروف</span></div>
</div>
<div class="card">
<h3>زمن الاستجابة</h3>
<div class="value" id="latency">-- ms</div>
</div>
<div class="card">
<h3>عدد الطلبات المخدومة</h3>
<div class="value" id="reqCount">--</div>
</div>
<div class="card">
<h3>مدة تشغيل التطبيق</h3>
<div class="value" id="uptime">--</div>
</div>
</div>
<div id="log">
<div class="log-line">جاهز... اضغط "ابدأ المراقبة"</div>
</div>
<script>
let timer = null;
function fmtUptime(sec) {
const h = Math.floor(sec / 3600);
const m = Math.floor((sec % 3600) / 60);
const s = Math.floor(sec % 60);
return `${h}س ${m}د ${s}ث`;
}
function logLine(text, ok) {
const log = document.getElementById('log');
const div = document.createElement('div');
div.className = 'log-line';
div.style.color = ok ? '#16a34a' : '#dc2626';
div.textContent = text;
log.prepend(div);
while (log.children.length > 20) log.removeChild(log.lastChild);
}
async function checkOnce() {
const base = document.getElementById('appUrl').value.replace(/\/$/, '');
const start = performance.now();
try {
const res = await fetch(base + '/health', { cache: 'no-store' });
const latencyMs = Math.round(performance.now() - start);
const data = await res.json();
document.getElementById('statusBadge').textContent = res.ok ? 'يعمل ✅' : 'متعطل ❌';
document.getElementById('statusBadge').className = 'badge ' + (res.ok ? 'up' : 'down');
document.getElementById('latency').textContent = latencyMs + ' ms';
document.getElementById('reqCount').textContent = data.requests_served ?? '--';
document.getElementById('uptime').textContent = data.uptime_seconds ? fmtUptime(data.uptime_seconds) : '--';
logLine(`${new Date().toLocaleTimeString('ar')} — OK (${latencyMs}ms)`, true);
} catch (err) {
document.getElementById('statusBadge').textContent = 'متعطل ❌';
document.getElementById('statusBadge').className = 'badge down';
document.getElementById('latency').textContent = '-- ms';
logLine(`${new Date().toLocaleTimeString('ar')} — فشل الاتصال: ${err.message}`, false);
}
}
function startMonitoring() {
if (timer) clearInterval(timer);
checkOnce();
timer = setInterval(checkOnce, 5000);
}
</script>
</body>
</html>

عرض الملف

@@ -0,0 +1,6 @@
{
"timestamp": "2026-07-28 19:13:13",
"status": "UP",
"http_code": "200",
"response_ms": 1765
}

3
task1-deploy/monitor.log Normal file
عرض الملف

@@ -0,0 +1,3 @@
2026-07-28 19:12:12 | status=UP | http_code=200 | response_ms=721
2026-07-28 19:12:43 | status=UP | http_code=200 | response_ms=578
2026-07-28 19:13:13 | status=UP | http_code=200 | response_ms=1765

45
task1-deploy/monitor.sh Executable file
عرض الملف

@@ -0,0 +1,45 @@
#!/bin/bash
URL="${APP_URL:-http://localhost:8080/health}"
INTERVAL="${CHECK_INTERVAL:-30}"
LOG_FILE="${LOG_FILE:-monitor.log}"
ALERT_FILE="${ALERT_FILE:-alerts.log}"
METRICS_JSON="${METRICS_JSON:-metrics.json}"
MAX_TIMEOUT=5
echo "🔍 بدء المراقبة على: $URL (كل ${INTERVAL}s) — اضغط Ctrl+C للإيقاف"
while true; do
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
START_NS=$(date +%s%N)
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time "$MAX_TIMEOUT" "$URL")
CURL_EXIT=$?
END_NS=$(date +%s%N)
RESPONSE_MS=$(( (END_NS - START_NS) / 1000000 ))
if [ "$CURL_EXIT" -eq 0 ] && [ "$HTTP_CODE" == "200" ]; then
STATUS="UP"
else
STATUS="DOWN"
fi
LINE="$TIMESTAMP | status=$STATUS | http_code=$HTTP_CODE | response_ms=$RESPONSE_MS"
echo "$LINE"
echo "$LINE" >> "$LOG_FILE"
if [ "$STATUS" == "DOWN" ]; then
echo "🚨 $TIMESTAMP ALERT: الخدمة غير متاحة (http_code=$HTTP_CODE)" | tee -a "$ALERT_FILE"
fi
cat > "$METRICS_JSON" <<EOF
{
"timestamp": "$TIMESTAMP",
"status": "$STATUS",
"http_code": "$HTTP_CODE",
"response_ms": $RESPONSE_MS
}
EOF
sleep "$INTERVAL"
done

عرض الملف

@@ -0,0 +1,3 @@
flask==3.0.3
gunicorn==22.0.0
flask-cors

عرض الملف

@@ -0,0 +1,121 @@
# Incident Postmortem Report
## Application Outage Due to Repeated OOMKilled Events
## 1. Incident Overview
Incident ID: INC-2026-001
Severity: SEV-2
Duration: 45 minutes
Service: Ghaymah Hosted Application
Status: Resolved
The application experienced repeated downtime caused by Kubernetes OOMKilled events.
The container exceeded its allocated memory limit, causing Kubernetes to terminate
and restart the container repeatedly.
## 2. Executive Summary
On July 28, 2026, the application experienced intermittent availability issues
for approximately 45 minutes.
The root cause was excessive memory consumption inside the application container.
Once the container exceeded its configured memory limit, Kubernetes terminated
the container with an OOMKilled event.
The service recovered after adjusting resource configuration and implementing
a better scaling strategy.
## 3. Impact
During the incident:
- Users experienced 502/503 errors.
- API requests failed intermittently.
- Application availability decreased for 45 minutes.
- No data loss occurred because the application was stateless.
## 4. Detection
The incident was detected through:
- Kubernetes container restart events.
- OOMKilled status reported by the platform.
- Increased application response latency.
- Monitoring alerts triggered by high memory utilization.
## 5. Timeline
| Time | Event |
|---|---|
| 10:00 | Traffic increased above normal level |
| 10:02 | Container memory usage exceeded 90% |
| 10:04 | Kubernetes terminated container (OOMKilled) |
| 10:05 | New container instance started |
| 10:10 | Memory increased again and container crashed |
| 10:45 | Service stabilized after mitigation |
## 6. Root Cause Analysis
### Immediate Cause
The container exceeded its configured memory limit.
### Root Cause
The application was running with insufficient memory resources
and no horizontal scaling mechanism.
Possible contributing factors:
- Memory leak inside the application.
- Missing memory-based autoscaling.
- No early warning alerts.
- Single container handling all traffic.
## 7. Resolution
The incident was resolved by:
- Increasing container memory allocation.
- Restarting unhealthy workloads.
- Reviewing application memory usage.
- Preparing an autoscaling policy.
## 8. Preventive Actions
| Action | Priority |
|---|---|
| Enable Horizontal Pod Autoscaler | High |
| Add memory utilization alerts | High |
| Perform memory profiling | Medium |
| Improve health checks | Medium |
| Add resource limits based on metrics | High |
autoscaling:
minReplicas: 2
maxReplicas: 8
metrics:
memory:
targetUtilization: 70
cpu:
targetUtilization: 75
behavior:
scaleUp:
stabilizationWindowSeconds: 60
scaleDown:
stabilizationWindowSeconds: 300
resources:
requests:
memory: 256Mi
cpu: 500m
limits:
memory: 512Mi
cpu: 1000m
## Monitoring Strategy
The following monitoring improvements will be implemented:
- Memory usage dashboard.
- Alert when memory exceeds 75%.
- Critical alert when memory exceeds 90%.
- Monitor container restart count.
- Track OOMKilled events.
- Synthetic health checks.

عرض الملف

@@ -0,0 +1,113 @@
# المهمة 4 — قابلية التوسع وتوزيع الأحمال (15,000 req/s)
## 1) Architecture Diagram
```mermaid
flowchart TD
U[المستخدمون] --> DNS[Ghaymah DNS / CDN Edge]
DNS --> LB[Load Balancer<br/>غيمة]
LB --> C1[حاوية 1<br/>500 req/s]
LB --> C2[حاوية 2<br/>500 req/s]
LB --> C3[حاوية 3<br/>500 req/s]
LB --> C4[...]
LB --> C39[حاوية 39<br/>500 req/s]
C1 --> CACHE[(Redis Cache)]
C2 --> CACHE
C3 --> CACHE
C39 --> CACHE
CACHE --> DB[(قاعدة بيانات رئيسية)]
C1 --> BLOCK[(Ghaymah Block Storage<br/>للبيانات الدائمة)]
C39 --> BLOCK
subgraph Autoscaler["Auto-scaling Controller"]
METRICS[مقاييس CPU/Memory/RPS] --> DECIDE{تجاوز الحد؟}
DECIDE -- نعم --> ADD[إضافة حاويات جديدة]
DECIDE -- لا --> KEEP[إبقاء العدد الحالي]
end
LB -. تقرير المقاييس .-> METRICS
ADD -. توسيع .-> LB
```
**الفكرة:** طلبات المستخدمين تصل أولاً لـ Load Balancer الذي يوزّعها على مجموعة
حاويات متطابقة (horizontal scaling)، كل حاوية تتعامل مع الحمل الخاص بها وتتصل
بطبقة cache مشتركة قبل قاعدة البيانات لتقليل الضغط عليها، بينما البيانات الدائمة
(uploads, session files, ...) تُخزَّن على Ghaymah Block Storage القابل للربط
بأي حاوية جديدة.
---
## 2) حساب عدد الحاويات المطلوبة
المعطيات:
- الحمل المستهدف: **15,000 req/s**
- سعة الحاوية الواحدة: **500 req/s**
- هامش أمان: **30%** (لتفادي التشبع عند تذبذب الحمل أو فقدان حاوية)
**الحساب:**
```
عدد الحاويات الأساسي = 15,000 / 500 = 30 حاوية
مع هامش الأمان 30%:
30 × 1.30 = 39 حاوية
```
➡️ **العدد المطلوب فعلياً = 39 حاوية** (وليس 30)، بحيث لو سقطت بضع حاويات
أو ارتفع الحمل مؤقتاً 20-30% فوق المتوقع، النظام لا يصل للتشبع الكامل.
**توصية عملية للإعداد:**
```yaml
min_replicas: 12 # يغطي حمل القاعدة العادي (ليس ذروة اليوم)
max_replicas: 39 # يغطي ذروة 15,000 req/s + الهامش
target_cpu: 65%
target_rps_per_pod: 400 # هدف أقل من السعة القصوى (500) لإعطاء هامش استجابة
```
---
## 3) استراتيجية Cold Start للحاويات الجديدة
المشكلة: حاوية جديدة تحتاج وقتاً (تحميل صورة، تهيئة التطبيق، اتصال بقاعدة
البيانات) قبل أن تكون جاهزة فعلياً لاستقبال حمل — لو أرسل لها الـ LB طلبات
فوراً ستفشل أو تبطئ الاستجابة.
الحل المقترح:
1. **Readiness Probe صارم:** لا يُضاف الـ pod لقائمة الـ Load Balancer إلا
بعد نجاح فحص `/health` عدة مرات متتالية (مثلاً 3 نجاحات متتالية كل 5 ثوانٍ).
2. **Pre-warmed pool (نسخ دافئة جاهزة):** الاحتفاظ بعدد أدنى من الحاويات
(`min_replicas`) يعمل باستمرار حتى في أوقات الحمل المنخفض، بدل الاعتماد
بالكامل على scale-from-zero، لأن بدء التشغيل من الصفر أبطأ بكثير.
2. **صور خفيفة (slim images):** استخدام صور أساس صغيرة (مثل `python:3.11-slim`)
لتقليل وقت سحب الصورة (image pull time) عند جدولة حاوية جديدة على node جديد.
3. **Predictive/Proactive scaling:** التوسع بناءً على اتجاه الحمل (trend)
وليس فقط عندما يتجاوز الحمل الحد الحالي — مثال: لو الحمل يرتفع بمعدل ثابت
خلال آخر دقيقتين، ابدأ بإضافة حاويات الآن بدل الانتظار حتى الوصول للحد الأقصى.
4. **Connection draining عند الإزالة:** عند تقليص العدد (scale-down)، إعطاء
الحاوية مهلة لإنهاء الطلبات الجارية قبل إيقافها فعلياً، لتفادي أخطاء للمستخدمين.
---
## 4) استخدام Ghaymah Block Storage لأحمال العمل ذات الحالة (Stateful)
التطبيق نفسه (API) عادة **stateless** — أي حاوية جديدة يمكن أن تخدم أي طلب
دون الحاجة لبيانات محفوظة محلياً. لكن بعض المكونات تحتاج تخزيناً دائماً:
- **ملفات مرفوعة من المستخدمين** (صور، مستندات) يجب أن تبقى متاحة حتى لو
تغيّرت الحاوية التي تخدم الطلب التالي.
- **قواعد بيانات أو أنظمة queue** تحتاج تخزيناً لا يُفقد عند إعادة تشغيل الحاوية.
- **ملفات cache دائمة أو logs** يُراد الاحتفاظ بها عبر إعادة الجدولة.
**كيف يُستخدم Ghaymah Block Storage هنا:**
- يُربط (mount) كـ volume دائم لأي حاوية تحتاج تخزيناً (مثل خدمة قاعدة
البيانات أو خدمة رفع الملفات)، بحيث تبقى البيانات موجودة حتى لو حُذفت
الحاوية وأُعيد إنشاؤها من جديد على node مختلف.
- يُفصل عن الحاويات نفسها (decoupled)، فيمكن لأي نسخة جديدة من الخدمة أن
"ترث" نفس البيانات بمجرد إعادة ربط نفس الـ volume، بدل تخزين البيانات
داخل الحاوية (وهو ما يفقد عند إعادة التشغيل).
- بالنسبة للـ API نفسه (الطبقة الأمامية عالية التوسع من 39 حاوية)، يبقى
**stateless تماماً** ولا يحتاج Block Storage مباشرة — فقط الطبقات الخلفية
(قاعدة البيانات، تخزين الملفات) هي من تحتاجه، مما يسمح للطبقة الأمامية
بالتوسع والتقلص بحرية دون القلق على فقدان بيانات.

عرض الملف

@@ -0,0 +1,62 @@
# المهمة 5 — لوحة مراقبة mithal.space
## الملفات
- `collector.py` — يجمع المقاييس كل دقيقة (latency, uptime, SSL, DNS, search) ويخزنها في:
- `metrics.csv` (سجل تاريخي كامل، صف لكل فحص)
- `latest.json` (أحدث قراءة، يقرأها الـ dashboard)
- `history.json` (آخر 1440 قراءة = 24 ساعة، يقرأها الـ dashboard للرسم البياني والجدول)
- `dashboard.html` — يعرض uptime%، رسم بياني لزمن الاستجابة، حالة SSL، وسجل آخر 10 فحوصات
- `requirements.txt` — اعتماديات بايثون (`requests`)
## 1) التشغيل محلياً
```bash
cd task5-mithal-dashboard
pip install -r requirements.txt
# فحص واحد للتجربة
python collector.py --once
# تشغيل مستمر (يفحص كل دقيقة)
python collector.py &
# تقديم الملفات (dashboard.html + json files) عبر خادم بسيط
python -m http.server 8000
```
افتح المتصفح على: `http://localhost:8000/dashboard.html`
> ملاحظة: الـ dashboard يقرأ `history.json` و`latest.json` عبر `fetch()`، لذلك
> يجب تشغيله من خادم HTTP (مثل `http.server`) وليس بفتح الملف مباشرة (`file://`)
> لتفادي قيود CORS في المتصفح.
## 2) النشر على Ghaymah
نفس نمط النشر في المهمة 1: نضع `collector.py` و`dashboard.html` خلف سيرفر
بسيط (مثال: Flask/`http.server`) داخل حاوية واحدة، وننشرها كتطبيق Ghaymah:
```dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY collector.py dashboard.html ./
EXPOSE 8000
# يشغّل التجميع في الخلفية + خادم استضافة الملفات في الواجهة
CMD sh -c "python collector.py & python -m http.server 8000"
```
```bash
docker build -t mithal-monitor:v1 .
docker push registry.ghaymah.systems/<org>/mithal-monitor:v1
ghaymah deploy --app mithal-monitor \
--image registry.ghaymah.systems/<org>/mithal-monitor:v1 \
--port 8000
```
بعد النشر، الرابط الناتج (مثال: `https://mithal-monitor.ghaymah.systems/dashboard.html`)
يعرض اللوحة الحية، وتُحدَّث القراءات تلقائياً كل دقيقة عبر `collector.py`
الذي يعمل باستمرار داخل نفس الحاوية.
## 3) ملاحظة حول رابط البحث (Search Response)
عدّل المتغير `SEARCH_URL` داخل `collector.py` ليطابق مسار البحث الفعلي في
موقع mithal.space (حالياً موضوع كمثال `mithal.space/search?q=test`)، حسب
البنية الفعلية للموقع.

عرض الملف

@@ -0,0 +1,150 @@
"""
collector.py
يجمع مقاييس مراقبة لموقع mithal.space كل دقيقة ويخزنها في CSV و JSON:
- Latency: زمن استجابة HTTP
- Uptime: هل الموقع متاح (status code)
- SSL: حالة الشهادة وتاريخ انتهائها
- DNS: وقت تحليل DNS
- Search Response: زمن الرد على عملية بحث (query) داخل الموقع
الاستخدام:
python collector.py # يعمل باستمرار كل 60 ثانية
python collector.py --once # فحص واحد فقط (مفيد للاختبار / cron)
"""
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 requests
TARGET_URL = "https://mithal.space"
# عدّل هذا لمسار بحث فعلي في الموقع لو مختلف
SEARCH_URL = "https://mithal.space/search?q=test"
CSV_FILE = Path("metrics.csv")
JSON_FILE = Path("latest.json")
HISTORY_JSON = Path("history.json")
CHECK_INTERVAL = 60 # ثانية
HISTORY_MAX = 1440 # الاحتفاظ بآخر 24 ساعة (فحص كل دقيقة)
def check_dns(hostname: str) -> float:
"""يقيس وقت تحليل DNS بالمللي ثانية."""
start = time.perf_counter()
try:
socket.gethostbyname(hostname)
return round((time.perf_counter() - start) * 1000, 2)
except socket.gaierror:
return -1.0
def check_http(url: str) -> dict:
"""يقيس زمن الاستجابة وكود الحالة."""
try:
start = time.perf_counter()
resp = requests.get(url, timeout=10)
elapsed_ms = round((time.perf_counter() - start) * 1000, 2)
return {"status_code": resp.status_code, "latency_ms": elapsed_ms, "up": resp.status_code < 400}
except requests.RequestException as e:
return {"status_code": None, "latency_ms": -1, "up": False, "error": str(e)}
def check_ssl(hostname: str, port: int = 443) -> dict:
"""يفحص حالة شهادة SSL وعدد الأيام المتبقية على انتهائها."""
try:
ctx = ssl.create_default_context()
with socket.create_connection((hostname, port), timeout=10) 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_left = (expiry - datetime.now(timezone.utc)).days
return {"valid": True, "expires_on": expiry.isoformat(), "days_left": days_left}
except Exception as e:
return {"valid": False, "error": str(e)}
def check_search(url: str) -> dict:
"""يرسل طلب بحث ويقيس زمن الرد."""
try:
start = time.perf_counter()
resp = requests.get(url, timeout=10)
elapsed_ms = round((time.perf_counter() - start) * 1000, 2)
return {"status_code": resp.status_code, "latency_ms": elapsed_ms}
except requests.RequestException as e:
return {"status_code": None, "latency_ms": -1, "error": str(e)}
def run_check() -> dict:
hostname = urlparse(TARGET_URL).hostname
timestamp = datetime.now(timezone.utc).isoformat()
http_result = check_http(TARGET_URL)
dns_ms = check_dns(hostname)
ssl_result = check_ssl(hostname)
search_result = check_search(SEARCH_URL)
return {
"timestamp": timestamp,
"up": http_result.get("up", False),
"status_code": http_result.get("status_code"),
"latency_ms": http_result.get("latency_ms"),
"dns_ms": dns_ms,
"ssl_valid": ssl_result.get("valid"),
"ssl_days_left": ssl_result.get("days_left"),
"search_latency_ms": search_result.get("latency_ms"),
}
def append_csv(row: dict):
is_new = not CSV_FILE.exists()
with open(CSV_FILE, "a", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=list(row.keys()))
if is_new:
writer.writeheader()
writer.writerow(row)
def update_json(row: dict):
# أحدث قراءة (يقرأها الـ dashboard مباشرة)
JSON_FILE.write_text(json.dumps(row, ensure_ascii=False, indent=2), encoding="utf-8")
# سجل تاريخي محدود (لآخر 24 ساعة) يستخدمه الـ dashboard للرسم البياني والجدول
history = []
if HISTORY_JSON.exists():
try:
history = json.loads(HISTORY_JSON.read_text(encoding="utf-8"))
except json.JSONDecodeError:
history = []
history.append(row)
history = history[-HISTORY_MAX:]
HISTORY_JSON.write_text(json.dumps(history, ensure_ascii=False, indent=2), encoding="utf-8")
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--once", action="store_true", help="تشغيل فحص واحد فقط ثم الخروج")
args = parser.parse_args()
while True:
row = run_check()
append_csv(row)
update_json(row)
print(f"[{row['timestamp']}] up={row['up']} latency={row['latency_ms']}ms "
f"dns={row['dns_ms']}ms ssl_days_left={row['ssl_days_left']} "
f"search={row['search_latency_ms']}ms")
if args.once:
break
time.sleep(CHECK_INTERVAL)
if __name__ == "__main__":
main()

عرض الملف

@@ -0,0 +1,171 @@
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<title>لوحة مراقبة mithal.space</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.4/chart.umd.min.js"></script>
<style>
:root {
--up: #16a34a; --down: #dc2626; --warn: #f59e0b;
--bg: #0f172a; --card: #1e293b; --text: #e2e8f0; --muted: #94a3b8;
}
* { box-sizing: border-box; }
body { background: var(--bg); color: var(--text); font-family: "Segoe UI", Tahoma, sans-serif; margin:0; padding:24px; }
h1 { margin-bottom:4px; }
.sub { color: var(--muted); margin-bottom:24px; }
.grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(220px,1fr)); gap:16px; margin-bottom:20px; }
.card { background: var(--card); border-radius:12px; padding:20px; border:1px solid #334155; }
.card h3 { margin:0 0 8px; color: var(--muted); font-size:14px; font-weight:500; }
.card .value { font-size:26px; font-weight:700; }
.badge { display:inline-block; padding:4px 12px; border-radius:999px; font-size:14px; font-weight:700; }
.badge.up { background:rgba(22,163,74,.15); color:var(--up); }
.badge.down { background:rgba(220,38,38,.15); color:var(--down); }
.badge.warn { background:rgba(245,158,11,.15); color:var(--warn); }
.chart-card { background:var(--card); border-radius:12px; padding:20px; border:1px solid #334155; margin-bottom:20px; }
table { width:100%; border-collapse: collapse; font-size:13px; }
th, td { padding:8px 10px; text-align:right; border-bottom:1px solid #334155; }
th { color: var(--muted); font-weight:600; }
.log-card { background:var(--card); border-radius:12px; padding:20px; border:1px solid #334155; }
</style>
</head>
<body>
<h1>📡 لوحة مراقبة mithal.space</h1>
<div class="sub">تحديث تلقائي كل دقيقة (يقرأ من history.json / latest.json الناتجة عن collector.py)</div>
<div class="grid">
<div class="card">
<h3>Uptime (آخر 24 ساعة)</h3>
<div class="value" id="uptimePct">--%</div>
</div>
<div class="card">
<h3>الحالة الحالية</h3>
<div class="value"><span id="statusBadge" class="badge down">--</span></div>
</div>
<div class="card">
<h3>شهادة SSL</h3>
<div class="value" id="sslInfo">--</div>
</div>
<div class="card">
<h3>زمن تحليل DNS</h3>
<div class="value" id="dnsMs">-- ms</div>
</div>
</div>
<div class="chart-card">
<h3 style="color:var(--muted); margin-top:0;">زمن الاستجابة — آخر ساعة</h3>
<canvas id="latencyChart" height="90"></canvas>
</div>
<div class="log-card">
<h3 style="color:var(--muted); margin-top:0;">آخر 10 فحوصات</h3>
<table>
<thead>
<tr><th>الوقت</th><th>الحالة</th><th>HTTP</th><th>زمن الاستجابة</th><th>DNS</th><th>البحث</th></tr>
</thead>
<tbody id="logBody"></tbody>
</table>
</div>
<script>
let chart = null;
async function loadData() {
try {
const [historyRes, latestRes] = await Promise.all([
fetch('history.json', { cache: 'no-store' }),
fetch('latest.json', { cache: 'no-store' })
]);
const history = await historyRes.json();
const latest = await latestRes.json();
renderSummary(history, latest);
renderChart(history);
renderLog(history);
} catch (err) {
document.getElementById('statusBadge').textContent = 'لا يوجد اتصال بالبيانات';
console.error(err);
}
}
function renderSummary(history, latest) {
// Uptime % لآخر 24 ساعة
const last24h = history.slice(-1440);
const upCount = last24h.filter(r => r.up).length;
const pct = last24h.length ? ((upCount / last24h.length) * 100).toFixed(2) : '0.00';
document.getElementById('uptimePct').textContent = pct + '%';
// الحالة الحالية
const badge = document.getElementById('statusBadge');
badge.textContent = latest.up ? 'يعمل ✅' : 'متعطل ❌';
badge.className = 'badge ' + (latest.up ? 'up' : 'down');
// SSL
const sslEl = document.getElementById('sslInfo');
if (latest.ssl_valid) {
const days = latest.ssl_days_left;
sslEl.textContent = `صالحة (${days} يوم متبقي)`;
sslEl.parentElement.querySelector('.value').style.color = days < 14 ? 'var(--warn)' : 'var(--text)';
} else {
sslEl.textContent = 'غير صالحة ⚠️';
}
// DNS
document.getElementById('dnsMs').textContent = (latest.dns_ms ?? '--') + ' ms';
}
function renderChart(history) {
const lastHour = history.slice(-60); // فحص كل دقيقة → آخر 60 نقطة = ساعة
const labels = lastHour.map(r => new Date(r.timestamp).toLocaleTimeString('ar'));
const data = lastHour.map(r => r.latency_ms >= 0 ? r.latency_ms : null);
if (chart) { chart.destroy(); }
chart = new Chart(document.getElementById('latencyChart'), {
type: 'line',
data: {
labels,
datasets: [{
label: 'زمن الاستجابة (ms)',
data,
borderColor: '#2563eb',
backgroundColor: 'rgba(37,99,235,.15)',
tension: 0.3,
fill: true,
pointRadius: 2
}]
},
options: {
responsive: true,
scales: {
x: { ticks: { color: '#94a3b8' }, grid: { color: '#334155' } },
y: { ticks: { color: '#94a3b8' }, grid: { color: '#334155' } }
},
plugins: { legend: { labels: { color: '#e2e8f0' } } }
}
});
}
function renderLog(history) {
const last10 = history.slice(-10).reverse();
const body = document.getElementById('logBody');
body.innerHTML = '';
for (const r of last10) {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${new Date(r.timestamp).toLocaleString('ar')}</td>
<td>${r.up ? '✅ يعمل' : '❌ متعطل'}</td>
<td>${r.status_code ?? '--'}</td>
<td>${r.latency_ms >= 0 ? r.latency_ms + ' ms' : '--'}</td>
<td>${r.dns_ms >= 0 ? r.dns_ms + ' ms' : '--'}</td>
<td>${r.search_latency_ms >= 0 ? r.search_latency_ms + ' ms' : '--'}</td>
`;
body.appendChild(tr);
}
}
loadData();
setInterval(loadData, 60000);
</script>
</body>
</html>

عرض الملف

@@ -0,0 +1 @@
requests==2.32.3