Ghaymah SRE exam submission - Mohamed Adel

هذا الالتزام موجود في:
amohamedadel29
2026-07-28 03:54:42 +03:00
التزام 9bf0dfa0e8
18 ملفات معدلة مع 1184 إضافات و0 حذوفات

عرض الملف

@@ -0,0 +1,17 @@
# ---- Base image ----
FROM python:3.12-slim
WORKDIR /app
# ---- Copy app files ----
COPY monitor.py .
COPY dashboard.html .
COPY entrypoint.sh .
RUN chmod +x entrypoint.sh
# ---- Expose the dashboard port ----
EXPOSE 8080
# ---- Run monitor.py in the background + serve dashboard.html on 8080 ----
CMD ["./entrypoint.sh"]

عرض الملف

@@ -0,0 +1,164 @@
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>لوحة مراقبة mithal.space</title>
<style>
:root{
--bg:#0b0f0d; --panel:#101512; --line:#1e2a24;
--green:#5fd88a; --red:#ff6b6b; --amber:#ffb454;
--text:#d7e0da; --dim:#7c8f85;
--mono:"IBM Plex Mono","SFMono-Regular",Consolas,monospace;
}
*{box-sizing:border-box;}
body{margin:0;background:radial-gradient(circle at 20% -10%, #142019 0%, transparent 60%), var(--bg);
color:var(--text);font-family:var(--mono);padding:28px 20px 60px;direction:rtl;}
.wrap{max-width:1040px;margin:0 auto;}
header{border-bottom:1px solid var(--line);padding-bottom:16px;margin-bottom:22px;}
h1{font-size:20px;margin:0;font-weight:600;}
h1 span{color:var(--green);}
.sub{color:var(--dim);font-size:12px;margin-top:4px;}
.grid{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:20px;}
@media(max-width:820px){.grid{grid-template-columns:repeat(2,1fr);}}
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:16px;}
.card .label{color:var(--dim);font-size:11px;text-transform:uppercase;letter-spacing:1px;}
.card .value{font-size:24px;font-weight:700;margin-top:8px;}
.ok{color:var(--green);} .bad{color:var(--red);} .warn{color:var(--amber);}
.panel{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:18px;margin-bottom:16px;}
.panel h2{font-size:13px;color:var(--dim);text-transform:uppercase;letter-spacing:1px;margin:0 0 14px;}
#chart{width:100%;height:160px;display:block;}
table{width:100%;border-collapse:collapse;font-size:12px;}
th,td{text-align:right;padding:8px 6px;border-bottom:1px solid var(--line);}
th{color:var(--dim);font-weight:500;}
.pill{padding:2px 8px;border-radius:20px;font-size:11px;font-weight:600;}
.pill.up{background:rgba(95,216,138,.12);color:var(--green);}
.pill.down{background:rgba(255,107,107,.12);color:var(--red);}
.note{color:var(--dim);font-size:11px;margin-top:6px;}
footer{color:var(--dim);font-size:11px;text-align:center;margin-top:30px;}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>لوحة مراقبة <span>mithal.space</span></h1>
<div class="sub">Ghaymah SRE Exam — Q5 · يقرأ بيانات من metrics.json (ينتجها monitor.py كل دقيقة)</div>
</header>
<div class="grid">
<div class="card">
<div class="label">Uptime (24 ساعة)</div>
<div class="value" id="uptimeValue"></div>
</div>
<div class="card">
<div class="label">آخر زمن استجابة</div>
<div class="value" id="latencyValue"></div>
</div>
<div class="card">
<div class="label">شهادة SSL</div>
<div class="value" id="sslValue"></div>
</div>
<div class="card">
<div class="label">زمن DNS</div>
<div class="value" id="dnsValue"></div>
</div>
</div>
<div class="panel">
<h2>زمن الاستجابة — آخر ساعة</h2>
<svg id="chart" viewBox="0 0 1000 160" preserveAspectRatio="none"></svg>
<div class="note">كل نقطة = فحص واحد (كل دقيقة تقريبًا)</div>
</div>
<div class="panel">
<h2>سجل آخر 10 فحوصات</h2>
<table>
<thead><tr><th>الوقت</th><th>الحالة</th><th>Status Code</th><th>زمن الاستجابة</th><th>DNS</th><th>Search</th></tr></thead>
<tbody id="logBody"><tr><td colspan="6">جاري تحميل البيانات من metrics.json...</td></tr></tbody>
</table>
</div>
<footer>مصدر البيانات: metrics.json الناتج عن monitor.py (نفس المجلد) · حدّث الصفحة لتحديث البيانات، أو استضف monitor.py على Ghaymah كمهمة مجدولة (cron/scheduled task)</footer>
</div>
<script>
async function loadData(){
let data = [];
try{
const res = await fetch('./metrics.json', {cache:'no-store'});
data = await res.json();
}catch(e){
document.getElementById('logBody').innerHTML =
'<tr><td colspan="6">تعذر تحميل metrics.json — تأكد أن monitor.py يعمل وأنه بجانب هذا الملف على نفس الاستضافة.</td></tr>';
return;
}
if(!data.length) return;
// Uptime % over last 24h (or all available data if less)
const last24h = data.slice(-1440);
const upCount = last24h.filter(d => d.up).length;
const uptimePct = ((upCount / last24h.length) * 100).toFixed(2);
const uptimeEl = document.getElementById('uptimeValue');
uptimeEl.textContent = uptimePct + '%';
uptimeEl.className = 'value ' + (uptimePct >= 99.9 ? 'ok' : uptimePct >= 99 ? 'warn' : 'bad');
const latest = data[data.length - 1];
const latencyEl = document.getElementById('latencyValue');
latencyEl.textContent = (latest.latency_ms ?? '—') + ' ms';
latencyEl.className = 'value ' + (latest.up ? 'ok' : 'bad');
const sslEl = document.getElementById('sslValue');
if(latest.ssl_valid){
sslEl.textContent = latest.ssl_days_remaining + ' يوم متبقي';
sslEl.className = 'value ' + (latest.ssl_days_remaining < 14 ? 'warn' : 'ok');
} else {
sslEl.textContent = 'غير صالحة';
sslEl.className = 'value bad';
}
document.getElementById('dnsValue').textContent = (latest.dns_ms ?? '—') + ' ms';
drawChart(data.slice(-60));
drawLog(data.slice(-10).reverse());
}
function drawChart(points){
const svg = document.getElementById('chart');
if(points.length < 2){ svg.innerHTML=''; return; }
const w = 1000, h = 160, pad = 10;
const vals = points.map(p => p.latency_ms || 0);
const maxMs = Math.max(...vals, 50);
const step = (w - pad*2) / (points.length - 1);
const line = points.map((p,i) => {
const x = pad + i*step;
const y = h - pad - (((p.latency_ms||0) / maxMs) * (h - pad*2));
return `${x},${y}`;
}).join(' ');
svg.innerHTML = `
<polyline fill="none" stroke="#5fd88a" stroke-width="2" points="${line}" />
${points.map((p,i)=>{
const x = pad + i*step;
const y = h - pad - (((p.latency_ms||0) / maxMs) * (h - pad*2));
return `<circle cx="${x}" cy="${y}" r="2.5" fill="${p.up?'#5fd88a':'#ff6b6b'}" />`;
}).join('')}
`;
}
function drawLog(rows){
document.getElementById('logBody').innerHTML = rows.map(r => `
<tr>
<td>${new Date(r.timestamp).toLocaleTimeString('ar-EG')}</td>
<td><span class="pill ${r.up?'up':'down'}">${r.up?'UP':'DOWN'}</span></td>
<td>${r.status_code ?? '—'}</td>
<td>${r.latency_ms ?? '—'} ms</td>
<td>${r.dns_ms ?? '—'} ms</td>
<td>${r.search_response_ms ?? '—'} ms</td>
</tr>
`).join('');
}
loadData();
setInterval(loadData, 60000);
</script>
</body>
</html>

