الملفات
Ghaymah-Exam-GamalMohamed-SRE/Q5/index.html
Gamal0909 439a31ba93 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.
2026-07-27 23:08:39 +03:00

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 &amp; 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>