feat: initialize repository with SRE infrastructure, monitoring dashboard, and deployment documentation
هذا الالتزام موجود في:
32
q1-deploy-monitor/Dockerfile
Normal file
32
q1-deploy-monitor/Dockerfile
Normal file
@@ -0,0 +1,32 @@
|
||||
# Stage 1: Build binary using lightweight Go Alpine image
|
||||
FROM golang:1.24-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Cache dependencies
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build lightweight, statically linked binary stripped of debug information (-s -w)
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o server main.go
|
||||
|
||||
# Stage 2: Minimal runtime image using scratch (~10-15MB final image size)
|
||||
FROM scratch
|
||||
|
||||
# Copy CA certificates for HTTPS requests if needed
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
|
||||
# Copy static binary from builder stage
|
||||
COPY --from=builder /app/server /server
|
||||
|
||||
# Run as non-root user (nobody) for enhanced security
|
||||
USER 65534:65534
|
||||
|
||||
# Expose port 8080
|
||||
EXPOSE 8080
|
||||
|
||||
# Launch server
|
||||
ENTRYPOINT ["/server"]
|
||||
411
q1-deploy-monitor/dashboard.html
Normal file
411
q1-deploy-monitor/dashboard.html
Normal file
@@ -0,0 +1,411 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ar" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ghaymah SRE - لوحة المراقبة</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&family=Tajawal:wght@400;500;700;800&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-dark: #0b0f19;
|
||||
--card-bg: rgba(18, 26, 44, 0.75);
|
||||
--card-border: rgba(255, 255, 255, 0.08);
|
||||
--primary: #3b82f6;
|
||||
--primary-glow: rgba(59, 130, 246, 0.4);
|
||||
--success: #10b981;
|
||||
--success-glow: rgba(16, 185, 129, 0.4);
|
||||
--danger: #ef4444;
|
||||
--danger-glow: rgba(239, 68, 68, 0.4);
|
||||
--text-main: #f3f4f6;
|
||||
--text-muted: #9ca3af;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Tajawal', 'Outfit', sans-serif;
|
||||
background-color: var(--bg-dark);
|
||||
background-image:
|
||||
radial-gradient(circle at 15% 20%, rgba(59, 130, 246, 0.12) 0%, transparent 40%),
|
||||
radial-gradient(circle at 85% 80%, rgba(16, 185, 129, 0.08) 0%, transparent 40%);
|
||||
color: var(--text-main);
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 1100px;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2.5rem;
|
||||
padding-bottom: 1.5rem;
|
||||
border-bottom: 1px solid var(--card-border);
|
||||
}
|
||||
|
||||
.title-group h1 {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #9ca3af 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.title-group p {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.95rem;
|
||||
margin-top: 0.3rem;
|
||||
}
|
||||
|
||||
.live-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||
padding: 0.4rem 0.9rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--success);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pulse-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: var(--success);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 10px var(--success);
|
||||
animation: pulse 1.8s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); }
|
||||
70% { transform: scale(1); box-shadow: 0 0 0 10px rgba(16, 185, 129, 0); }
|
||||
100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
|
||||
}
|
||||
|
||||
/* Grid Layout */
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: 1.25rem;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
||||
transition: transform 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-4px);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 0.6rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 2.2rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
font-family: 'Outfit', sans-serif;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.card-subtext {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
margin-top: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
/* Status Badge */
|
||||
.status-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.35rem 0.9rem;
|
||||
border-radius: 0.6rem;
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.status-healthy {
|
||||
background: rgba(16, 185, 129, 0.15);
|
||||
color: var(--success);
|
||||
border: 1px solid rgba(16, 185, 129, 0.4);
|
||||
box-shadow: 0 0 15px var(--success-glow);
|
||||
}
|
||||
|
||||
.status-down {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: var(--danger);
|
||||
border: 1px solid rgba(239, 68, 68, 0.4);
|
||||
box-shadow: 0 0 15px var(--danger-glow);
|
||||
}
|
||||
|
||||
/* Activity Table */
|
||||
.logs-section {
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: 1.25rem;
|
||||
padding: 1.5rem;
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.logs-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.logs-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
text-align: right;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.logs-table th {
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--card-border);
|
||||
}
|
||||
|
||||
.logs-table td {
|
||||
padding: 0.85rem 1rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
||||
color: var(--text-main);
|
||||
font-family: 'Outfit', 'Tajawal', sans-serif;
|
||||
}
|
||||
|
||||
.badge-pill {
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 0.4rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pill-200 { background: rgba(16, 185, 129, 0.2); color: var(--success); }
|
||||
.pill-err { background: rgba(239, 68, 68, 0.2); color: var(--danger); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- Header -->
|
||||
<header>
|
||||
<div class="title-group">
|
||||
<h1>🌩️ Ghaymah SRE Monitor</h1>
|
||||
<p>مراقبة أداء واستجابة التطبيق المباشرة (Real-time Health Dashboard)</p>
|
||||
</div>
|
||||
<div class="live-badge">
|
||||
<span class="pulse-dot"></span>
|
||||
<span id="lastUpdatedText">مباشر (تحديث كل 3 ثوانٍ)</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Metrics Cards -->
|
||||
<div class="metrics-grid">
|
||||
<!-- 1. Status Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span>حالة النظام (Status)</span>
|
||||
<div class="card-icon">⚡</div>
|
||||
</div>
|
||||
<div id="statusBadgeContainer">
|
||||
<span id="statusTag" class="status-tag status-healthy">● متصل (Healthy)</span>
|
||||
</div>
|
||||
<div class="card-subtext" id="httpCodeSubtext">كود الاستجابة: HTTP 200 OK</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. Latency Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span>زمن الاستجابة (Latency)</span>
|
||||
<div class="card-icon">⏱️</div>
|
||||
</div>
|
||||
<div class="card-value"><span id="latencyValue">--</span> <small style="font-size: 1rem; font-weight: 400; color: var(--text-muted);">ms</small></div>
|
||||
<div class="card-subtext">
|
||||
<span>متوسط الاستجابة:</span>
|
||||
<strong id="avgLatencyValue" style="color: var(--primary);">-- ms</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. Total Requests Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span>إجمالي الطلبات (Total Requests)</span>
|
||||
<div class="card-icon">📊</div>
|
||||
</div>
|
||||
<div class="card-value" id="totalRequestsValue">--</div>
|
||||
<div class="card-subtext">
|
||||
<span>مدة التشغيل (Uptime):</span>
|
||||
<strong id="uptimeValue" style="color: #ffffff;">-- ثانية</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Check Logs -->
|
||||
<div class="logs-section">
|
||||
<div class="logs-header">
|
||||
<h3>سجل الفحوصات الأخيرة (Health Check Logs)</h3>
|
||||
<span style="font-size: 0.85rem; color: var(--text-muted);" id="apiTargetUrl">الهدف: http://localhost:8080/metrics</span>
|
||||
</div>
|
||||
<table class="logs-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>الوقت (UTC)</th>
|
||||
<th>الحالة</th>
|
||||
<th>كود HTTP</th>
|
||||
<th>زمن الاستجابة</th>
|
||||
<th>إجمالي الطلبات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="logsTableBody">
|
||||
<tr>
|
||||
<td colspan="5" style="text-align: center; color: var(--text-muted); padding: 1.5rem;">جاري جلب البيانات...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_METRICS_URL = 'http://localhost:8080/metrics';
|
||||
const FALLBACK_STATUS_URL = 'status.json';
|
||||
const logsHistory = [];
|
||||
|
||||
async function fetchMetrics() {
|
||||
let data = null;
|
||||
let source = 'API';
|
||||
|
||||
try {
|
||||
// Try fetching direct API endpoint
|
||||
const response = await fetch(API_METRICS_URL, { cache: 'no-store' });
|
||||
if (response.ok) {
|
||||
data = await response.json();
|
||||
}
|
||||
} catch (err) {
|
||||
// Fallback to status.json if direct API fails or CORS blocked
|
||||
try {
|
||||
const statusRes = await fetch(FALLBACK_STATUS_URL, { cache: 'no-store' });
|
||||
if (statusRes.ok) {
|
||||
data = await statusRes.json();
|
||||
source = 'status.json';
|
||||
}
|
||||
} catch (e) {
|
||||
data = null;
|
||||
}
|
||||
}
|
||||
|
||||
updateDashboardUI(data);
|
||||
}
|
||||
|
||||
function updateDashboardUI(data) {
|
||||
const statusTag = document.getElementById('statusTag');
|
||||
const httpCodeSubtext = document.getElementById('httpCodeSubtext');
|
||||
const latencyValue = document.getElementById('latencyValue');
|
||||
const avgLatencyValue = document.getElementById('avgLatencyValue');
|
||||
const totalRequestsValue = document.getElementById('totalRequestsValue');
|
||||
const uptimeValue = document.getElementById('uptimeValue');
|
||||
|
||||
const nowIso = new Date().toLocaleTimeString('ar-SA');
|
||||
|
||||
if (data && (data.status === 'healthy' || data.status === 'ok')) {
|
||||
statusTag.className = 'status-tag status-healthy';
|
||||
statusTag.innerHTML = '● متصل (Healthy)';
|
||||
httpCodeSubtext.textContent = 'كود الاستجابة: HTTP 200 OK';
|
||||
|
||||
const latency = data.last_request_latency_ms || data.check_latency_ms || 0;
|
||||
latencyValue.textContent = latency;
|
||||
avgLatencyValue.textContent = (data.average_latency_ms || 0) + ' ms';
|
||||
totalRequestsValue.textContent = (data.total_requests || 0).toLocaleString();
|
||||
uptimeValue.textContent = (data.uptime_seconds || 0) + ' ثانية';
|
||||
|
||||
addLogEntry(nowIso, 'Healthy', 200, latency + ' ms', data.total_requests || 0);
|
||||
} else {
|
||||
statusTag.className = 'status-tag status-down';
|
||||
statusTag.innerHTML = '✖ غير متصل (Down)';
|
||||
httpCodeSubtext.textContent = 'كود الاستجابة: HTTP 500 / Connection Error';
|
||||
|
||||
latencyValue.textContent = '--';
|
||||
avgLatencyValue.textContent = '-- ms';
|
||||
totalRequestsValue.textContent = '--';
|
||||
uptimeValue.textContent = '0 ثانية';
|
||||
|
||||
addLogEntry(nowIso, 'Down', 500, '--', '--');
|
||||
}
|
||||
}
|
||||
|
||||
function addLogEntry(time, status, code, latency, reqCount) {
|
||||
logsHistory.unshift({ time, status, code, latency, reqCount });
|
||||
if (logsHistory.length > 7) logsHistory.pop();
|
||||
|
||||
const tbody = document.getElementById('logsTableBody');
|
||||
tbody.innerHTML = logsHistory.map(entry => `
|
||||
<tr>
|
||||
<td>${entry.time}</td>
|
||||
<td>
|
||||
<span class="badge-pill ${entry.status === 'Healthy' ? 'pill-200' : 'pill-err'}">
|
||||
${entry.status}
|
||||
</span>
|
||||
</td>
|
||||
<td>HTTP ${entry.code}</td>
|
||||
<td>${entry.latency}</td>
|
||||
<td>${entry.reqCount}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Initial fetch & set interval (3 seconds)
|
||||
fetchMetrics();
|
||||
setInterval(fetchMetrics, 3000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
68
q1-deploy-monitor/health-check.sh
Executable file
68
q1-deploy-monitor/health-check.sh
Executable file
@@ -0,0 +1,68 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Configuration
|
||||
API_URL="${API_URL:-https://ghaymah-task-da46b224b9fc.hosted.ghaymah.systems}"
|
||||
CHECK_INTERVAL=30
|
||||
STATUS_FILE="status.json"
|
||||
|
||||
echo "=================================================="
|
||||
echo " Starting Ghaymah SRE Monitoring Agent "
|
||||
echo " Target URL : $API_URL "
|
||||
echo " Interval : ${CHECK_INTERVAL}s "
|
||||
echo "=================================================="
|
||||
|
||||
while true; do
|
||||
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
START_TIME=$(date +%s%N)
|
||||
HTTP_RESPONSE=$(curl -s -w "\n%{http_code}" "${API_URL}/metrics")
|
||||
END_TIME=$(date +%s%N)
|
||||
|
||||
# Calculate latency in milliseconds
|
||||
LATENCY_MS=$(( (END_TIME - START_TIME) / 1000000 ))
|
||||
|
||||
HTTP_BODY=$(echo "$HTTP_RESPONSE" | sed '$d')
|
||||
HTTP_CODE=$(echo "$HTTP_RESPONSE" | tail -n1)
|
||||
|
||||
if [ "$HTTP_CODE" -eq 200 ]; then
|
||||
STATUS="healthy"
|
||||
UPTIME=$(echo "$HTTP_BODY" | grep -o '"uptime_seconds":[0-9]*' | cut -d':' -f2)
|
||||
TOTAL_REQ=$(echo "$HTTP_BODY" | grep -o '"total_requests":[0-9]*' | cut -d':' -f2)
|
||||
AVG_LATENCY=$(echo "$HTTP_BODY" | grep -o '"average_latency_ms":[0-9.]*' | cut -d':' -f2)
|
||||
LAST_LATENCY=$(echo "$HTTP_BODY" | grep -o '"last_request_latency_ms":[0-9.]*' | cut -d':' -f2)
|
||||
|
||||
echo "[ $TIMESTAMP ] [OK 200] Status: $STATUS | Latency: ${LATENCY_MS}ms | Total Requests: ${TOTAL_REQ:-0} | Avg Latency: ${AVG_LATENCY:-0}ms"
|
||||
|
||||
# Export JSON metrics status file for dashboard consumption
|
||||
cat <<EOF > "$STATUS_FILE"
|
||||
{
|
||||
"status": "$STATUS",
|
||||
"http_code": $HTTP_CODE,
|
||||
"check_timestamp": "$TIMESTAMP",
|
||||
"check_latency_ms": $LATENCY_MS,
|
||||
"uptime_seconds": ${UPTIME:-0},
|
||||
"total_requests": ${TOTAL_REQ:-0},
|
||||
"average_latency_ms": ${AVG_LATENCY:-0},
|
||||
"last_request_latency_ms": ${LAST_LATENCY:-$LATENCY_MS}
|
||||
}
|
||||
EOF
|
||||
else
|
||||
STATUS="unhealthy"
|
||||
echo "[ $TIMESTAMP ] [ERROR ${HTTP_CODE:-000}] Target $API_URL is DOWN or unreachable! Latency: ${LATENCY_MS}ms"
|
||||
|
||||
cat <<EOF > "$STATUS_FILE"
|
||||
{
|
||||
"status": "$STATUS",
|
||||
"http_code": ${HTTP_CODE:-0},
|
||||
"check_timestamp": "$TIMESTAMP",
|
||||
"check_latency_ms": $LATENCY_MS,
|
||||
"uptime_seconds": 0,
|
||||
"total_requests": 0,
|
||||
"average_latency_ms": 0,
|
||||
"last_request_latency_ms": 0
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
sleep $CHECK_INTERVAL
|
||||
done
|
||||
10
q1-deploy-monitor/status.json
Normal file
10
q1-deploy-monitor/status.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"status": "healthy",
|
||||
"http_code": 200,
|
||||
"check_timestamp": "2026-07-28T10:09:20Z",
|
||||
"check_latency_ms": 67,
|
||||
"uptime_seconds": 5973,
|
||||
"total_requests": 203,
|
||||
"average_latency_ms": 1.76,
|
||||
"last_request_latency_ms": 0.27
|
||||
}
|
||||
المرجع في مشكلة جديدة
حظر مستخدم