عرض الملف

@@ -0,0 +1,10 @@
#!/usr/bin/env bash
# Starts monitor.py in the background (checks mithal.space every 60s,
# writes metrics.csv + metrics.json into /app), then serves the current
# directory (including dashboard.html and metrics.json) over HTTP on 8080
# so the dashboard can fetch('./metrics.json') directly.
set -e
python3 monitor.py --url "${MONITOR_TARGET_URL:-https://mithal.space}" --interval 60 &
exec python3 -m http.server 8080

عرض الملف

@@ -0,0 +1,172 @@
"""
monitor.py — Monitoring script for mithal.space (Q5)
Collects every 60 seconds:
- Latency: HTTP request response time
- Uptime: is the site reachable? (status code)
- SSL: certificate validity + days until expiry
- DNS: DNS resolution time
- Search Response: time to send a query and get a response
Stores results in both CSV and JSON so dashboard.html can read either.
Usage:
python3 monitor.py --url https://mithal.space --interval 60
python3 monitor.py --once # single run, useful for cron/testing
"""
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 urllib.request
import urllib.error
CSV_FILE = Path(__file__).parent / "metrics.csv"
JSON_FILE = Path(__file__).parent / "metrics.json"
CSV_FIELDS = [
"timestamp", "up", "status_code", "latency_ms",
"dns_ms", "ssl_valid", "ssl_days_remaining",
"search_ok", "search_response_ms", "error",
]
def check_dns(hostname: str):
"""Returns DNS resolution time in ms, or None on failure."""
start = time.perf_counter()
try:
socket.gethostbyname(hostname)
return round((time.perf_counter() - start) * 1000, 2)
except socket.gaierror:
return None
def check_ssl(hostname: str, port: int = 443):
"""Returns (is_valid, days_remaining) for the site's SSL certificate."""
try:
ctx = ssl.create_default_context()
with socket.create_connection((hostname, port), timeout=5) 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_remaining = (expiry - datetime.now(timezone.utc)).days
return True, days_remaining
except Exception:
return False, None
def check_http(url: str, timeout: int = 10):
"""Returns (up, status_code, latency_ms, error)."""
start = time.perf_counter()
try:
req = urllib.request.Request(url, headers={"User-Agent": "Ghaymah-SRE-Monitor/1.0"})
with urllib.request.urlopen(req, timeout=timeout) as resp:
latency_ms = round((time.perf_counter() - start) * 1000, 2)
return True, resp.status, latency_ms, None
except urllib.error.HTTPError as e:
latency_ms = round((time.perf_counter() - start) * 1000, 2)
# site responded, just with an error status (e.g. 404/500)
return e.code < 500, e.code, latency_ms, str(e)
except Exception as e:
latency_ms = round((time.perf_counter() - start) * 1000, 2)
return False, None, latency_ms, str(e)
def check_search(base_url: str, query: str = "test", timeout: int = 10):
"""
Simulates sending a search query to the site and measuring response time.
Adjust `search_path` below to match mithal.space's actual search endpoint
once known; falls back to hitting the homepage with a query string.
"""
search_path = "/search"
url = f"{base_url.rstrip('/')}{search_path}?q={query}"
start = time.perf_counter()
try:
req = urllib.request.Request(url, headers={"User-Agent": "Ghaymah-SRE-Monitor/1.0"})
with urllib.request.urlopen(req, timeout=timeout) as resp:
elapsed_ms = round((time.perf_counter() - start) * 1000, 2)
return resp.status < 400, elapsed_ms
except Exception:
elapsed_ms = round((time.perf_counter() - start) * 1000, 2)
return False, elapsed_ms
def run_check(url: str):
parsed = urlparse(url)
hostname = parsed.hostname
dns_ms = check_dns(hostname)
up, status_code, latency_ms, error = check_http(url)
ssl_valid, ssl_days = check_ssl(hostname)
search_ok, search_ms = check_search(url)
return {
"timestamp": datetime.now(timezone.utc).isoformat(),
"up": up,
"status_code": status_code,
"latency_ms": latency_ms,
"dns_ms": dns_ms,
"ssl_valid": ssl_valid,
"ssl_days_remaining": ssl_days,
"search_ok": search_ok,
"search_response_ms": search_ms,
"error": error,
}
def append_csv(row: dict):
is_new = not CSV_FILE.exists()
with open(CSV_FILE, "a", newline="") as f:
writer = csv.DictWriter(f, fieldnames=CSV_FIELDS)
if is_new:
writer.writeheader()
writer.writerow(row)
def append_json(row: dict, keep_last: int = 1440):
"""Keeps a rolling window of the last N checks (1440 = 24h at 1/min)."""
data = []
if JSON_FILE.exists():
try:
data = json.loads(JSON_FILE.read_text())
except json.JSONDecodeError:
data = []
data.append(row)
data = data[-keep_last:]
JSON_FILE.write_text(json.dumps(data, indent=2))
def main():
parser = argparse.ArgumentParser(description="Monitor mithal.space")
parser.add_argument("--url", default="https://mithal.space")
parser.add_argument("--interval", type=int, default=60, help="seconds between checks")
parser.add_argument("--once", action="store_true", help="run a single check and exit")
args = parser.parse_args()
while True:
result = run_check(args.url)
append_csv(result)
append_json(result)
status = "UP" if result["up"] else "DOWN"
print(
f"[{result['timestamp']}] {status} | "
f"latency={result['latency_ms']}ms | dns={result['dns_ms']}ms | "
f"ssl_days_left={result['ssl_days_remaining']} | "
f"search={result['search_response_ms']}ms"
)
if args.once:
break
time.sleep(args.interval)
if __name__ == "__main__":
main()