first commit
هذا الالتزام موجود في:
8
q1-deploy-monitor/.dockerignore
Normal file
8
q1-deploy-monitor/.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.git
|
||||
.gitignore
|
||||
*.log
|
||||
status.json
|
||||
.venv/
|
||||
README.md
|
||||
30
q1-deploy-monitor/Dockerfile
Normal file
30
q1-deploy-monitor/Dockerfile
Normal file
@@ -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"]
|
||||
97
q1-deploy-monitor/README.md
Normal file
97
q1-deploy-monitor/README.md
Normal file
@@ -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 دون تعديل الكود.
|
||||
83
q1-deploy-monitor/app/main.py
Normal file
83
q1-deploy-monitor/app/main.py
Normal file
@@ -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)
|
||||
232
q1-deploy-monitor/dashboard.html
Normal file
232
q1-deploy-monitor/dashboard.html
Normal file
@@ -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>
|
||||
104
q1-deploy-monitor/health-check.py
Normal file
104
q1-deploy-monitor/health-check.py
Normal file
@@ -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.")
|
||||
42
q1-deploy-monitor/health-check.sh
Normal file
42
q1-deploy-monitor/health-check.sh
Normal file
@@ -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
|
||||
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
|
||||
المرجع في مشكلة جديدة
حظر مستخدم