Upload files to "q1-deploy-monitor"
هذا الالتزام موجود في:
35
q1-deploy-monitor/Dockerfile
Normal file
35
q1-deploy-monitor/Dockerfile
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# --- Build/deps stage ---
|
||||||
|
FROM python:3.12-slim AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install build deps only where needed, keep layer cacheable
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir --user -r requirements.txt
|
||||||
|
|
||||||
|
# --- Runtime stage ---
|
||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
# Security: run as a non-root user (required best practice for ghaymah.systems containers)
|
||||||
|
RUN useradd --create-home --shell /usr/sbin/nologin appuser
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /root/.local /home/appuser/.local
|
||||||
|
COPY app.py .
|
||||||
|
|
||||||
|
ENV PATH=/home/appuser/.local/bin:$PATH \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
PORT=8080
|
||||||
|
|
||||||
|
RUN chown -R appuser:appuser /app
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Container-native healthcheck (in addition to ghaymah platform probes hitting /health)
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
|
CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8080/health', timeout=2).status==200 else 1)"
|
||||||
|
|
||||||
|
# Use gunicorn in production instead of Flask's dev server
|
||||||
|
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "--workers", "2", "--threads", "4", "--timeout", "30", "app:app"]
|
||||||
98
q1-deploy-monitor/dashboard.html
Normal file
98
q1-deploy-monitor/dashboard.html
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<!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 App Dashboard</title>
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--bg:#0b0f14; --card:#121821; --line:#212a36;
|
||||||
|
--ok:#22c55e; --bad:#ef4444; --text:#e6edf3; --muted:#8b98a8; --accent:#3b82f6;
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
body{margin:0;background:var(--bg);color:var(--text);font-family:-apple-system,Segoe UI,Tahoma,Arial,sans-serif;padding:24px}
|
||||||
|
h1{font-size:20px;margin:0 0 20px}
|
||||||
|
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;margin-bottom:20px}
|
||||||
|
.card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:18px}
|
||||||
|
.label{color:var(--muted);font-size:13px;margin-bottom:8px}
|
||||||
|
.value{font-size:28px;font-weight:700}
|
||||||
|
.status-dot{display:inline-block;width:12px;height:12px;border-radius:50%;margin-left:8px}
|
||||||
|
.up{background:var(--ok)} .down{background:var(--bad)}
|
||||||
|
table{width:100%;border-collapse:collapse;background:var(--card);border:1px solid var(--line);border-radius:12px;overflow:hidden}
|
||||||
|
th,td{padding:10px 14px;text-align:right;border-bottom:1px solid var(--line);font-size:13px}
|
||||||
|
th{color:var(--muted);font-weight:600}
|
||||||
|
tr:last-child td{border-bottom:none}
|
||||||
|
.pill{padding:2px 8px;border-radius:6px;font-size:12px}
|
||||||
|
.pill.ok{background:rgba(34,197,94,.15);color:var(--ok)}
|
||||||
|
.pill.bad{background:rgba(239,68,68,.15);color:var(--bad)}
|
||||||
|
.footer{color:var(--muted);font-size:12px;margin-top:14px}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>📊 لوحة مراقبة التطبيق — myapp.ghaymah.systems</h1>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<div class="card">
|
||||||
|
<div class="label">الحالة الحالية</div>
|
||||||
|
<div class="value" id="statusValue">--<span class="status-dot" id="statusDot"></span></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="label">آخر زمن استجابة</div>
|
||||||
|
<div class="value" id="latencyValue">-- ms</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="label">إجمالي الطلبات</div>
|
||||||
|
<div class="value" id="requestsValue">--</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="label">آخر تحديث</div>
|
||||||
|
<div class="value" id="lastCheckValue" style="font-size:16px">--</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>الوقت</th><th>الحالة</th><th>Status Code</th><th>زمن الاستجابة (ms)</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="historyBody"></tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="footer">يقرأ هذا الـ dashboard البيانات من <code>status.json</code> الذي يولّده <code>health-check.sh</code>. يُحدَّث تلقائياً كل 15 ثانية.</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const STATE_URL = "status.json"; // same-origin file written by health-check.sh
|
||||||
|
const POLL_MS = 15000;
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(STATE_URL + "?_=" + Date.now(), { cache: "no-store" });
|
||||||
|
const data = await res.json();
|
||||||
|
const last = data.last_check || (data.history || []).slice(-1)[0];
|
||||||
|
if (!last) return;
|
||||||
|
|
||||||
|
document.getElementById("statusValue").innerHTML =
|
||||||
|
(last.healthy ? "متصل" : "متعطل") +
|
||||||
|
`<span class="status-dot ${last.healthy ? "up" : "down"}"></span>`;
|
||||||
|
document.getElementById("latencyValue").textContent = `${last.latency_ms.toFixed(0)} ms`;
|
||||||
|
document.getElementById("requestsValue").textContent = last.request_count ?? "--";
|
||||||
|
document.getElementById("lastCheckValue").textContent = new Date(last.timestamp).toLocaleString("ar-EG");
|
||||||
|
|
||||||
|
const rows = (data.history || []).slice(-10).reverse().map(h => `
|
||||||
|
<tr>
|
||||||
|
<td>${new Date(h.timestamp).toLocaleTimeString("ar-EG")}</td>
|
||||||
|
<td><span class="pill ${h.healthy ? "ok" : "bad"}">${h.healthy ? "OK" : "FAIL"}</span></td>
|
||||||
|
<td>${h.status_code}</td>
|
||||||
|
<td>${h.latency_ms.toFixed(0)}</td>
|
||||||
|
</tr>`).join("");
|
||||||
|
document.getElementById("historyBody").innerHTML = rows;
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById("statusValue").innerHTML = `تعذر التحميل<span class="status-dot down"></span>`;
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
setInterval(refresh, POLL_MS);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
99
q1-deploy-monitor/health-check.sh
Normal file
99
q1-deploy-monitor/health-check.sh
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# health-check.sh
|
||||||
|
# Polls the deployed app's /health and /metrics endpoints every 30 seconds,
|
||||||
|
# logs status + response time, and writes a rolling JSON snapshot that
|
||||||
|
# dashboard.html polls to render live status.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./health-check.sh https://myapp.ghaymah.systems
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
APP_URL="${1:-https://myapp.ghaymah.systems}"
|
||||||
|
INTERVAL_SECONDS=30
|
||||||
|
LOG_FILE="./health-check.log"
|
||||||
|
STATE_FILE="./status.json" # consumed by dashboard.html
|
||||||
|
MAX_HISTORY=100 # keep last N checks in status.json
|
||||||
|
ALERT_WEBHOOK="${ALERT_WEBHOOK_URL:-}" # optional: Slack/Telegram webhook for alerting
|
||||||
|
|
||||||
|
FAIL_STREAK=0
|
||||||
|
FAIL_THRESHOLD=3 # alert after 3 consecutive failures (90s of downtime)
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo "$(date -u '+%Y-%m-%dT%H:%M:%SZ') $*" | tee -a "$LOG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
send_alert() {
|
||||||
|
local message="$1"
|
||||||
|
log "ALERT: ${message}"
|
||||||
|
if [[ -n "$ALERT_WEBHOOK" ]]; then
|
||||||
|
curl -s -X POST -H 'Content-Type: application/json' \
|
||||||
|
-d "{\"text\": \"🚨 ${message}\"}" "$ALERT_WEBHOOK" >/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
init_state_file() {
|
||||||
|
[[ -f "$STATE_FILE" ]] || echo '{"history": []}' > "$STATE_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
append_result() {
|
||||||
|
local timestamp="$1" status_code="$2" latency_ms="$3" request_count="$4" healthy="$5"
|
||||||
|
|
||||||
|
local tmp
|
||||||
|
tmp=$(mktemp)
|
||||||
|
python3 - "$STATE_FILE" "$timestamp" "$status_code" "$latency_ms" "$request_count" "$healthy" "$MAX_HISTORY" <<'PYEOF' > "$tmp"
|
||||||
|
import json, sys
|
||||||
|
|
||||||
|
state_file, ts, code, latency, req_count, healthy, max_hist = sys.argv[1:8]
|
||||||
|
try:
|
||||||
|
with open(state_file) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
|
data = {"history": []}
|
||||||
|
|
||||||
|
data["history"].append({
|
||||||
|
"timestamp": ts,
|
||||||
|
"status_code": int(code),
|
||||||
|
"latency_ms": float(latency),
|
||||||
|
"request_count": int(req_count) if req_count.isdigit() else None,
|
||||||
|
"healthy": healthy == "1",
|
||||||
|
})
|
||||||
|
data["history"] = data["history"][-int(max_hist):]
|
||||||
|
data["last_check"] = data["history"][-1]
|
||||||
|
print(json.dumps(data, ensure_ascii=False))
|
||||||
|
PYEOF
|
||||||
|
mv "$tmp" "$STATE_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
log "Starting monitor for ${APP_URL} (interval=${INTERVAL_SECONDS}s)"
|
||||||
|
init_state_file
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
START_MS=$(date +%s%3N)
|
||||||
|
|
||||||
|
# -o /dev/null discards body, -w prints status code, --max-time caps hang time
|
||||||
|
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "${APP_URL}/health" || echo "000")
|
||||||
|
|
||||||
|
END_MS=$(date +%s%3N)
|
||||||
|
LATENCY_MS=$((END_MS - START_MS))
|
||||||
|
|
||||||
|
REQUEST_COUNT=$(curl -s --max-time 5 "${APP_URL}/metrics" 2>/dev/null \
|
||||||
|
| python3 -c "import sys,json; print(json.load(sys.stdin).get('request_count',''))" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [[ "$HTTP_CODE" == "200" ]]; then
|
||||||
|
log "OK status=${HTTP_CODE} latency=${LATENCY_MS}ms requests=${REQUEST_COUNT}"
|
||||||
|
FAIL_STREAK=0
|
||||||
|
append_result "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$HTTP_CODE" "$LATENCY_MS" "${REQUEST_COUNT:-0}" 1
|
||||||
|
else
|
||||||
|
FAIL_STREAK=$((FAIL_STREAK + 1))
|
||||||
|
log "FAIL status=${HTTP_CODE} latency=${LATENCY_MS}ms fail_streak=${FAIL_STREAK}"
|
||||||
|
append_result "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$HTTP_CODE" "$LATENCY_MS" "${REQUEST_COUNT:-0}" 0
|
||||||
|
|
||||||
|
if (( FAIL_STREAK >= FAIL_THRESHOLD )); then
|
||||||
|
send_alert "${APP_URL} has failed ${FAIL_STREAK} consecutive health checks (last status=${HTTP_CODE})"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep "$INTERVAL_SECONDS"
|
||||||
|
done
|
||||||
المرجع في مشكلة جديدة
حظر مستخدم