commit 43e034fe64ac46a18c655d2b06995d52ed1231bd Author: momenlotfy Date: Tue Jul 28 20:41:37 2026 +0300 Initial Ghaymah tasks setup diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..9f3189a --- /dev/null +++ b/.github/workflows/deploy.yml @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..7f5ed84 --- /dev/null +++ b/README.md @@ -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. diff --git a/task1-deploy/.ghaymah.json b/task1-deploy/.ghaymah.json new file mode 100644 index 0000000..449219f --- /dev/null +++ b/task1-deploy/.ghaymah.json @@ -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" +} diff --git a/task1-deploy/Dockerfile b/task1-deploy/Dockerfile new file mode 100644 index 0000000..9e12035 --- /dev/null +++ b/task1-deploy/Dockerfile @@ -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"] diff --git a/task1-deploy/app.py b/task1-deploy/app.py new file mode 100644 index 0000000..96de9af --- /dev/null +++ b/task1-deploy/app.py @@ -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) diff --git a/task1-deploy/dashboard.html b/task1-deploy/dashboard.html new file mode 100644 index 0000000..fd6ddbd --- /dev/null +++ b/task1-deploy/dashboard.html @@ -0,0 +1,161 @@ + + + + +لوحة مراقبة التطبيق - Ghaymah + + + + +

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

+
يتم الفحص تلقائياً كل 5 ثوانٍ عبر /health و /metrics
+ +
+ + +
+ +
+
+

الحالة

+
غير معروف
+
+
+

زمن الاستجابة

+
-- ms
+
+
+

عدد الطلبات المخدومة

+
--
+
+
+

مدة تشغيل التطبيق

+
--
+
+
+ +
+
جاهز... اضغط "ابدأ المراقبة"
+
+ + + + + diff --git a/task1-deploy/metrics.json b/task1-deploy/metrics.json new file mode 100644 index 0000000..1d0c499 --- /dev/null +++ b/task1-deploy/metrics.json @@ -0,0 +1,6 @@ +{ + "timestamp": "2026-07-28 19:13:13", + "status": "UP", + "http_code": "200", + "response_ms": 1765 +} diff --git a/task1-deploy/monitor.log b/task1-deploy/monitor.log new file mode 100644 index 0000000..a73e50b --- /dev/null +++ b/task1-deploy/monitor.log @@ -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 diff --git a/task1-deploy/monitor.sh b/task1-deploy/monitor.sh new file mode 100755 index 0000000..da1cf1a --- /dev/null +++ b/task1-deploy/monitor.sh @@ -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" < DNS[Ghaymah DNS / CDN Edge] + DNS --> LB[Load Balancer
غيمة] + + LB --> C1[حاوية 1
500 req/s] + LB --> C2[حاوية 2
500 req/s] + LB --> C3[حاوية 3
500 req/s] + LB --> C4[...] + LB --> C39[حاوية 39
500 req/s] + + C1 --> CACHE[(Redis Cache)] + C2 --> CACHE + C3 --> CACHE + C39 --> CACHE + + CACHE --> DB[(قاعدة بيانات رئيسية)] + C1 --> BLOCK[(Ghaymah Block Storage
للبيانات الدائمة)] + 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 مباشرة — فقط الطبقات الخلفية + (قاعدة البيانات، تخزين الملفات) هي من تحتاجه، مما يسمح للطبقة الأمامية + بالتوسع والتقلص بحرية دون القلق على فقدان بيانات. diff --git a/task5-mithal-dashboard/README.md b/task5-mithal-dashboard/README.md new file mode 100644 index 0000000..140ac17 --- /dev/null +++ b/task5-mithal-dashboard/README.md @@ -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//mithal-monitor:v1 +ghaymah deploy --app mithal-monitor \ + --image registry.ghaymah.systems//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`)، حسب +البنية الفعلية للموقع. diff --git a/task5-mithal-dashboard/collector.py b/task5-mithal-dashboard/collector.py new file mode 100644 index 0000000..12ef11a --- /dev/null +++ b/task5-mithal-dashboard/collector.py @@ -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() diff --git a/task5-mithal-dashboard/dashboard.html b/task5-mithal-dashboard/dashboard.html new file mode 100644 index 0000000..eafe204 --- /dev/null +++ b/task5-mithal-dashboard/dashboard.html @@ -0,0 +1,171 @@ + + + + +لوحة مراقبة mithal.space + + + + + +

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

+
تحديث تلقائي كل دقيقة (يقرأ من history.json / latest.json الناتجة عن collector.py)
+ +
+
+

Uptime (آخر 24 ساعة)

+
--%
+
+
+

الحالة الحالية

+
--
+
+
+

شهادة SSL

+
--
+
+
+

زمن تحليل DNS

+
-- ms
+
+
+ +
+

زمن الاستجابة — آخر ساعة

+ +
+ +
+

آخر 10 فحوصات

+ + + + + +
الوقتالحالةHTTPزمن الاستجابةDNSالبحث
+
+ + + + + diff --git a/task5-mithal-dashboard/requirements.txt b/task5-mithal-dashboard/requirements.txt new file mode 100644 index 0000000..d80d9fc --- /dev/null +++ b/task5-mithal-dashboard/requirements.txt @@ -0,0 +1 @@ +requests==2.32.3