هذا الالتزام موجود في:
2026-07-27 00:05:12 +03:00
التزام 80980bbf16
19 ملفات معدلة مع 1912 إضافات و0 حذوفات

عرض الملف

@@ -0,0 +1,8 @@
__pycache__/
*.pyc
.git
.gitignore
*.log
status.json
.venv/
README.md

عرض الملف

@@ -0,0 +1,30 @@
# ---- Base image -----------------------------------------------------------
FROM python:3.11-slim AS base
# Prevent .pyc files & enable unbuffered logging (important for container logs)
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PORT=8080
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/ ./app/
# ---- Run as a non-root user (security best practice) -----------------------
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
USER appuser
EXPOSE 8080
# ---- Container-level health check -----------------------------------------
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD python -c "import urllib.request,sys; \
sys.exit(0) if urllib.request.urlopen('http://127.0.0.1:8080/health', timeout=3).status == 200 else sys.exit(1)"
# ---- Production WSGI server -------------------------------------------------
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "--workers", "2", "--threads", "4", "--timeout", "30", "app.main:app"]

عرض الملف

@@ -0,0 +1,97 @@
# نشر تطبيق ومراقبته على غيمة (Ghaymah Containers)
حل كامل لنشر واجهة برمجية (API) بسيطة على منصة الحاويات في `ghaymah.systems`، مع سكربت مراقبة ولوحة تحكم مباشرة.
## محتويات المشروع
```
.
├── Dockerfile
├── requirements.txt
├── .dockerignore
├── app/
│ └── main.py # تطبيق Flask + /health + /metrics
├── health-check.sh # سكربت مراقبة بلغة bash (فحص كل 30 ثانية)
├── health-check.py
└── dashboard.html # لوحة مراقبة (HTML/CSS/JS) تعمل من المتصفح مباشرة
```
## 1) بناء واختبار الصورة محليًا
```bash
docker build -t sample-api:latest .
docker run --rm -p 8080:8080 sample-api:latest
# في نافذة أخرى
curl http://localhost:8080/health
curl http://localhost:8080/metrics
```
## 2) النشر على ghaymah.systems
منصة الحاويات في Ghaymah تدعم النشر إما عبر ربط مستودع Git (بناء تلقائي من الـ Dockerfile) أو عبر دفع صورة جاهزة إلى سجل الحاويات الخاص بها. الخطوات العامة:
1. سجّل الدخول إلى console الخاص بـ **ghaymah.systems**.
2. من قسم **Containers**، اختر **New Service / Deploy Container**.
3. اختر مصدر النشر:
- **Git Repository**: اربط المستودع الذي يحتوي هذا المشروع (يجب أن يحتوي على `Dockerfile` في الجذر) — المنصة تبني الصورة تلقائيًا من الـ Dockerfile.
- **أو Container Registry**: ابنِ وادفع الصورة يدويًا:
```bash
docker build -t <registry>/<namespace>/sample-api:1.0 .
docker push <registry>/<namespace>/sample-api:1.0
```
ثم أدخل مسار الصورة هذا عند إنشاء الخدمة.
4. اضبط إعدادات الخدمة:
- **Port**: `8080` (نفس المنفذ في `EXPOSE` وفي `PORT` env).
- **Health Check Path**: `/health` (المنصة تستخدمه لمعرفة جاهزية الحاوية).
- **Environment Variables**: أضف `PORT=8080` إن احتاج الأمر.
- **Resources**: ابدأ بأصغر خطة (0.5 vCPU / 256-512MB) كافية لهذا التطبيق التجريبي.
5. اضغط **Deploy**. بعد اكتمال النشر ستحصل على رابط عام مثل:
`https://sample-api-xxxx.ghaymah.systems`
6. تحقق من النشر:
```bash
curl https://sample-api-xxxx.ghaymah.systems/health
```
> ملاحظة: أسماء الأزرار والحقول الدقيقة قد تختلف قليلًا حسب نسخة الواجهة الحالية على `ghaymah.systems` — الخطوات أعلاه تعكس تدفق العمل القياسي لمنصات الحاويات (Container-as-a-Service)، راجع `docs.ghaymah.cloud` لأي تفاصيل محدّثة.
## 3) تشغيل سكربت المراقبة
بعد النشر، شغّل سكربت المراقبة موجّهًا إلى الرابط العام للتطبيق:
```bash
# Python (الخيار الموصى به)
python health-check.py --url https://sample-api-xxxx.ghaymah.systems --interval 30
# أو bash
chmod +x monitor.sh
./monitor.sh https://sample-api-xxxx.ghaymah.systems 30
```
السكربت يقوم بـ:
- فحص `/health` كل 30 ثانية.
- تسجيل الحالة وزمن الاستجابة في `monitor.log`.
- كتابة آخر نتيجة في `status.json`.
- إطلاق تنبيه (`ALERT`) في السجل بعد 3 فحوصات فاشلة متتالية.
يمكن تشغيله كخدمة نظام دائمة (systemd) أو كحاوية منفصلة (sidecar) بجانب التطبيق نفسه.
## 4) لوحة المراقبة (Dashboard)
افتح `dashboard.html` مباشرة في المتصفح (لا يحتاج خادمًا، ملف ثابت واحد):
1. أدخل الرابط العام للتطبيق في حقل الاتصال، مثل:
`https://sample-api-xxxx.ghaymah.systems`
2. اضغط **اتصال**.
تعرض اللوحة:
- **الحالة**: مؤشر أخضر (يعمل) / أحمر (متوقف)، مع نبض حي عند التشغيل السليم.
- **زمن الاستجابة**: آخر قيمة + رسم بياني لآخر 30 قراءة.
- **عدد الطلبات**: القيمة الإجمالية القادمة من `/metrics`.
- **مدة التشغيل** وسجل مباشر لكل عملية فحص.
اللوحة تستدعي `/metrics` كل 5 ثوانٍ عبر `fetch()` مباشرة من المتصفح؛ التطبيق (`main.py`) يضيف ترويسة `Access-Control-Allow-Origin: *` لذلك لا حاجة لخادم وسيط.
## ملاحظات إنتاجية (Production Notes)
- التطبيق يعمل بمستخدم غير جذري (`non-root user`) داخل الحاوية.
- يُستخدم `gunicorn` كخادم WSGI إنتاجي بدلاً من خادم التطوير المدمج في Flask.
- `HEALTHCHECK` مضمّن في الـ Dockerfile نفسه، بالإضافة إلى فحص خارجي (`health-check.py`) — طبقتا مراقبة مستقلتان.
- للتوسع: يمكن رفع عدد النسخ (replicas) من إعدادات الخدمة في Ghaymah دون تعديل الكود.

