Ghaymah SRE exam submission - Mohamed Adel
هذا الالتزام موجود في:
17
q5-mithal-monitor/Dockerfile
Normal file
17
q5-mithal-monitor/Dockerfile
Normal file
@@ -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"]
|
||||
164
q5-mithal-monitor/dashboard.html
Normal file
164
q5-mithal-monitor/dashboard.html
Normal file
@@ -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>
|
||||
10
q5-mithal-monitor/entrypoint.sh
Normal file
10
q5-mithal-monitor/entrypoint.sh
Normal file
@@ -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
|
||||
172
q5-mithal-monitor/monitor.py
Normal file
172
q5-mithal-monitor/monitor.py
Normal file
@@ -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()
|
||||
المرجع في مشكلة جديدة
حظر مستخدم