Add CI/CD pipeline, architecture documentation, and monitoring application
- 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
Q5/index.html
Normal file
395
Q5/index.html
Normal file
@@ -0,0 +1,395 @@
|
||||
<!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>
|
||||
المرجع في مشكلة جديدة
حظر مستخدم