عرض الملف

@@ -0,0 +1,83 @@
"""
Simple Python API service.
Exposes:
GET / -> basic info
GET /health -> liveness/readiness probe
GET /metrics -> runtime metrics consumed by the monitoring dashboard
"""
import os
import time
import threading
from datetime import datetime, timezone
from flask import Flask, jsonify
app = Flask(__name__)
START_TIME = time.time()
# --- In-memory metrics (thread-safe) --------------------------------------
_lock = threading.Lock()
_metrics = {
"requests_total": 0,
"total_response_time_ms": 0.0,
}
@app.before_request
def _start_timer():
from flask import g
g.start_time = time.perf_counter()
@app.after_request
def _record_metrics(response):
from flask import g
elapsed_ms = (time.perf_counter() - getattr(g, "start_time", time.perf_counter())) * 1000
with _lock:
_metrics["requests_total"] += 1
_metrics["total_response_time_ms"] += elapsed_ms
# Allow the standalone dashboard.html to call this API from any origin
response.headers["Access-Control-Allow-Origin"] = "*"
return response
@app.route("/")
def index():
return jsonify({
"service": "sample-api",
"message": "API is running. See /health and /metrics."
})
@app.route("/health")
def health():
"""Used by the container platform's health checks and the monitor.py script."""
return jsonify({
"status": "healthy",
"timestamp": datetime.now(timezone.utc).isoformat(),
"uptime_seconds": round(time.time() - START_TIME, 2),
}), 200
@app.route("/metrics")
def metrics():
"""Used by dashboard.html to render live stats."""
with _lock:
total = _metrics["requests_total"]
total_time = _metrics["total_response_time_ms"]
avg_ms = round(total_time / total, 2) if total else 0.0
return jsonify({
"status": "healthy",
"uptime_seconds": round(time.time() - START_TIME, 2),
"requests_total": total,
"avg_response_time_ms": avg_ms,
"timestamp": datetime.now(timezone.utc).isoformat(),
})
if __name__ == "__main__":
port = int(os.environ.get("PORT", 8080))
app.run(host="0.0.0.0", port=port)

