Ghaymah SRE exam submission - Mohamed Adel
هذا الالتزام موجود في:
30
q1-deploy-monitor/Dockerfile
Normal file
30
q1-deploy-monitor/Dockerfile
Normal file
@@ -0,0 +1,30 @@
|
||||
# ---- Base image ----
|
||||
FROM python:3.12-slim
|
||||
|
||||
# ---- Metadata ----
|
||||
LABEL maintainer="Mohamed Adel"
|
||||
LABEL description="Simple Flask API with /health endpoint, deployed on Ghaymah Containers"
|
||||
|
||||
# ---- Working directory ----
|
||||
WORKDIR /app
|
||||
|
||||
# ---- Install dependencies first (better layer caching) ----
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# ---- Copy application code ----
|
||||
COPY app.py .
|
||||
|
||||
# ---- Create non-root user for security ----
|
||||
RUN useradd -m appuser
|
||||
USER appuser
|
||||
|
||||
# ---- Expose the app port ----
|
||||
EXPOSE 8080
|
||||
|
||||
# ---- Container-level healthcheck (used by Ghaymah / Docker runtime) ----
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')" || exit 1
|
||||
|
||||
# ---- Run the app with a production WSGI server ----
|
||||
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "--workers", "2", "app:app"]
|
||||
65
q1-deploy-monitor/app.py
Normal file
65
q1-deploy-monitor/app.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
Simple demo API for the Ghaymah SRE exam (Q1).
|
||||
|
||||
Endpoints:
|
||||
GET / -> basic welcome message
|
||||
GET /health -> liveness/readiness probe used by Ghaymah + monitoring script
|
||||
GET /metrics -> lightweight JSON metrics used by the dashboard
|
||||
(uptime, request count, avg response time)
|
||||
"""
|
||||
|
||||
import time
|
||||
from flask import Flask, jsonify
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
START_TIME = time.time()
|
||||
REQUEST_COUNT = 0
|
||||
TOTAL_RESPONSE_TIME = 0.0
|
||||
|
||||
|
||||
@app.before_request
|
||||
def _start_timer():
|
||||
global REQUEST_COUNT
|
||||
REQUEST_COUNT += 1
|
||||
app.config["_req_start"] = time.time()
|
||||
|
||||
|
||||
@app.after_request
|
||||
def _record_timing(response):
|
||||
global TOTAL_RESPONSE_TIME
|
||||
elapsed = time.time() - app.config.get("_req_start", time.time())
|
||||
TOTAL_RESPONSE_TIME += elapsed
|
||||
response.headers["X-Response-Time-ms"] = f"{elapsed * 1000:.2f}"
|
||||
return response
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return jsonify(message="Ghaymah SRE exam demo API is running", status="ok")
|
||||
|
||||
|
||||
@app.route("/health")
|
||||
def health():
|
||||
"""Used by: Ghaymah container platform health checks, and the
|
||||
external monitoring script (health-check.sh)."""
|
||||
return jsonify(status="healthy", uptime_seconds=round(time.time() - START_TIME, 2)), 200
|
||||
|
||||
|
||||
@app.route("/metrics")
|
||||
def metrics():
|
||||
"""Used by the dashboard.html to render status / avg response time / request count."""
|
||||
avg_response_ms = (
|
||||
(TOTAL_RESPONSE_TIME / REQUEST_COUNT) * 1000 if REQUEST_COUNT else 0
|
||||
)
|
||||
return jsonify(
|
||||
status="healthy",
|
||||
uptime_seconds=round(time.time() - START_TIME, 2),
|
||||
request_count=REQUEST_COUNT,
|
||||
avg_response_time_ms=round(avg_response_ms, 2),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# For local testing only; production uses gunicorn (see Dockerfile CMD)
|
||||
app.run(host="0.0.0.0", port=8080)
|
||||
212
q1-deploy-monitor/dashboard.html
Normal file
212
q1-deploy-monitor/dashboard.html
Normal file
@@ -0,0 +1,212 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ar" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>لوحة مراقبة التطبيق — Ghaymah SRE Exam</title>
|
||||
<style>
|
||||
:root{
|
||||
--bg:#0b0f0d;
|
||||
--panel:#101512;
|
||||
--line:#1e2a24;
|
||||
--amber:#ffb454;
|
||||
--green:#5fd88a;
|
||||
--red:#ff6b6b;
|
||||
--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:980px;margin:0 auto;}
|
||||
header{
|
||||
display:flex;justify-content:space-between;align-items:flex-end;
|
||||
border-bottom:1px solid var(--line);
|
||||
padding-bottom:16px;margin-bottom:22px;flex-wrap:wrap;gap:10px;
|
||||
}
|
||||
h1{font-size:20px;margin:0;font-weight:600;letter-spacing:.5px;}
|
||||
h1 span{color:var(--green);}
|
||||
.sub{color:var(--dim);font-size:12px;margin-top:4px;}
|
||||
.controls{display:flex;gap:8px;align-items:center;}
|
||||
input[type=text]{
|
||||
background:var(--panel);border:1px solid var(--line);color:var(--text);
|
||||
padding:8px 10px;border-radius:6px;font-family:var(--mono);font-size:12px;width:260px;
|
||||
}
|
||||
button{
|
||||
background:var(--green);color:#04150a;border:none;border-radius:6px;
|
||||
padding:8px 14px;font-family:var(--mono);font-weight:600;font-size:12px;cursor:pointer;
|
||||
}
|
||||
button:hover{filter:brightness(1.08);}
|
||||
.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-bottom:20px;}
|
||||
@media(max-width:760px){.grid{grid-template-columns:1fr;}}
|
||||
.card{
|
||||
background:var(--panel);border:1px solid var(--line);border-radius:10px;
|
||||
padding:18px;position:relative;overflow:hidden;
|
||||
}
|
||||
.card .label{color:var(--dim);font-size:11px;text-transform:uppercase;letter-spacing:1px;}
|
||||
.card .value{font-size:30px;font-weight:700;margin-top:8px;}
|
||||
.status-up{color:var(--green);}
|
||||
.status-down{color:var(--red);}
|
||||
.dot{display:inline-block;width:9px;height:9px;border-radius:50%;margin-left:8px;}
|
||||
.dot.up{background:var(--green);box-shadow:0 0 8px var(--green);}
|
||||
.dot.down{background:var(--red);box-shadow:0 0 8px var(--red);}
|
||||
.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);}
|
||||
footer{color:var(--dim);font-size:11px;text-align:center;margin-top:30px;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
|
||||
<header>
|
||||
<div>
|
||||
<h1>لوحة <span>مراقبة</span> التطبيق</h1>
|
||||
<div class="sub">Ghaymah SRE Exam — Q1 · يقرأ من /metrics و /health كل 5 ثوانٍ</div>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<input type="text" id="apiUrl" placeholder="https://your-app.ghaymah.systems" value="https://ghaymah-exam-app-06b532d0a81b.hosted.ghaymah.systems">
|
||||
<button onclick="setUrl()">اتصال</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<div class="label">الحالة</div>
|
||||
<div class="value" id="statusValue"><span class="dot up"></span><span id="statusText">جاري التحميل...</span></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">زمن الاستجابة</div>
|
||||
<div class="value" id="latencyValue">—</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">عدد الطلبات</div>
|
||||
<div class="value" id="requestsValue">—</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>زمن الاستجابة — آخر 20 فحص</h2>
|
||||
<svg id="chart" viewBox="0 0 900 160" preserveAspectRatio="none"></svg>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>سجل الفحوصات</h2>
|
||||
<table>
|
||||
<thead><tr><th>الوقت</th><th>الحالة</th><th>زمن الاستجابة</th></tr></thead>
|
||||
<tbody id="logBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<footer>يعمل الفحص كل 5 ثوانٍ في المتصفح · مصدر البيانات: نفس تطبيق Q1 (endpoint: /metrics)</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let API_URL = "";
|
||||
let history = []; // {t, ok, ms}
|
||||
const MAX_POINTS = 20;
|
||||
|
||||
function setUrl(){
|
||||
API_URL = document.getElementById('apiUrl').value.trim().replace(/\/$/, '');
|
||||
history = [];
|
||||
poll();
|
||||
}
|
||||
|
||||
function fmtTime(d){
|
||||
return d.toLocaleTimeString('ar-EG', {hour:'2-digit', minute:'2-digit', second:'2-digit'});
|
||||
}
|
||||
|
||||
async function poll(){
|
||||
const statusText = document.getElementById('statusText');
|
||||
const statusValue = document.getElementById('statusValue');
|
||||
const latencyValue = document.getElementById('latencyValue');
|
||||
const requestsValue = document.getElementById('requestsValue');
|
||||
|
||||
if(!API_URL){
|
||||
statusText.textContent = "أدخل رابط التطبيق فوق";
|
||||
return;
|
||||
}
|
||||
|
||||
const t0 = performance.now();
|
||||
let ok = false, ms = 0, reqCount = '—';
|
||||
try{
|
||||
const res = await fetch(API_URL + '/metrics', {cache:'no-store'});
|
||||
ms = Math.round(performance.now() - t0);
|
||||
if(res.ok){
|
||||
const data = await res.json();
|
||||
ok = true;
|
||||
reqCount = data.request_count ?? '—';
|
||||
}
|
||||
}catch(e){
|
||||
ms = Math.round(performance.now() - t0);
|
||||
ok = false;
|
||||
}
|
||||
|
||||
statusValue.innerHTML = `<span class="dot ${ok?'up':'down'}"></span><span class="${ok?'status-up':'status-down'}">${ok?'يعمل':'متوقف'}</span>`;
|
||||
latencyValue.textContent = ms + ' ms';
|
||||
requestsValue.textContent = reqCount;
|
||||
|
||||
const now = new Date();
|
||||
history.push({t: now, ok, ms});
|
||||
if(history.length > MAX_POINTS) history.shift();
|
||||
|
||||
drawChart();
|
||||
drawLog();
|
||||
|
||||
setTimeout(poll, 5000);
|
||||
}
|
||||
|
||||
function drawChart(){
|
||||
const svg = document.getElementById('chart');
|
||||
if(history.length < 2){ svg.innerHTML=''; return; }
|
||||
const w = 900, h = 160, pad = 10;
|
||||
const maxMs = Math.max(...history.map(p=>p.ms), 50);
|
||||
const step = (w - pad*2) / (MAX_POINTS - 1);
|
||||
let points = history.map((p,i)=>{
|
||||
const x = pad + i*step;
|
||||
const y = h - pad - ((p.ms / maxMs) * (h - pad*2));
|
||||
return `${x},${y}`;
|
||||
}).join(' ');
|
||||
svg.innerHTML = `
|
||||
<polyline fill="none" stroke="#5fd88a" stroke-width="2" points="${points}" />
|
||||
${history.map((p,i)=>{
|
||||
const x = pad + i*step;
|
||||
const y = h - pad - ((p.ms / maxMs) * (h - pad*2));
|
||||
return `<circle cx="${x}" cy="${y}" r="3" fill="${p.ok?'#5fd88a':'#ff6b6b'}" />`;
|
||||
}).join('')}
|
||||
`;
|
||||
}
|
||||
|
||||
function drawLog(){
|
||||
const body = document.getElementById('logBody');
|
||||
body.innerHTML = history.slice().reverse().slice(0,10).map(p=>`
|
||||
<tr>
|
||||
<td>${fmtTime(p.t)}</td>
|
||||
<td><span class="pill ${p.ok?'up':'down'}">${p.ok?'UP':'DOWN'}</span></td>
|
||||
<td>${p.ms} ms</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Auto-connect on load using the pre-filled deployed URL
|
||||
window.addEventListener('DOMContentLoaded', setUrl);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
66
q1-deploy-monitor/health-check.sh
Normal file
66
q1-deploy-monitor/health-check.sh
Normal file
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# health-check.sh
|
||||
# Polls the app's /health endpoint every 30 seconds, logs status + response
|
||||
# time to a CSV file that the dashboard (dashboard.html) can read, and
|
||||
# alerts (stderr + log) if the app is unhealthy for 3 consecutive checks.
|
||||
#
|
||||
# Usage:
|
||||
# ./health-check.sh https://your-app.ghaymah.systems
|
||||
#
|
||||
# Run in the background on the host, or as a sidecar container, or as a
|
||||
# systemd service (recommended for production on Ghaymah).
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
APP_URL="${1:-http://localhost:8080}"
|
||||
HEALTH_ENDPOINT="${APP_URL%/}/health"
|
||||
CHECK_INTERVAL=30 # seconds
|
||||
LOG_FILE="./metrics.csv"
|
||||
FAIL_THRESHOLD=3
|
||||
consecutive_failures=0
|
||||
|
||||
# Create CSV header if the file doesn't exist yet
|
||||
if [[ ! -f "$LOG_FILE" ]]; then
|
||||
echo "timestamp,status,http_code,response_time_ms" > "$LOG_FILE"
|
||||
fi
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
|
||||
}
|
||||
|
||||
log "Starting health monitor for: $HEALTH_ENDPOINT (interval: ${CHECK_INTERVAL}s)"
|
||||
|
||||
while true; do
|
||||
start_ms=$(date +%s%3N)
|
||||
|
||||
# -o /dev/null discards body, -w prints http_code, -s silent, -m 5 timeout 5s
|
||||
http_code=$(curl -s -o /dev/null -w "%{http_code}" -m 5 "$HEALTH_ENDPOINT")
|
||||
curl_exit=$?
|
||||
|
||||
end_ms=$(date +%s%3N)
|
||||
response_time_ms=$((end_ms - start_ms))
|
||||
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
if [[ $curl_exit -eq 0 && "$http_code" == "200" ]]; then
|
||||
status="UP"
|
||||
consecutive_failures=0
|
||||
log "OK - ${HEALTH_ENDPOINT} - ${http_code} - ${response_time_ms}ms"
|
||||
else
|
||||
status="DOWN"
|
||||
consecutive_failures=$((consecutive_failures + 1))
|
||||
log "FAIL - ${HEALTH_ENDPOINT} - http_code=${http_code:-none} curl_exit=${curl_exit} - failure #${consecutive_failures}"
|
||||
|
||||
if [[ $consecutive_failures -ge $FAIL_THRESHOLD ]]; then
|
||||
log "ALERT: ${consecutive_failures} consecutive failures! Application appears DOWN." >&2
|
||||
# In production: send this to Slack/Email/PagerDuty/Ghaymah alerting webhook, e.g.:
|
||||
# curl -s -X POST -H "Content-Type: application/json" \
|
||||
# -d "{\"text\":\"ALERT: ${APP_URL} is down (${consecutive_failures} consecutive failures)\"}" \
|
||||
# "$ALERT_WEBHOOK_URL"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "${timestamp},${status},${http_code:-0},${response_time_ms}" >> "$LOG_FILE"
|
||||
|
||||
sleep "$CHECK_INTERVAL"
|
||||
done
|
||||
2
q1-deploy-monitor/requirements.txt
Normal file
2
q1-deploy-monitor/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
flask==3.0.3
|
||||
gunicorn==22.0.0
|
||||
المرجع في مشكلة جديدة
حظر مستخدم