Initial Ghaymah tasks setup

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

عرض الملف

@@ -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