Initial Ghaymah tasks setup
هذا الالتزام موجود في:
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
|
||||
المرجع في مشكلة جديدة
حظر مستخدم