عرض الملف

@@ -0,0 +1,232 @@
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>لوحة مراقبة التطبيق</title>
<style>
:root{
--bg:#0b0f14;
--panel:#111823;
--panel-border:#1e2a38;
--text:#dbe4ee;
--muted:#7d8ba0;
--ok:#33d17a;
--bad:#f2495c;
--warn:#f2b632;
--accent:#3ea6ff;
--mono: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
}
*{box-sizing:border-box;}
body{
margin:0;
background:var(--bg);
color:var(--text);
font-family: var(--mono);
min-height:100vh;
padding:28px 20px 60px;
}
.wrap{max-width:960px;margin:0 auto;}
header{
display:flex;justify-content:space-between;align-items:flex-end;
margin-bottom:22px;flex-wrap:wrap;gap:12px;
}
h1{font-size:1.25rem;margin:0;letter-spacing:.02em;font-weight:600;}
.subtitle{color:var(--muted);font-size:.8rem;margin-top:4px;}
.config{display:flex;gap:8px;align-items:center;}
.config input{
background:#0f151d;border:1px solid var(--panel-border);color:var(--text);
padding:8px 10px;border-radius:6px;font-family:var(--mono);font-size:.8rem;width:280px;
}
.config button{
background:var(--accent);border:none;color:#04101c;font-weight:700;
padding:8px 14px;border-radius:6px;cursor:pointer;font-family:var(--mono);font-size:.8rem;
}
.config button:hover{filter:brightness(1.1);}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;margin-bottom:16px;}
.card{
background:var(--panel);border:1px solid var(--panel-border);border-radius:10px;
padding:16px 18px;position:relative;overflow:hidden;
}
.label{color:var(--muted);font-size:.72rem;text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px;}
.value{font-size:1.9rem;font-weight:700;line-height:1.1;}
.value.small{font-size:1.1rem;}
.status-row{display:flex;align-items:center;gap:10px;}
.dot{width:12px;height:12px;border-radius:50%;background:var(--muted);flex:0 0 auto;}
.dot.ok{background:var(--ok);box-shadow:0 0 0 0 rgba(51,209,122,.6);animation:pulse 2s infinite;}
.dot.bad{background:var(--bad);}
@keyframes pulse{
0%{box-shadow:0 0 0 0 rgba(51,209,122,.55);}
70%{box-shadow:0 0 0 8px rgba(51,209,122,0);}
100%{box-shadow:0 0 0 0 rgba(51,209,122,0);}
}
.chart-card{grid-column:1/-1;}
canvas{width:100%;height:140px;display:block;}
.log{
background:var(--panel);border:1px solid var(--panel-border);border-radius:10px;
padding:14px 18px;font-size:.76rem;color:var(--muted);max-height:170px;overflow-y:auto;
}
.log div{padding:2px 0;border-bottom:1px dashed #1a2431;}
.log .fail{color:var(--bad);}
.log .ok-line{color:var(--ok);}
footer{margin-top:18px;color:var(--muted);font-size:.7rem;text-align:center;}
</style>
</head>
<body>
<div class="wrap">
<header>
<div>
<h1>لوحة مراقبة التطبيق</h1>
<div class="subtitle">فحص كل <span id="intervalLabel">5</span> ثوانٍ · Ghaymah Containers</div>
</div>
<div class="config">
<input id="apiUrl" type="text" placeholder="https://your-app.ghaymah.systems" />
<button id="connectBtn">اتصال</button>
</div>
</header>
<div class="grid">
<div class="card">
<div class="label">الحالة</div>
<div class="status-row">
<div class="dot" id="statusDot"></div>
<div class="value small" id="statusText">غير متصل</div>
</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 class="card">
<div class="label">مدة التشغيل</div>
<div class="value small" id="uptimeValue">--</div>
</div>
</div>
<div class="card chart-card">
<div class="label">زمن الاستجابة (آخر 30 قراءة)</div>
<canvas id="chart"></canvas>
</div>
<div class="log" id="log"></div>
<footer>يعتمد على نقاط الوصول /health و /metrics · يعمل بالكامل من المتصفح</footer>
</div>
<script>
const POLL_MS = 5000;
const HISTORY_LEN = 30;
let latencyHistory = [];
let timer = null;
const els = {
dot: document.getElementById('statusDot'),
statusText: document.getElementById('statusText'),
latency: document.getElementById('latencyValue'),
requests: document.getElementById('requestsValue'),
uptime: document.getElementById('uptimeValue'),
log: document.getElementById('log'),
apiUrl: document.getElementById('apiUrl'),
connectBtn: document.getElementById('connectBtn'),
};
function fmtUptime(sec){
if(sec == null) return '--';
const h = Math.floor(sec/3600), m = Math.floor((sec%3600)/60), s = Math.floor(sec%60);
return `${h}س ${m}د ${s}ث`;
}
function addLog(msg, ok){
const line = document.createElement('div');
line.className = ok ? 'ok-line' : 'fail';
const time = new Date().toLocaleTimeString('ar-EG');
line.textContent = `[${time}] ${msg}`;
els.log.prepend(line);
while(els.log.children.length > 40) els.log.removeChild(els.log.lastChild);
}
function drawChart(){
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const dpr = window.devicePixelRatio || 1;
const w = canvas.clientWidth, h = canvas.clientHeight;
canvas.width = w * dpr; canvas.height = h * dpr;
ctx.scale(dpr, dpr);
ctx.clearRect(0,0,w,h);
if(latencyHistory.length < 2){
ctx.fillStyle = '#3a4a5c';
ctx.font = '12px monospace';
ctx.fillText('بانتظار بيانات كافية...', 10, h/2);
return;
}
const max = Math.max(...latencyHistory, 10) * 1.2;
const stepX = w / (HISTORY_LEN - 1);
ctx.strokeStyle = '#3ea6ff';
ctx.lineWidth = 2;
ctx.beginPath();
latencyHistory.forEach((v,i) => {
const x = i * stepX;
const y = h - (v / max) * (h - 10) - 5;
i === 0 ? ctx.moveTo(x,y) : ctx.lineTo(x,y);
});
ctx.stroke();
ctx.lineTo((latencyHistory.length-1)*stepX, h);
ctx.lineTo(0, h);
ctx.closePath();
ctx.fillStyle = 'rgba(62,166,255,0.12)';
ctx.fill();
}
async function poll(){
const base = els.apiUrl.value.trim().replace(/\/$/, '');
if(!base) return;
const start = performance.now();
try{
const res = await fetch(base + '/metrics', {cache:'no-store'});
const elapsed = Math.round(performance.now() - start);
if(!res.ok) throw new Error('HTTP ' + res.status);
const data = await res.json();
els.dot.className = 'dot ok';
els.statusText.textContent = 'يعمل';
els.latency.textContent = (data.avg_response_time_ms ?? elapsed) + ' ms';
els.requests.textContent = data.requests_total ?? '--';
els.uptime.textContent = fmtUptime(data.uptime_seconds);
latencyHistory.push(elapsed);
if(latencyHistory.length > HISTORY_LEN) latencyHistory.shift();
drawChart();
addLog(`الفحص ناجح - زمن الاستجابة ${elapsed}ms`, true);
}catch(err){
els.dot.className = 'dot bad';
els.statusText.textContent = 'متوقف / لا يستجيب';
addLog('فشل الفحص: ' + err.message, false);
}
}
els.connectBtn.addEventListener('click', () => {
if(timer) clearInterval(timer);
latencyHistory = [];
poll();
timer = setInterval(poll, POLL_MS);
});
window.addEventListener('resize', drawChart);
drawChart();
</script>
</body>
</html>

عرض الملف

@@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""
Lightweight uptime/health monitor.
Polls the target app's /health endpoint every CHECK_INTERVAL seconds,
logs status + response time, and writes the latest result to a JSON
file (status.json) that can be consumed by other tools or dashboards.
Usage:
python health-check.py --url https://your-app.ghaymah.systems
python health-check.py --url https://your-app.ghaymah.systems --interval 30
"""
import argparse
import json
import logging
import time
import urllib.request
import urllib.error
from datetime import datetime, timezone
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler("monitor.log"),
logging.StreamHandler(),
],
)
log = logging.getLogger("monitor")
STATUS_FILE = "status.json"
FAILURE_THRESHOLD = 3 # consecutive failures before raising an "ALERT"
def check_health(url: str, timeout: float = 5.0):
start = time.perf_counter()
try:
with urllib.request.urlopen(url, timeout=timeout) as resp:
elapsed_ms = round((time.perf_counter() - start) * 1000, 2)
body = json.loads(resp.read().decode())
return {
"ok": resp.status == 200,
"http_status": resp.status,
"response_time_ms": elapsed_ms,
"body": body,
}
except urllib.error.HTTPError as e:
elapsed_ms = round((time.perf_counter() - start) * 1000, 2)
return {"ok": False, "http_status": e.code, "response_time_ms": elapsed_ms, "error": str(e)}
except Exception as e: # DNS errors, timeouts, connection refused, etc.
elapsed_ms = round((time.perf_counter() - start) * 1000, 2)
return {"ok": False, "http_status": None, "response_time_ms": elapsed_ms, "error": str(e)}
def write_status(result: dict, consecutive_failures: int):
payload = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"status": "healthy" if result["ok"] else "unhealthy",
"http_status": result.get("http_status"),
"response_time_ms": result.get("response_time_ms"),
"consecutive_failures": consecutive_failures,
}
with open(STATUS_FILE, "w") as f:
json.dump(payload, f, indent=2)
def main():
parser = argparse.ArgumentParser(description="Poll /health endpoint on an interval.")
parser.add_argument("--url", required=True, help="Base URL of the app, e.g. https://myapp.ghaymah.systems")
parser.add_argument("--interval", type=int, default=30, help="Seconds between checks (default: 30)")
args = parser.parse_args()
health_url = args.url.rstrip("/") + "/health"
log.info("Starting monitor for %s every %ss", health_url, args.interval)
consecutive_failures = 0
while True:
result = check_health(health_url)
write_status(result, consecutive_failures)
if result["ok"]:
if consecutive_failures > 0:
log.info("Service RECOVERED after %d failed check(s).", consecutive_failures)
consecutive_failures = 0
log.info("OK status=%s response_time=%sms", result["http_status"], result["response_time_ms"])
else:
consecutive_failures += 1
log.warning(
"FAIL attempt=%d status=%s error=%s",
consecutive_failures, result.get("http_status"), result.get("error"),
)
if consecutive_failures >= FAILURE_THRESHOLD:
log.error("ALERT: %s has failed %d consecutive health checks!", args.url, consecutive_failures)
time.sleep(args.interval)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
log.info("Monitor stopped by user.")

عرض الملف

@@ -0,0 +1,42 @@
#!/usr/bin/env bash
#
# Simple bash health monitor - polls /health every 30s.
# Usage: ./monitor.sh https://your-app.ghaymah.systems [interval_seconds]
set -euo pipefail
BASE_URL="${1:?Usage: $0 <base_url> [interval_seconds]}"
INTERVAL="${2:-30}"
HEALTH_URL="${BASE_URL%/}/health"
LOG_FILE="monitor.log"
FAILURE_THRESHOLD=3
failures=0
log() {
echo "$(date -u +"%Y-%m-%dT%H:%M:%SZ") [$1] $2" | tee -a "$LOG_FILE"
}
log "INFO" "Starting monitor for $HEALTH_URL every ${INTERVAL}s"
while true; do
start_ns=$(date +%s%N)
http_code=$(curl -o /tmp/health_resp.json -s -w "%{http_code}" --max-time 5 "$HEALTH_URL" || echo "000")
end_ns=$(date +%s%N)
elapsed_ms=$(( (end_ns - start_ns) / 1000000 ))
if [ "$http_code" == "200" ]; then
if [ "$failures" -gt 0 ]; then
log "INFO" "Service RECOVERED after $failures failed check(s)."
fi
failures=0
log "INFO" "OK status=$http_code response_time=${elapsed_ms}ms"
else
failures=$((failures + 1))
log "WARN" "FAIL attempt=$failures status=$http_code"
if [ "$failures" -ge "$FAILURE_THRESHOLD" ]; then
log "ERROR" "ALERT: $BASE_URL has failed $failures consecutive health checks!"
fi
fi
sleep "$INTERVAL"
done

عرض الملف

@@ -0,0 +1,2 @@
flask==3.0.3
gunicorn==22.0.0