- Created a GitHub Actions workflow for CI/CD to deploy to Ghyamah, including testing, building, and pushing Docker images. - Added architecture design document for handling 15,000 requests per second, detailing system components, capacity planning, and cold start strategies. - Introduced a Python-based uptime/latency/SSL monitor with a static dashboard, utilizing standard libraries only. - Included Dockerfile and entrypoint script for the monitoring application, ensuring it runs as a non-root user and handles process management. - Added a .dockerignore file to exclude unnecessary files from the Docker build context. - Created an HTML dashboard for visualizing monitoring metrics, including uptime, latency, and SSL certificate status.
395 أسطر
14 KiB
HTML
395 أسطر
14 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Site Watch — Status</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.4/chart.umd.min.js"></script>
|
|
<style>
|
|
:root{
|
|
--bg:#0b1220;
|
|
--surface:#121b2e;
|
|
--surface-alt:#182541;
|
|
--border:#23324f;
|
|
--text:#e7ecf5;
|
|
--muted:#8a97b3;
|
|
--accent:#5eead4;
|
|
--ok:#34d399;
|
|
--warn:#fbbf24;
|
|
--down:#f87171;
|
|
--mono:'IBM Plex Mono', ui-monospace, monospace;
|
|
--sans:'Inter', system-ui, sans-serif;
|
|
}
|
|
*{box-sizing:border-box;}
|
|
html,body{margin:0;padding:0;background:var(--bg);color:var(--text);font-family:var(--sans);}
|
|
body{min-height:100vh;padding:32px 24px 64px;}
|
|
a{color:var(--accent);}
|
|
|
|
.wrap{max-width:1080px;margin:0 auto;}
|
|
|
|
/* ---- Header / pulse signature ---- */
|
|
header{
|
|
display:flex;
|
|
flex-direction:column;
|
|
gap:18px;
|
|
border:1px solid var(--border);
|
|
background:linear-gradient(180deg,var(--surface) 0%, var(--surface-alt) 100%);
|
|
border-radius:14px;
|
|
padding:22px 26px;
|
|
position:relative;
|
|
overflow:hidden;
|
|
}
|
|
.header-top{display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:14px;}
|
|
.brand{display:flex;flex-direction:column;gap:4px;}
|
|
.eyebrow{font-family:var(--mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted);}
|
|
.target{font-family:var(--mono);font-size:22px;font-weight:600;color:var(--text);word-break:break-all;}
|
|
.status-pill{
|
|
display:inline-flex;align-items:center;gap:8px;
|
|
font-family:var(--mono);font-size:12px;font-weight:600;letter-spacing:.04em;
|
|
padding:7px 14px;border-radius:999px;border:1px solid var(--border);
|
|
background:rgba(255,255,255,0.02);white-space:nowrap;height:fit-content;
|
|
}
|
|
.dot{width:8px;height:8px;border-radius:50%;background:var(--muted);box-shadow:0 0 0 0 rgba(0,0,0,0);}
|
|
.dot.up{background:var(--ok);animation:pulse-dot 2s infinite;}
|
|
.dot.down{background:var(--down);animation:pulse-dot 1s infinite;}
|
|
|
|
@keyframes pulse-dot{
|
|
0%{box-shadow:0 0 0 0 rgba(52,211,153,.55);}
|
|
70%{box-shadow:0 0 0 8px rgba(52,211,153,0);}
|
|
100%{box-shadow:0 0 0 0 rgba(52,211,153,0);}
|
|
}
|
|
|
|
.pulse-line{width:100%;height:44px;opacity:.85;}
|
|
.pulse-line path{
|
|
fill:none;stroke:var(--accent);stroke-width:1.6;
|
|
stroke-dasharray:1200;stroke-dashoffset:1200;
|
|
animation:draw-pulse 3.2s linear infinite;
|
|
}
|
|
@keyframes draw-pulse{
|
|
0%{stroke-dashoffset:1200;}
|
|
100%{stroke-dashoffset:0;}
|
|
}
|
|
@media (prefers-reduced-motion: reduce){
|
|
.dot.up,.dot.down,.pulse-line path{animation:none;}
|
|
}
|
|
|
|
.meta-row{display:flex;gap:18px;flex-wrap:wrap;font-family:var(--mono);font-size:12px;color:var(--muted);}
|
|
.meta-row span b{color:var(--text);font-weight:600;}
|
|
|
|
/* ---- Stat cards ---- */
|
|
.stats{
|
|
display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-top:22px;
|
|
}
|
|
@media (max-width:820px){.stats{grid-template-columns:repeat(2,1fr);}}
|
|
.stat-card{
|
|
background:var(--surface);border:1px solid var(--border);border-radius:12px;
|
|
padding:16px 18px;display:flex;flex-direction:column;gap:6px;
|
|
}
|
|
.stat-label{font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:.1em;color:var(--muted);}
|
|
.stat-value{font-family:var(--mono);font-size:28px;font-weight:700;line-height:1.1;}
|
|
.stat-sub{font-size:12px;color:var(--muted);}
|
|
.stat-value.ok{color:var(--ok);}
|
|
.stat-value.warn{color:var(--warn);}
|
|
.stat-value.down{color:var(--down);}
|
|
|
|
/* ---- Chart panel ---- */
|
|
.panel{
|
|
margin-top:22px;background:var(--surface);border:1px solid var(--border);
|
|
border-radius:12px;padding:20px 22px;
|
|
}
|
|
.panel-title{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:14px;flex-wrap:wrap;gap:8px;}
|
|
.panel-title h2{font-size:14px;margin:0;font-family:var(--mono);text-transform:uppercase;letter-spacing:.08em;color:var(--text);}
|
|
.panel-title .hint{font-size:12px;color:var(--muted);}
|
|
.chart-holder{height:280px;position:relative;}
|
|
|
|
/* ---- Table ---- */
|
|
table{width:100%;border-collapse:collapse;font-family:var(--mono);font-size:12.5px;}
|
|
thead th{
|
|
text-align:left;color:var(--muted);font-weight:600;text-transform:uppercase;
|
|
font-size:10.5px;letter-spacing:.08em;padding:8px 10px;border-bottom:1px solid var(--border);
|
|
}
|
|
tbody td{padding:9px 10px;border-bottom:1px solid rgba(255,255,255,0.04);color:var(--text);}
|
|
tbody tr:hover{background:rgba(255,255,255,0.02);}
|
|
.badge{
|
|
display:inline-block;padding:2px 8px;border-radius:6px;font-size:11px;font-weight:600;
|
|
}
|
|
.badge.ok{background:rgba(52,211,153,.12);color:var(--ok);}
|
|
.badge.down{background:rgba(248,113,113,.12);color:var(--down);}
|
|
|
|
footer{margin-top:26px;text-align:center;font-family:var(--mono);font-size:11px;color:var(--muted);}
|
|
.empty{color:var(--muted);font-size:13px;padding:20px 0;text-align:center;}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="wrap">
|
|
|
|
<header>
|
|
<div class="header-top">
|
|
<div class="brand">
|
|
<span class="eyebrow">Uptime & Performance</span>
|
|
<span class="target" id="targetUrl">—</span>
|
|
</div>
|
|
<div class="status-pill">
|
|
<span class="dot" id="statusDot"></span>
|
|
<span id="statusText">CHECKING…</span>
|
|
</div>
|
|
</div>
|
|
|
|
<svg class="pulse-line" viewBox="0 0 600 44" preserveAspectRatio="none">
|
|
<path d="M0,22 L120,22 L140,6 L160,38 L180,22 L260,22 L280,10 L300,34 L320,22 L600,22"/>
|
|
</svg>
|
|
|
|
<div class="meta-row">
|
|
<span>Checks every <b id="metaInterval">—</b></span>
|
|
<span>Window <b id="metaRetention">—</b></span>
|
|
<span>Last check <b id="metaUpdated">—</b></span>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="stats">
|
|
<div class="stat-card">
|
|
<span class="stat-label">Uptime · 24h</span>
|
|
<span class="stat-value" id="statUptime">—</span>
|
|
<span class="stat-sub" id="statUptimeSub">— checks recorded</span>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-label">Latency · latest</span>
|
|
<span class="stat-value" id="statLatency">—</span>
|
|
<span class="stat-sub" id="statLatencySub">HTTP response time</span>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-label">DNS resolve</span>
|
|
<span class="stat-value" id="statDns">—</span>
|
|
<span class="stat-sub">Latest lookup time</span>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-label">SSL expires in</span>
|
|
<span class="stat-value" id="statSsl">—</span>
|
|
<span class="stat-sub" id="statSslSub">Certificate validity</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<div class="panel-title">
|
|
<h2>Latency — last 60 minutes</h2>
|
|
<span class="hint" id="chartHint">site vs. search endpoint, ms</span>
|
|
</div>
|
|
<div class="chart-holder"><canvas id="latencyChart"></canvas></div>
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<div class="panel-title">
|
|
<h2>Recent checks</h2>
|
|
<span class="hint">last 10</span>
|
|
</div>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Time (UTC)</th>
|
|
<th>Status</th>
|
|
<th>Latency</th>
|
|
<th>DNS</th>
|
|
<th>Search</th>
|
|
<th>SSL days</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="checksBody">
|
|
<tr><td colspan="6" class="empty">Waiting for first data…</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<footer>Reads <code>data/metrics.json</code> · refreshes every 30s</footer>
|
|
</div>
|
|
|
|
<script>
|
|
const DATA_URL = 'data/metrics.json';
|
|
const REFRESH_MS = 30000;
|
|
let chart;
|
|
|
|
function fmtMs(v){ return (v === null || v === undefined) ? '—' : Math.round(v) + ' ms'; }
|
|
function fmtDays(v){ return (v === null || v === undefined) ? '—' : v + 'd'; }
|
|
function fmtTime(iso){
|
|
try{
|
|
const d = new Date(iso);
|
|
return d.toISOString().substr(11,8);
|
|
}catch(e){ return iso; }
|
|
}
|
|
|
|
function classifySsl(days){
|
|
if(days === null || days === undefined) return '';
|
|
if(days < 7) return 'down';
|
|
if(days < 21) return 'warn';
|
|
return 'ok';
|
|
}
|
|
|
|
function renderStats(payload){
|
|
const checks = payload.checks || [];
|
|
document.getElementById('targetUrl').textContent = payload.target || '—';
|
|
document.getElementById('metaInterval').textContent = (payload.check_interval_seconds || '—') + 's';
|
|
document.getElementById('metaRetention').textContent = (payload.retention_hours || '—') + 'h';
|
|
document.getElementById('metaUpdated').textContent = payload.updated_at ? fmtTime(payload.updated_at) + ' UTC' : '—';
|
|
|
|
if(checks.length === 0){
|
|
document.getElementById('statusText').textContent = 'NO DATA';
|
|
return;
|
|
}
|
|
|
|
const latest = checks[checks.length - 1];
|
|
const successCount = checks.filter(c => c.success).length;
|
|
const uptimePct = (successCount / checks.length * 100);
|
|
|
|
// Status pill
|
|
const dot = document.getElementById('statusDot');
|
|
const statusText = document.getElementById('statusText');
|
|
if(latest.success){
|
|
dot.className = 'dot up';
|
|
statusText.textContent = `UP · HTTP ${latest.http_status}`;
|
|
} else {
|
|
dot.className = 'dot down';
|
|
statusText.textContent = latest.http_status ? `DEGRADED · HTTP ${latest.http_status}` : 'DOWN';
|
|
}
|
|
|
|
// Uptime stat
|
|
const uptimeEl = document.getElementById('statUptime');
|
|
uptimeEl.textContent = uptimePct.toFixed(2) + '%';
|
|
uptimeEl.className = 'stat-value ' + (uptimePct >= 99.5 ? 'ok' : uptimePct >= 97 ? 'warn' : 'down');
|
|
document.getElementById('statUptimeSub').textContent = `${successCount}/${checks.length} checks succeeded`;
|
|
|
|
// Latency stat
|
|
const latEl = document.getElementById('statLatency');
|
|
latEl.textContent = fmtMs(latest.latency_ms);
|
|
latEl.className = 'stat-value ' + (latest.latency_ms == null ? 'down' : latest.latency_ms < 500 ? 'ok' : latest.latency_ms < 1500 ? 'warn' : 'down');
|
|
|
|
// DNS stat
|
|
document.getElementById('statDns').textContent = fmtMs(latest.dns_ms);
|
|
|
|
// SSL stat
|
|
const sslEl = document.getElementById('statSsl');
|
|
sslEl.textContent = fmtDays(latest.ssl_days_remaining);
|
|
sslEl.className = 'stat-value ' + classifySsl(latest.ssl_days_remaining);
|
|
document.getElementById('statSslSub').textContent = latest.ssl_days_remaining != null
|
|
? 'Certificate validity' : 'Not monitored over HTTPS';
|
|
}
|
|
|
|
function renderChart(payload){
|
|
const checks = payload.checks || [];
|
|
const cutoff = Date.now() - 60 * 60 * 1000;
|
|
const recent = checks.filter(c => new Date(c.timestamp).getTime() >= cutoff);
|
|
const source = recent.length > 0 ? recent : checks.slice(-60);
|
|
|
|
const labels = source.map(c => fmtTime(c.timestamp));
|
|
const siteData = source.map(c => c.latency_ms);
|
|
const searchData = source.map(c => c.search_latency_ms);
|
|
const hasSearch = searchData.some(v => v !== null && v !== undefined);
|
|
|
|
document.getElementById('chartHint').textContent = hasSearch
|
|
? 'site vs. search endpoint, ms' : 'site response time, ms';
|
|
|
|
const datasets = [{
|
|
label: 'Site',
|
|
data: siteData,
|
|
borderColor: '#5eead4',
|
|
backgroundColor: 'rgba(94,234,212,0.12)',
|
|
borderWidth: 2,
|
|
pointRadius: 0,
|
|
tension: 0.25,
|
|
fill: true,
|
|
}];
|
|
if(hasSearch){
|
|
datasets.push({
|
|
label: 'Search endpoint',
|
|
data: searchData,
|
|
borderColor: '#fbbf24',
|
|
backgroundColor: 'transparent',
|
|
borderWidth: 2,
|
|
borderDash: [4,3],
|
|
pointRadius: 0,
|
|
tension: 0.25,
|
|
});
|
|
}
|
|
|
|
const cfg = {
|
|
type: 'line',
|
|
data: { labels, datasets },
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
interaction: { mode: 'index', intersect: false },
|
|
scales: {
|
|
x: {
|
|
ticks: { color: '#8a97b3', maxTicksLimit: 8, font: { family: 'IBM Plex Mono', size: 10 } },
|
|
grid: { color: 'rgba(255,255,255,0.04)' },
|
|
},
|
|
y: {
|
|
beginAtZero: true,
|
|
ticks: { color: '#8a97b3', font: { family: 'IBM Plex Mono', size: 10 } },
|
|
grid: { color: 'rgba(255,255,255,0.06)' },
|
|
title: { display: true, text: 'ms', color: '#8a97b3', font: { size: 11 } },
|
|
},
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: hasSearch,
|
|
labels: { color: '#e7ecf5', font: { family: 'IBM Plex Mono', size: 11 }, boxWidth: 12 },
|
|
},
|
|
tooltip: {
|
|
backgroundColor: '#182541',
|
|
borderColor: '#23324f',
|
|
borderWidth: 1,
|
|
titleFont: { family: 'IBM Plex Mono' },
|
|
bodyFont: { family: 'IBM Plex Mono' },
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
if(chart){
|
|
chart.data = cfg.data;
|
|
chart.update('none');
|
|
} else {
|
|
chart = new Chart(document.getElementById('latencyChart'), cfg);
|
|
}
|
|
}
|
|
|
|
function renderTable(payload){
|
|
const checks = (payload.checks || []).slice(-10).reverse();
|
|
const body = document.getElementById('checksBody');
|
|
if(checks.length === 0){
|
|
body.innerHTML = '<tr><td colspan="6" class="empty">Waiting for first data…</td></tr>';
|
|
return;
|
|
}
|
|
body.innerHTML = checks.map(c => `
|
|
<tr>
|
|
<td>${fmtTime(c.timestamp)}</td>
|
|
<td><span class="badge ${c.success ? 'ok' : 'down'}">${c.http_status ?? 'ERR'}</span></td>
|
|
<td>${fmtMs(c.latency_ms)}</td>
|
|
<td>${fmtMs(c.dns_ms)}</td>
|
|
<td>${fmtMs(c.search_latency_ms)}</td>
|
|
<td>${fmtDays(c.ssl_days_remaining)}</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
async function refresh(){
|
|
try{
|
|
const res = await fetch(DATA_URL + '?t=' + Date.now(), { cache: 'no-store' });
|
|
if(!res.ok) throw new Error('HTTP ' + res.status);
|
|
const payload = await res.json();
|
|
renderStats(payload);
|
|
renderChart(payload);
|
|
renderTable(payload);
|
|
}catch(err){
|
|
document.getElementById('statusText').textContent = 'DASHBOARD ERROR';
|
|
document.getElementById('statusDot').className = 'dot down';
|
|
console.error('Failed to load metrics:', err);
|
|
}
|
|
}
|
|
|
|
refresh();
|
|
setInterval(refresh, REFRESH_MS);
|
|
</script>
|
|
</body>
|
|
</html> |