Ghaymah intern
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY monitor.py ./
|
||||
COPY dashboard/ ./dashboard/
|
||||
COPY start.sh ./
|
||||
|
||||
RUN chmod +x start.sh && mkdir -p dashboard/data
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
||||
CMD python3 -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/', timeout=5)" || exit 1
|
||||
|
||||
CMD ["./start.sh"]
|
||||
@@ -0,0 +1,103 @@
|
||||
# لوحة مراقبة mithal.space
|
||||
|
||||
مشروع كامل: سكريبت بايثون بيجمع 5 مقاييس كل دقيقة من `mithal.space`، وداشبورد HTML/CSS/JS بيعرضها لايف.
|
||||
|
||||
## البنية
|
||||
|
||||
```
|
||||
mithal-monitor/
|
||||
├── monitor.py ← يجمع المقاييس كل 60 ثانية
|
||||
├── start.sh ← يشغّل monitor.py + سيرفر الداشبورد معًا
|
||||
├── Dockerfile
|
||||
├── dashboard/
|
||||
│ ├── index.html
|
||||
│ ├── style.css
|
||||
│ ├── app.js
|
||||
│ └── data/ ← يتكتب فيه تلقائيًا وقت التشغيل
|
||||
│ ├── metrics_history.json (يقرأه الداشبورد)
|
||||
│ └── metrics_log.csv (أرشيف كامل لكل فحص)
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## المقاييس المُجمّعة كل دقيقة
|
||||
|
||||
| المقياس | الطريقة |
|
||||
|---|---|
|
||||
| Latency | زمن `HTTP GET /` بالميلي ثانية |
|
||||
| Uptime | `status_code` بين 200-399 = up |
|
||||
| SSL | الاتصال بالمنفذ 443 وقراءة تاريخ انتهاء الشهادة |
|
||||
| DNS | زمن `socket.getaddrinfo()` بالميلي ثانية |
|
||||
| Search Response | زمن `GET /search?q=test` |
|
||||
|
||||
> ⚠️ رابط البحث `SEARCH_URL` في `monitor.py` مبني على تخمين معقول (`/search?q=`). لو الرابط الفعلي لصفحة نتائج البحث في mithal.space مختلف، عدّل المتغير في أول الملف.
|
||||
|
||||
## التجربة المحلية (بدون Docker)
|
||||
|
||||
```
|
||||
cd mithal-monitor
|
||||
python3 monitor.py
|
||||
```
|
||||
سيبه شغال دقيقة أو اتنين، هيبدأ يطبع نتائج كل فحص في الترمينال، وينشئ:
|
||||
```
|
||||
dashboard/data/metrics_history.json
|
||||
dashboard/data/metrics_log.csv
|
||||
```
|
||||
|
||||
في تاب تاني، شغّل سيرفر للداشبورد:
|
||||
```
|
||||
cd dashboard
|
||||
python3 -m http.server 8080
|
||||
```
|
||||
افتح المتصفح على:
|
||||
```
|
||||
http://localhost:8080/
|
||||
```
|
||||
|
||||
## التجربة بالـ Docker
|
||||
|
||||
```
|
||||
docker build -t mithal-monitor:local .
|
||||
docker run -d -p 8080:8080 --name mithal-monitor mithal-monitor:local
|
||||
```
|
||||
افتح `http://localhost:8080/` وانتظر دقيقة عشان أول فحص يكتمل ويظهر في الداشبورد.
|
||||
|
||||
```
|
||||
docker logs -f mithal-monitor
|
||||
```
|
||||
هتشوف نتائج كل فحص لايف.
|
||||
|
||||
```
|
||||
docker stop mithal-monitor
|
||||
docker rm mithal-monitor
|
||||
```
|
||||
|
||||
## النشر على ghaymah.systems
|
||||
|
||||
بما إن Ghaymah بتستخدم Git-Based Deployment (تاخد الكود من GitHub مباشرة وتبنيه بنفسها):
|
||||
|
||||
1. ارفع المشروع على GitHub:
|
||||
```
|
||||
git init
|
||||
git add .
|
||||
git commit -m "mithal.space monitoring dashboard"
|
||||
git branch -M main
|
||||
git remote add origin https://github.com/Moetaz-Alfy/mithal-monitor.git
|
||||
git push -u origin main
|
||||
```
|
||||
|
||||
2. في لوحة تحكم Ghaymah:
|
||||
- أنشئ **Project** جديد
|
||||
- **Deploy Application** → **Git-Based Deployment → Deploying from Public GitHub**
|
||||
- Application Name: `mithal-monitor`
|
||||
- Repository URL: رابط الريبو
|
||||
- Branch/Commit SHA: أول commit
|
||||
- Port: `8080`
|
||||
- Public Access: مفعّل
|
||||
|
||||
3. بعد ما ياخد رابط عام، افتحه وانتظر دقيقة لأول فحص، وهتشوف الداشبورد شغال لايف.
|
||||
|
||||
## ملاحظات مهمة
|
||||
|
||||
- **البيانات مؤقتة داخل الحاوية**: `dashboard/data/` بتتكتب جوه نفس الحاوية. لو الحاوية اتعاد تشغيلها، البيانات التاريخية بتضيع ويبدأ الجمع من الصفر. لو محتاج الاحتفاظ بالتاريخ عبر إعادة التشغيل، اربط مجلد `dashboard/data/` بـ **Ghaymah Block Storage** (Persistent Volume) بدل ما يفضل جوه الحاوية.
|
||||
- **حماية الموقع (403)**: بعض الشبكات السحابية بترجع `403 Forbidden` لو الموقع عنده حماية ضد البوتات. لو حصل كده وأنت متأكد إن الموقع شغال فعليًا، جرب تشغيل المراقبة من شبكة/سيرفر تاني.
|
||||
- **معدل الفحص**: الداشبورد بيحدّث نفسه كل 60 ثانية تلقائيًا (`REFRESH_INTERVAL_MS` في `app.js`)، بالتوازي مع دورة الفحص نفسها كل 60 ثانية في `monitor.py`.
|
||||
@@ -0,0 +1,138 @@
|
||||
const REFRESH_INTERVAL_MS = 60000;
|
||||
let chart = null;
|
||||
|
||||
function fmtDate(iso) {
|
||||
const d = new Date(iso);
|
||||
return d.toLocaleString('ar-EG', { hour12: false });
|
||||
}
|
||||
|
||||
function fmtTimeShort(iso) {
|
||||
const d = new Date(iso);
|
||||
return d.toLocaleTimeString('ar-EG', { hour12: false });
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
try {
|
||||
const res = await fetch('data/metrics_history.json', { cache: 'no-store' });
|
||||
if (!res.ok) throw new Error('metrics file not found yet');
|
||||
const history = await res.json();
|
||||
if (!history.length) throw new Error('no data yet');
|
||||
render(history);
|
||||
} catch (err) {
|
||||
document.getElementById('overallStatus').textContent = 'بانتظار أول فحص...';
|
||||
document.getElementById('logTableBody').innerHTML =
|
||||
`<tr><td colspan="7">${err.message}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
function render(history) {
|
||||
const latest = history[history.length - 1];
|
||||
|
||||
// ---- Overall status pill ----
|
||||
const statusPill = document.getElementById('overallStatus');
|
||||
statusPill.textContent = latest.up ? 'الموقع شغال (UP)' : 'الموقع متوقف (DOWN)';
|
||||
statusPill.className = 'status-pill ' + (latest.up ? 'up' : 'down');
|
||||
|
||||
// ---- Uptime (last 24h) ----
|
||||
const now = new Date();
|
||||
const last24h = history.filter(r => (now - new Date(r.timestamp)) <= 24 * 60 * 60 * 1000);
|
||||
const upCount = last24h.filter(r => r.up).length;
|
||||
const uptimePct = last24h.length ? ((upCount / last24h.length) * 100).toFixed(2) : '--';
|
||||
document.getElementById('uptimeValue').textContent = uptimePct + '%';
|
||||
document.getElementById('uptimeDetail').textContent = `${upCount} من ${last24h.length} فحص`;
|
||||
|
||||
// ---- Latest latency ----
|
||||
document.getElementById('latencyValue').textContent =
|
||||
latest.latency_ms != null ? Math.round(latest.latency_ms) + ' ms' : '--';
|
||||
document.getElementById('latencyDetail').textContent = 'آخر فحص: ' + fmtTimeShort(latest.timestamp);
|
||||
|
||||
// ---- SSL ----
|
||||
const sslEl = document.getElementById('sslValue');
|
||||
const sslDetail = document.getElementById('sslDetail');
|
||||
if (latest.ssl_valid) {
|
||||
sslEl.textContent = latest.ssl_days_remaining + ' يوم متبقي';
|
||||
sslEl.style.color = latest.ssl_days_remaining < 14 ? '#e74c3c' : '#2ecc71';
|
||||
sslDetail.textContent = 'تنتهي في: ' + fmtDate(latest.ssl_expiry);
|
||||
} else {
|
||||
sslEl.textContent = 'غير صالحة';
|
||||
sslEl.style.color = '#e74c3c';
|
||||
sslDetail.textContent = 'فشل التحقق من الشهادة';
|
||||
}
|
||||
|
||||
// ---- DNS ----
|
||||
document.getElementById('dnsValue').textContent =
|
||||
latest.dns_ms != null ? Math.round(latest.dns_ms) + ' ms' : '--';
|
||||
document.getElementById('dnsDetail').textContent = 'آخر فحص: ' + fmtTimeShort(latest.timestamp);
|
||||
|
||||
// ---- Search response ----
|
||||
document.getElementById('searchValue').textContent =
|
||||
latest.search_latency_ms != null ? Math.round(latest.search_latency_ms) + ' ms' : '--';
|
||||
document.getElementById('searchDetail').textContent =
|
||||
'Status: ' + (latest.search_status ?? '--');
|
||||
|
||||
// ---- Chart: last hour of latency ----
|
||||
const lastHour = history.filter(r => (now - new Date(r.timestamp)) <= 60 * 60 * 1000);
|
||||
renderChart(lastHour);
|
||||
|
||||
// ---- Log table: last 10 checks ----
|
||||
renderLogTable(history.slice(-10).reverse());
|
||||
}
|
||||
|
||||
function renderChart(data) {
|
||||
const labels = data.map(r => fmtTimeShort(r.timestamp));
|
||||
const values = data.map(r => r.latency_ms);
|
||||
|
||||
const ctx = document.getElementById('latencyChart');
|
||||
if (chart) {
|
||||
chart.data.labels = labels;
|
||||
chart.data.datasets[0].data = values;
|
||||
chart.update();
|
||||
return;
|
||||
}
|
||||
|
||||
chart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels,
|
||||
datasets: [{
|
||||
label: 'زمن الاستجابة (ms)',
|
||||
data: values,
|
||||
borderColor: '#4f8cff',
|
||||
backgroundColor: 'rgba(79,140,255,0.1)',
|
||||
tension: 0.3,
|
||||
fill: true,
|
||||
pointRadius: 2,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: {
|
||||
x: { ticks: { color: '#5b6579', maxTicksLimit: 8 }, grid: { color: '#232d42' } },
|
||||
y: { ticks: { color: '#5b6579' }, grid: { color: '#232d42' }, beginAtZero: true }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderLogTable(rows) {
|
||||
const tbody = document.getElementById('logTableBody');
|
||||
if (!rows.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="7">لا توجد بيانات بعد</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = rows.map(r => `
|
||||
<tr>
|
||||
<td>${fmtDate(r.timestamp)}</td>
|
||||
<td class="${r.up ? 'status-up' : 'status-down'}">${r.up ? 'UP' : 'DOWN'}</td>
|
||||
<td>${r.status_code ?? '--'}</td>
|
||||
<td>${r.latency_ms != null ? Math.round(r.latency_ms) + ' ms' : '--'}</td>
|
||||
<td>${r.dns_ms != null ? Math.round(r.dns_ms) + ' ms' : '--'}</td>
|
||||
<td>${r.ssl_valid ? r.ssl_days_remaining + ' يوم' : 'غير صالحة'}</td>
|
||||
<td>${r.search_latency_ms != null ? Math.round(r.search_latency_ms) + ' ms' : '--'}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
loadData();
|
||||
setInterval(loadData, REFRESH_INTERVAL_MS);
|
||||
@@ -0,0 +1,132 @@
|
||||
[
|
||||
{
|
||||
"timestamp": "2026-07-27T11:56:17.989937+00:00",
|
||||
"error": null,
|
||||
"dns_ms": 49.33,
|
||||
"ssl_valid": true,
|
||||
"ssl_days_remaining": 50,
|
||||
"ssl_expiry": "2026-09-15T13:10:47+00:00",
|
||||
"status_code": 200,
|
||||
"latency_ms": 511.77,
|
||||
"up": true,
|
||||
"search_status": 200,
|
||||
"search_latency_ms": 527.81
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-07-27T11:58:23.314840+00:00",
|
||||
"error": null,
|
||||
"dns_ms": 37.98,
|
||||
"ssl_valid": true,
|
||||
"ssl_days_remaining": 50,
|
||||
"ssl_expiry": "2026-09-15T13:10:47+00:00",
|
||||
"status_code": 200,
|
||||
"latency_ms": 504.57,
|
||||
"up": true,
|
||||
"search_status": 200,
|
||||
"search_latency_ms": 471.32
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-07-27T11:59:24.683553+00:00",
|
||||
"error": null,
|
||||
"dns_ms": 0.51,
|
||||
"ssl_valid": true,
|
||||
"ssl_days_remaining": 50,
|
||||
"ssl_expiry": "2026-09-15T13:10:47+00:00",
|
||||
"status_code": 200,
|
||||
"latency_ms": 462.48,
|
||||
"up": true,
|
||||
"search_status": 200,
|
||||
"search_latency_ms": 477.04
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-07-27T12:00:25.962461+00:00",
|
||||
"error": null,
|
||||
"dns_ms": 31.21,
|
||||
"ssl_valid": true,
|
||||
"ssl_days_remaining": 50,
|
||||
"ssl_expiry": "2026-09-15T13:10:47+00:00",
|
||||
"status_code": 200,
|
||||
"latency_ms": 459.58,
|
||||
"up": true,
|
||||
"search_status": 200,
|
||||
"search_latency_ms": 483.24
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-07-27T12:01:27.276170+00:00",
|
||||
"error": null,
|
||||
"dns_ms": 0.57,
|
||||
"ssl_valid": true,
|
||||
"ssl_days_remaining": 50,
|
||||
"ssl_expiry": "2026-09-15T13:10:47+00:00",
|
||||
"status_code": 200,
|
||||
"latency_ms": 486.89,
|
||||
"up": true,
|
||||
"search_status": 200,
|
||||
"search_latency_ms": 453.1
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-07-27T12:02:28.551923+00:00",
|
||||
"error": null,
|
||||
"dns_ms": 30.0,
|
||||
"ssl_valid": true,
|
||||
"ssl_days_remaining": 50,
|
||||
"ssl_expiry": "2026-09-15T13:10:47+00:00",
|
||||
"status_code": 200,
|
||||
"latency_ms": 452.81,
|
||||
"up": true,
|
||||
"search_status": 200,
|
||||
"search_latency_ms": 471.59
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-07-27T12:03:29.838129+00:00",
|
||||
"error": null,
|
||||
"dns_ms": 0.56,
|
||||
"ssl_valid": true,
|
||||
"ssl_days_remaining": 50,
|
||||
"ssl_expiry": "2026-09-15T13:10:47+00:00",
|
||||
"status_code": 200,
|
||||
"latency_ms": 468.29,
|
||||
"up": true,
|
||||
"search_status": 200,
|
||||
"search_latency_ms": 482.41
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-07-27T12:04:31.124093+00:00",
|
||||
"error": null,
|
||||
"dns_ms": 37.49,
|
||||
"ssl_valid": true,
|
||||
"ssl_days_remaining": 50,
|
||||
"ssl_expiry": "2026-09-15T13:10:47+00:00",
|
||||
"status_code": 200,
|
||||
"latency_ms": 461.85,
|
||||
"up": true,
|
||||
"search_status": 200,
|
||||
"search_latency_ms": 487.46
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-07-27T12:05:32.449489+00:00",
|
||||
"error": null,
|
||||
"dns_ms": 0.58,
|
||||
"ssl_valid": true,
|
||||
"ssl_days_remaining": 50,
|
||||
"ssl_expiry": "2026-09-15T13:10:47+00:00",
|
||||
"status_code": 200,
|
||||
"latency_ms": 625.12,
|
||||
"up": true,
|
||||
"search_status": 200,
|
||||
"search_latency_ms": 582.92
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-07-27T12:06:34.902831+00:00",
|
||||
"error": null,
|
||||
"dns_ms": 34.69,
|
||||
"ssl_valid": true,
|
||||
"ssl_days_remaining": 50,
|
||||
"ssl_expiry": "2026-09-15T13:10:47+00:00",
|
||||
"status_code": 200,
|
||||
"latency_ms": 505.42,
|
||||
"up": true,
|
||||
"search_status": 200,
|
||||
"search_latency_ms": 453.19
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
timestamp,up,status_code,latency_ms,dns_ms,ssl_valid,ssl_days_remaining,ssl_expiry,search_status,search_latency_ms,error
|
||||
2026-07-27T11:56:17.989937+00:00,True,200,511.77,49.33,True,50,2026-09-15T13:10:47+00:00,200,527.81,
|
||||
2026-07-27T11:58:23.314840+00:00,True,200,504.57,37.98,True,50,2026-09-15T13:10:47+00:00,200,471.32,
|
||||
2026-07-27T11:59:24.683553+00:00,True,200,462.48,0.51,True,50,2026-09-15T13:10:47+00:00,200,477.04,
|
||||
2026-07-27T12:00:25.962461+00:00,True,200,459.58,31.21,True,50,2026-09-15T13:10:47+00:00,200,483.24,
|
||||
2026-07-27T12:01:27.276170+00:00,True,200,486.89,0.57,True,50,2026-09-15T13:10:47+00:00,200,453.1,
|
||||
2026-07-27T12:02:28.551923+00:00,True,200,452.81,30.0,True,50,2026-09-15T13:10:47+00:00,200,471.59,
|
||||
2026-07-27T12:03:29.838129+00:00,True,200,468.29,0.56,True,50,2026-09-15T13:10:47+00:00,200,482.41,
|
||||
2026-07-27T12:04:31.124093+00:00,True,200,461.85,37.49,True,50,2026-09-15T13:10:47+00:00,200,487.46,
|
||||
2026-07-27T12:05:32.449489+00:00,True,200,625.12,0.58,True,50,2026-09-15T13:10:47+00:00,200,582.92,
|
||||
2026-07-27T12:06:34.902831+00:00,True,200,505.42,34.69,True,50,2026-09-15T13:10:47+00:00,200,453.19,
|
||||
|
@@ -0,0 +1,82 @@
|
||||
<!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>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<h1>لوحة مراقبة mithal.space</h1>
|
||||
<div class="status-pill" id="overallStatus">جاري التحميل...</div>
|
||||
</header>
|
||||
|
||||
<section class="grid">
|
||||
|
||||
<div class="card">
|
||||
<div class="card-label">Uptime (آخر 24 ساعة)</div>
|
||||
<div class="big-value" id="uptimeValue">--%</div>
|
||||
<div class="sub-value" id="uptimeDetail">-- فحص من --</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-label">آخر زمن استجابة</div>
|
||||
<div class="big-value" id="latencyValue">-- ms</div>
|
||||
<div class="sub-value" id="latencyDetail">HTTP GET /</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-label">شهادة SSL</div>
|
||||
<div class="big-value" id="sslValue">--</div>
|
||||
<div class="sub-value" id="sslDetail">تنتهي في --</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-label">زمن تحليل DNS</div>
|
||||
<div class="big-value" id="dnsValue">-- ms</div>
|
||||
<div class="sub-value" id="dnsDetail">آخر فحص</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-label">زمن استجابة البحث</div>
|
||||
<div class="big-value" id="searchValue">-- ms</div>
|
||||
<div class="sub-value" id="searchDetail">GET /search?q=test</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<section class="chart-section">
|
||||
<h2>زمن الاستجابة — آخر ساعة</h2>
|
||||
<canvas id="latencyChart" height="90"></canvas>
|
||||
</section>
|
||||
|
||||
<section class="log-section">
|
||||
<h2>سجل آخر 10 فحوصات</h2>
|
||||
<table id="logTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>الوقت</th>
|
||||
<th>الحالة</th>
|
||||
<th>Status</th>
|
||||
<th>Latency</th>
|
||||
<th>DNS</th>
|
||||
<th>SSL (أيام متبقية)</th>
|
||||
<th>بحث</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="logTableBody">
|
||||
<tr><td colspan="7">جاري التحميل...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
يتحدث تلقائيًا كل 60 ثانية · مصدر البيانات: <code>data/metrics_history.json</code>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,126 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Segoe UI', Tahoma, Arial, sans-serif;
|
||||
background: #0f1420;
|
||||
color: #e7ecf5;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
margin: 0 0 14px;
|
||||
color: #c7cedb;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
padding: 8px 18px;
|
||||
border-radius: 999px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
background: #2a3448;
|
||||
color: #8b96ab;
|
||||
}
|
||||
|
||||
.status-pill.up {
|
||||
background: #123a26;
|
||||
color: #2ecc71;
|
||||
}
|
||||
|
||||
.status-pill.down {
|
||||
background: #3a1414;
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #171f30;
|
||||
border: 1px solid #2a3448;
|
||||
border-radius: 12px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
color: #8b96ab;
|
||||
font-size: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.big-value {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sub-value {
|
||||
color: #5b6579;
|
||||
font-size: 12px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.chart-section, .log-section {
|
||||
background: #171f30;
|
||||
border: 1px solid #2a3448;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 8px 10px;
|
||||
text-align: right;
|
||||
border-bottom: 1px solid #232d42;
|
||||
}
|
||||
|
||||
th {
|
||||
color: #8b96ab;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
td.status-up {
|
||||
color: #2ecc71;
|
||||
}
|
||||
|
||||
td.status-down {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
color: #5b6579;
|
||||
font-size: 12px;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
footer code {
|
||||
color: #8b96ab;
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Monitoring script for mithal.space
|
||||
Collects every 60 seconds:
|
||||
- Latency: HTTP response time for the homepage
|
||||
- Uptime: whether the site responds with a healthy status code
|
||||
- SSL: certificate validity and days remaining until expiry
|
||||
- DNS: DNS resolution time
|
||||
- Search response: time to get a response from the search endpoint
|
||||
|
||||
Storage:
|
||||
- dashboard/data/metrics_log.csv -> full historical log (append-only)
|
||||
- dashboard/data/metrics_history.json -> rolling window (last 24h) used by the dashboard
|
||||
|
||||
Only standard library is used - no extra dependencies to install.
|
||||
"""
|
||||
|
||||
import csv
|
||||
import json
|
||||
import socket
|
||||
import ssl
|
||||
import time
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Configuration
|
||||
# ---------------------------------------------------------------------------
|
||||
TARGET_HOST = "mithal.space"
|
||||
TARGET_URL = "https://mithal.space/"
|
||||
SEARCH_URL = "https://mithal.space/search?q=test" # adjust if the real search path differs
|
||||
CHECK_INTERVAL_SECONDS = 60
|
||||
REQUEST_TIMEOUT_SECONDS = 10
|
||||
ROLLING_WINDOW_MINUTES = 24 * 60 # keep last 24 hours (1 check/minute)
|
||||
|
||||
DATA_DIR = Path(__file__).parent / "dashboard" / "data"
|
||||
CSV_LOG_PATH = DATA_DIR / "metrics_log.csv"
|
||||
HISTORY_JSON_PATH = DATA_DIR / "metrics_history.json"
|
||||
|
||||
CSV_FIELDS = [
|
||||
"timestamp", "up", "status_code", "latency_ms",
|
||||
"dns_ms", "ssl_valid", "ssl_days_remaining", "ssl_expiry",
|
||||
"search_status", "search_latency_ms", "error",
|
||||
]
|
||||
|
||||
|
||||
def now_iso():
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
def check_dns(host):
|
||||
"""Measure DNS resolution time in milliseconds."""
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
socket.getaddrinfo(host, 443)
|
||||
elapsed_ms = round((time.perf_counter() - start) * 1000, 2)
|
||||
return elapsed_ms, None
|
||||
except socket.gaierror as exc:
|
||||
elapsed_ms = round((time.perf_counter() - start) * 1000, 2)
|
||||
return elapsed_ms, str(exc)
|
||||
|
||||
|
||||
def check_ssl(host, port=443):
|
||||
"""Connect to the host and read certificate expiry info."""
|
||||
context = ssl.create_default_context()
|
||||
try:
|
||||
with socket.create_connection((host, port), timeout=REQUEST_TIMEOUT_SECONDS) as sock:
|
||||
with context.wrap_socket(sock, server_hostname=host) as ssock:
|
||||
cert = ssock.getpeercert()
|
||||
not_after = cert.get("notAfter")
|
||||
expiry_dt = datetime.strptime(not_after, "%b %d %H:%M:%S %Y %Z").replace(tzinfo=timezone.utc)
|
||||
days_remaining = (expiry_dt - datetime.now(timezone.utc)).days
|
||||
return {
|
||||
"ssl_valid": True,
|
||||
"ssl_days_remaining": days_remaining,
|
||||
"ssl_expiry": expiry_dt.isoformat(),
|
||||
}, None
|
||||
except Exception as exc:
|
||||
return {
|
||||
"ssl_valid": False,
|
||||
"ssl_days_remaining": None,
|
||||
"ssl_expiry": None,
|
||||
}, str(exc)
|
||||
|
||||
|
||||
def check_http(url):
|
||||
"""Measure HTTP response time and status code."""
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "mithal-monitor/1.0"})
|
||||
with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT_SECONDS) as resp:
|
||||
elapsed_ms = round((time.perf_counter() - start) * 1000, 2)
|
||||
return resp.getcode(), elapsed_ms, None
|
||||
except urllib.error.HTTPError as exc:
|
||||
elapsed_ms = round((time.perf_counter() - start) * 1000, 2)
|
||||
return exc.code, elapsed_ms, None
|
||||
except Exception as exc:
|
||||
elapsed_ms = round((time.perf_counter() - start) * 1000, 2)
|
||||
return None, elapsed_ms, str(exc)
|
||||
|
||||
|
||||
def run_check():
|
||||
record = {"timestamp": now_iso(), "error": None}
|
||||
|
||||
# 1) DNS resolution time
|
||||
dns_ms, dns_err = check_dns(TARGET_HOST)
|
||||
record["dns_ms"] = dns_ms
|
||||
|
||||
# 2) SSL certificate status
|
||||
ssl_info, ssl_err = check_ssl(TARGET_HOST)
|
||||
record.update(ssl_info)
|
||||
|
||||
# 3) HTTP latency + uptime (status code)
|
||||
status_code, latency_ms, http_err = check_http(TARGET_URL)
|
||||
record["status_code"] = status_code
|
||||
record["latency_ms"] = latency_ms
|
||||
record["up"] = status_code is not None and 200 <= status_code < 400
|
||||
|
||||
# 4) Search response time
|
||||
search_status, search_latency_ms, search_err = check_http(SEARCH_URL)
|
||||
record["search_status"] = search_status
|
||||
record["search_latency_ms"] = search_latency_ms
|
||||
|
||||
errors = [e for e in (dns_err, ssl_err, http_err, search_err) if e]
|
||||
if errors:
|
||||
record["error"] = " | ".join(errors)
|
||||
|
||||
return record
|
||||
|
||||
|
||||
def append_csv(record):
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
file_exists = CSV_LOG_PATH.exists()
|
||||
with CSV_LOG_PATH.open("a", newline="", encoding="utf-8") as f:
|
||||
writer = csv.DictWriter(f, fieldnames=CSV_FIELDS)
|
||||
if not file_exists:
|
||||
writer.writeheader()
|
||||
writer.writerow(record)
|
||||
|
||||
|
||||
def update_history_json(record):
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
history = []
|
||||
if HISTORY_JSON_PATH.exists():
|
||||
try:
|
||||
history = json.loads(HISTORY_JSON_PATH.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
history = []
|
||||
history.append(record)
|
||||
history = history[-ROLLING_WINDOW_MINUTES:]
|
||||
HISTORY_JSON_PATH.write_text(json.dumps(history, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
|
||||
|
||||
def main():
|
||||
print(f"[{now_iso()}] Starting monitor for {TARGET_URL} (every {CHECK_INTERVAL_SECONDS}s)")
|
||||
while True:
|
||||
record = run_check()
|
||||
append_csv(record)
|
||||
update_history_json(record)
|
||||
|
||||
status_label = "UP" if record["up"] else "DOWN"
|
||||
print(
|
||||
f"[{record['timestamp']}] {status_label} "
|
||||
f"status={record['status_code']} latency={record['latency_ms']}ms "
|
||||
f"dns={record['dns_ms']}ms ssl_days={record['ssl_days_remaining']} "
|
||||
f"search={record['search_latency_ms']}ms"
|
||||
)
|
||||
if record["error"]:
|
||||
print(f" -> issues: {record['error']}")
|
||||
|
||||
time.sleep(CHECK_INTERVAL_SECONDS)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/bin/sh
|
||||
# Runs the monitor loop in the background and serves the dashboard folder.
|
||||
|
||||
python3 /app/monitor.py &
|
||||
|
||||
cd /app/dashboard && python3 -m http.server 8080
|
||||
Reference in New Issue
Block a user