Initial Ghaymah tasks setup
هذا الالتزام موجود في:
195
.github/workflows/deploy.yml
مباع
Normal file
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
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.
|
||||||
17
task1-deploy/.ghaymah.json
Normal file
17
task1-deploy/.ghaymah.json
Normal file
@@ -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
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
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
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>
|
||||||
6
task1-deploy/metrics.json
Normal file
6
task1-deploy/metrics.json
Normal file
@@ -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
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
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
|
||||||
3
task1-deploy/requirements.txt
Normal file
3
task1-deploy/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
flask==3.0.3
|
||||||
|
gunicorn==22.0.0
|
||||||
|
flask-cors
|
||||||
121
task2-postmortem/postmortem.md
Normal file
121
task2-postmortem/postmortem.md
Normal file
@@ -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.
|
||||||
|
|
||||||
|
|
||||||
113
task4-scalability/scalability.md
Normal file
113
task4-scalability/scalability.md
Normal file
@@ -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 مباشرة — فقط الطبقات الخلفية
|
||||||
|
(قاعدة البيانات، تخزين الملفات) هي من تحتاجه، مما يسمح للطبقة الأمامية
|
||||||
|
بالتوسع والتقلص بحرية دون القلق على فقدان بيانات.
|
||||||
62
task5-mithal-dashboard/README.md
Normal file
62
task5-mithal-dashboard/README.md
Normal file
@@ -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`)، حسب
|
||||||
|
البنية الفعلية للموقع.
|
||||||
150
task5-mithal-dashboard/collector.py
Normal file
150
task5-mithal-dashboard/collector.py
Normal file
@@ -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()
|
||||||
171
task5-mithal-dashboard/dashboard.html
Normal file
171
task5-mithal-dashboard/dashboard.html
Normal file
@@ -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>
|
||||||
1
task5-mithal-dashboard/requirements.txt
Normal file
1
task5-mithal-dashboard/requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
requests==2.32.3
|
||||||
المرجع في مشكلة جديدة
حظر مستخدم