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.
هذا الالتزام موجود في:
6
Q1/.dockerignore
Normal file
6
Q1/.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
data/
|
||||||
|
*.pyc
|
||||||
|
__pycache__/
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
README.md
|
||||||
13
Q1/Dockerfile
Normal file
13
Q1/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
FROM node:20-alpine3.16
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --chown=node:node server.js .
|
||||||
|
|
||||||
|
RUN npm init -y && npm install express cors
|
||||||
|
|
||||||
|
USER node
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["node" , "server.js"]
|
||||||
60
Q1/index.html
Normal file
60
Q1/index.html
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ar" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>لوحة مراقبة الـ API</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f4f7f6; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; }
|
||||||
|
.dashboard { background: white; padding: 30px; border-radius: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); text-align: center; width: 400px; }
|
||||||
|
h2 { color: #333; margin-bottom: 20px; }
|
||||||
|
.metric { background: #eef2f5; margin: 10px 0; padding: 15px; border-radius: 8px; font-size: 1.2em; display: flex; justify-content: space-between; }
|
||||||
|
.metric span { font-weight: bold; color: #0056b3; }
|
||||||
|
.status-dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; background: gray; margin-left: 8px; }
|
||||||
|
.online { background: #28a745; }
|
||||||
|
.offline { background: #dc3545; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="dashboard">
|
||||||
|
<h2>مراقبة النظام <span id="statusDot" class="status-dot"></span></h2>
|
||||||
|
|
||||||
|
<div class="metric">
|
||||||
|
الحالة: <span id="statusText">جاري التحميل...</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric">
|
||||||
|
زمن الاستجابة: <span id="responseTime">0 ms</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric">
|
||||||
|
عدد الطلبات الإجمالي: <span id="requestCount">0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const API_URL = "";
|
||||||
|
|
||||||
|
async function fetchMetrics() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(API_URL);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
document.getElementById('statusText').innerText = data.status;
|
||||||
|
document.getElementById('statusText').style.color = '#28a745';
|
||||||
|
document.getElementById('statusDot').className = 'status-dot online';
|
||||||
|
|
||||||
|
document.getElementById('responseTime').innerText = data.response_time_ms + ' ms';
|
||||||
|
document.getElementById('requestCount').innerText = data.request_count;
|
||||||
|
} catch (error) {
|
||||||
|
document.getElementById('statusText').innerText = 'انقطاع الاتصال (Offline)';
|
||||||
|
document.getElementById('statusText').style.color = '#dc3545';
|
||||||
|
document.getElementById('statusDot').className = 'status-dot offline';
|
||||||
|
document.getElementById('responseTime').innerText = '-';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(fetchMetrics, 5000);
|
||||||
|
fetchMetrics();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
28
Q1/monitor.sh
Executable file
28
Q1/monitor.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
API_URL=$1
|
||||||
|
|
||||||
|
echo "Starting monitor script..."
|
||||||
|
echo "API URL: $API_URL"
|
||||||
|
echo "------------------------------------------------"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
HTTP_STATUS=$(curl -o /dev/null -s -w "%{http_code}" -m 5 "$API_URL")
|
||||||
|
|
||||||
|
if [ "$HTTP_STATUS" -eq 200 ]; then
|
||||||
|
response=$(curl -s -m 5 "$API_URL")
|
||||||
|
|
||||||
|
status=$(echo "$response" | jq -r '.status' 2>/dev/null)
|
||||||
|
if [ -n "$status" ] && [ "$status" != "null" ]; then
|
||||||
|
echo "Status: $status | Response Time: ${response_time}ms"
|
||||||
|
else
|
||||||
|
echo "HTTP 200 OK, but invalid JSON format received."
|
||||||
|
fi
|
||||||
|
elif [ "$HTTP_STATUS" -eq 000 ]; then
|
||||||
|
echo "Application is Offline or unreachable (Timeout)."
|
||||||
|
else
|
||||||
|
echo "Application is unhealthy. HTTP Status: $HTTP_STATUS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 30
|
||||||
|
done
|
||||||
35
Q1/server.js
Normal file
35
Q1/server.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const cors = require('cors');
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
app.use(cors());
|
||||||
|
|
||||||
|
let requestCount = 0;
|
||||||
|
let lastResponseTime = 0;
|
||||||
|
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
requestCount++;
|
||||||
|
const start = Date.now();
|
||||||
|
res.on('finish', () => {
|
||||||
|
lastResponseTime = Date.now() - start;
|
||||||
|
});
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/health', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
status: 'Online',
|
||||||
|
uptime_seconds: process.uptime(),
|
||||||
|
request_count: requestCount,
|
||||||
|
response_time_ms: lastResponseTime
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.send('Welcome to the Backend API!');
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`API is running on port ${PORT}`);
|
||||||
|
});
|
||||||
271
Q2/readme,md
Normal file
271
Q2/readme,md
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
# Incident Postmortem: Core Backend API Service Interruption
|
||||||
|
|
||||||
|
> **Incident Date:** July 27, 2026
|
||||||
|
> **Affected Service:** Core .NET Backend API (Ghaymah Platform)
|
||||||
|
> **Severity:** SEV-1 (Critical)
|
||||||
|
> **Status:** Resolved
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Table of Contents
|
||||||
|
|
||||||
|
- [Incident Overview](#incident-overview)
|
||||||
|
- [Incident Metadata](#incident-metadata)
|
||||||
|
- [Executive Summary](#executive-summary)
|
||||||
|
- [Incident Timeline](#incident-timeline)
|
||||||
|
- [Root Cause Analysis](#root-cause-analysis)
|
||||||
|
- [Proposed Auto-Scaling Architecture](#proposed-auto-scaling-architecture)
|
||||||
|
- [Horizontal Pod Autoscaler Configuration](#horizontal-pod-autoscaler-configuration)
|
||||||
|
- [Observability & Early Detection](#observability--early-detection)
|
||||||
|
- [Action Items](#action-items)
|
||||||
|
- [Lessons Learned](#lessons-learned)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Incident Overview
|
||||||
|
|
||||||
|
On **July 27, 2026**, the **Core .NET Backend API** experienced a complete service outage lasting **45 minutes**.
|
||||||
|
|
||||||
|
The outage was caused by repeated **OOMKilled (Exit Code 137)** events after the application exhausted its available memory during a sudden traffic spike.
|
||||||
|
|
||||||
|
The platform entered a continuous **CrashLoop** state, resulting in **100% API failure** until memory resources were increased and the containers were restarted.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Incident Metadata
|
||||||
|
|
||||||
|
| Category | Details |
|
||||||
|
|-----------|---------|
|
||||||
|
| **Affected Service** | Core .NET Backend API |
|
||||||
|
| **Platform** | Ghaymah |
|
||||||
|
| **Date** | 2026-07-27 |
|
||||||
|
| **Downtime** | 45 Minutes |
|
||||||
|
| **Time** | 14:00 – 14:45 EEST |
|
||||||
|
| **Severity** | SEV-1 (Critical) |
|
||||||
|
| **Customer Impact** | 100% API transaction failures |
|
||||||
|
| **Observed Errors** | 502 Bad Gateway |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Executive Summary
|
||||||
|
|
||||||
|
A sudden **400% increase in traffic** caused the backend API to consume memory rapidly.
|
||||||
|
|
||||||
|
The application maintained an **unbounded in-memory cache**, storing increasingly large datasets without expiration or eviction.
|
||||||
|
|
||||||
|
Once the container reached its configured memory limit, the Linux kernel terminated the process (**OOMKilled - Exit Code 137**) to protect node stability.
|
||||||
|
|
||||||
|
The orchestration platform continuously restarted the containers, causing a **CrashLoopBackOff** cycle and preventing the service from recovering automatically.
|
||||||
|
|
||||||
|
Service was restored after:
|
||||||
|
|
||||||
|
- Increasing the container memory limit
|
||||||
|
- Restarting the backend pods
|
||||||
|
- Verifying successful application startup
|
||||||
|
|
||||||
|
Future mitigation requires:
|
||||||
|
|
||||||
|
- Memory-based auto-scaling
|
||||||
|
- Cache eviction policies
|
||||||
|
- API pagination
|
||||||
|
- Improved monitoring and alerting
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Incident Timeline
|
||||||
|
|
||||||
|
| Time | Event |
|
||||||
|
|------|-------|
|
||||||
|
| **13:50** | Traffic increased by approximately **400%** due to an unexpected external campaign. |
|
||||||
|
| **13:58** | Container memory utilization exceeded **95%** of allocated memory. |
|
||||||
|
| **14:00** | First container terminated with **OOMKilled (Exit Code 137)**. Service degradation begins. |
|
||||||
|
| **14:05 – 14:30** | Containers repeatedly restarted by the orchestrator, resulting in a CrashLoop and complete outage. |
|
||||||
|
| **14:30** | On-call infrastructure engineer identified repeated OOMKilled events from platform logs. |
|
||||||
|
| **14:35** | Memory limit increased from **512Mi** to **2Gi** and backend pods restarted manually. |
|
||||||
|
| **14:45** | Service stabilized and API responses returned **HTTP 200 OK**. Incident closed. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Root Cause Analysis
|
||||||
|
|
||||||
|
## Direct Cause
|
||||||
|
|
||||||
|
The Linux kernel terminated the backend process because the application attempted to allocate more memory than the container's configured memory limit.
|
||||||
|
|
||||||
|
```
|
||||||
|
Exit Code: 137
|
||||||
|
Reason: OOMKilled
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Underlying Causes
|
||||||
|
|
||||||
|
### Unbounded In-Memory Cache
|
||||||
|
|
||||||
|
The application stored data inside a local in-memory dictionary that had:
|
||||||
|
|
||||||
|
- No Time-To-Live (TTL)
|
||||||
|
- No maximum cache size
|
||||||
|
- No eviction policy
|
||||||
|
|
||||||
|
Large requests continuously expanded the cache until the process exhausted available memory.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Missing API Pagination
|
||||||
|
|
||||||
|
Several endpoints returned very large datasets.
|
||||||
|
|
||||||
|
Without pagination:
|
||||||
|
|
||||||
|
- Large payloads were cached
|
||||||
|
- Memory usage increased rapidly
|
||||||
|
- Garbage collection became inefficient
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Lack of Horizontal Auto-Scaling
|
||||||
|
|
||||||
|
The deployment relied on static memory limits and a fixed number of replicas.
|
||||||
|
|
||||||
|
As traffic increased:
|
||||||
|
|
||||||
|
- No new replicas were created
|
||||||
|
- Existing containers absorbed all incoming traffic
|
||||||
|
- Memory utilization reached critical levels
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Proposed Auto-Scaling Architecture
|
||||||
|
|
||||||
|
To prevent similar incidents, backend workloads should scale automatically based on memory utilization.
|
||||||
|
|
||||||
|
| Parameter | Recommended Value |
|
||||||
|
|-----------|-------------------|
|
||||||
|
| **Scaling Metric** | Average Memory Utilization |
|
||||||
|
| **Target Utilization** | 75% |
|
||||||
|
| **Minimum Replicas** | 3 |
|
||||||
|
| **Maximum Replicas** | 12 |
|
||||||
|
| **Scale-Up Policy** | Add up to 4 replicas immediately |
|
||||||
|
| **Scale-Down Policy** | Wait 5 minutes after memory falls below 40% |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Horizontal Pod Autoscaler Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: autoscaling/v2
|
||||||
|
kind: HorizontalPodAutoscaler
|
||||||
|
metadata:
|
||||||
|
name: backend-api-scaler
|
||||||
|
|
||||||
|
spec:
|
||||||
|
minReplicas: 3
|
||||||
|
maxReplicas: 12
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: memory
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: 75
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Observability & Early Detection
|
||||||
|
|
||||||
|
To move from reactive troubleshooting to proactive monitoring, the following observability improvements should be implemented.
|
||||||
|
|
||||||
|
## Alerting Rules
|
||||||
|
|
||||||
|
### Memory Utilization Alert
|
||||||
|
|
||||||
|
Trigger notification when:
|
||||||
|
|
||||||
|
- Memory utilization exceeds **70%**
|
||||||
|
- Sustained for **2 consecutive minutes**
|
||||||
|
|
||||||
|
Notification targets:
|
||||||
|
|
||||||
|
- Email
|
||||||
|
- Slack
|
||||||
|
- Webhook
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CrashLoop Detection
|
||||||
|
|
||||||
|
Create a high-priority incident whenever:
|
||||||
|
|
||||||
|
- Container restart count exceeds **3**
|
||||||
|
- Within a **10-minute** window
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dashboard Metrics
|
||||||
|
|
||||||
|
Create a dedicated monitoring dashboard showing:
|
||||||
|
|
||||||
|
- Container memory usage
|
||||||
|
- Container memory limits
|
||||||
|
- Application heap size
|
||||||
|
- Garbage Collection duration
|
||||||
|
- API request rate
|
||||||
|
- Current replica count
|
||||||
|
- Container restart count
|
||||||
|
- CPU utilization
|
||||||
|
- Response latency
|
||||||
|
- Error rate (4xx / 5xx)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Action Items
|
||||||
|
|
||||||
|
| Task | Owner | Priority | Status |
|
||||||
|
|------|-------|----------|--------|
|
||||||
|
| Implement cache eviction (LRU + TTL) | Development Team | Critical | In Progress |
|
||||||
|
| Enforce API pagination | Development Team | Critical | In Progress |
|
||||||
|
| Deploy memory-based Horizontal Pod Autoscaler | DevOps Team | High | Not Started |
|
||||||
|
| Create synthetic load tests (5× traffic) | QA Team | Medium | Not Started |
|
||||||
|
| Update on-call operational runbooks | Operations Team | Low | Completed |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Lessons Learned
|
||||||
|
|
||||||
|
The incident highlighted several architectural improvements required to increase platform resilience.
|
||||||
|
|
||||||
|
## Infrastructure
|
||||||
|
|
||||||
|
- Configure Horizontal Pod Autoscaler (HPA)
|
||||||
|
- Define resource requests and limits carefully
|
||||||
|
- Monitor memory consumption continuously
|
||||||
|
|
||||||
|
## Application
|
||||||
|
|
||||||
|
- Implement cache eviction (LRU)
|
||||||
|
- Apply cache expiration (TTL)
|
||||||
|
- Enforce pagination on all large API endpoints
|
||||||
|
- Optimize memory allocation patterns
|
||||||
|
|
||||||
|
## Operations
|
||||||
|
|
||||||
|
- Improve proactive alerting
|
||||||
|
- Expand observability dashboards
|
||||||
|
- Regularly execute load and stress testing
|
||||||
|
- Update incident response runbooks
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Resolution Summary
|
||||||
|
|
||||||
|
| Item | Result |
|
||||||
|
|------|--------|
|
||||||
|
| **Root Cause** | Unbounded in-memory cache caused container OOM |
|
||||||
|
| **Immediate Fix** | Increased memory limit (512Mi → 2Gi) and restarted pods |
|
||||||
|
| **Long-Term Fixes** | HPA, cache eviction, pagination, monitoring improvements |
|
||||||
|
| **Incident Status** | ✅ Resolved |
|
||||||
|
```
|
||||||
149
Q3-CICD/README.md
Normal file
149
Q3-CICD/README.md
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
# Ghaymah CLI Integration & Environment Strategy
|
||||||
|
|
||||||
|
This document outlines the deployment environment strategy and provides comprehensive documentation for installing, configuring, and authenticating with the **Ghaymah Command Line Interface (CLI)**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Table of Contents
|
||||||
|
|
||||||
|
- [Environment Strategy](#environment-strategy)
|
||||||
|
- [Staging Environment](#staging-environment)
|
||||||
|
- [Production Environment](#production-environment)
|
||||||
|
- [Ghaymah CLI](#ghaymah-cli)
|
||||||
|
- [Prerequisites](#prerequisites)
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [Authentication](#authentication)
|
||||||
|
- [Core Commands](#core-commands)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Environment Strategy
|
||||||
|
|
||||||
|
To ensure reliable deployments and stable software releases, Ghaymah uses separate environments for testing and production workloads.
|
||||||
|
|
||||||
|
| Feature | Staging | Production |
|
||||||
|
|----------|----------|------------|
|
||||||
|
| **Purpose** | Final testing, QA, and integration validation. Mirrors production configuration as closely as possible. | Live customer-facing environment with maximum stability and availability. |
|
||||||
|
| **Users** | Developers, QA engineers, and internal stakeholders. | End users and customers. |
|
||||||
|
| **Data** | Mock, seeded, or anonymized datasets. | Real production data. |
|
||||||
|
| **Deployment** | Automatic deployment after merging into the staging branch (Continuous Deployment). | Manual approval with version tracking (release tags or commit SHA). |
|
||||||
|
| **Resources** | Lower CPU and memory allocation to reduce infrastructure costs. | High availability, load balancing, monitoring, and autoscaling. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Ghaymah CLI
|
||||||
|
|
||||||
|
The **Ghaymah CLI** allows developers and CI/CD pipelines to interact with the platform directly from the terminal.
|
||||||
|
|
||||||
|
It can be used to:
|
||||||
|
|
||||||
|
- Authenticate with the platform
|
||||||
|
- Manage applications
|
||||||
|
- Deploy services
|
||||||
|
- View running applications
|
||||||
|
- Integrate deployments into CI/CD workflows
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before using the CLI, you must generate a **Personal Access Token**.
|
||||||
|
|
||||||
|
1. Log in to the **Ghaymah Web Console**.
|
||||||
|
2. Navigate to:
|
||||||
|
|
||||||
|
```
|
||||||
|
Account Settings
|
||||||
|
└── Developer Settings
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Generate a new **Personal Access Token**.
|
||||||
|
4. Copy the token immediately.
|
||||||
|
|
||||||
|
> **Note**
|
||||||
|
>
|
||||||
|
> The token is displayed only once. Store it securely.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Linux (Debian/Ubuntu/Linux Mint)
|
||||||
|
|
||||||
|
Install the CLI using:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sL https://cli.ghaymah.systems/install.sh | sudo bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify the installation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ghaymah --version
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
For local development or automated CI/CD pipelines, authenticate using your Personal Access Token.
|
||||||
|
|
||||||
|
### Step 1 — Export the Token
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export GHAYMAH_TOKEN="your_personal_access_token_here"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2 — Login
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ghaymah login --token "$GHAYMAH_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
If authentication succeeds, the CLI is ready to use.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Core Commands
|
||||||
|
|
||||||
|
## List Applications
|
||||||
|
|
||||||
|
Display all deployed applications and their current health status.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ghaymah apps list
|
||||||
|
```
|
||||||
|
|
||||||
|
Example output:
|
||||||
|
|
||||||
|
```text
|
||||||
|
NAME STATUS REGION
|
||||||
|
frontend Running eu-central
|
||||||
|
backend Running eu-central
|
||||||
|
database Running eu-central
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Check CLI Version
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ghaymah --version
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Display Help
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ghaymah --help
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Environment Summary
|
||||||
|
|
||||||
|
| Environment | Deployment | Data | Approval |
|
||||||
|
|-------------|------------|------|----------|
|
||||||
|
| **Staging** | Automatic | Test/Mock | Not Required |
|
||||||
|
| **Production** | Manual | Live | Required |
|
||||||
112
Q3-CICD/workflow.yml
Normal file
112
Q3-CICD/workflow.yml
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
name: CI/CD Pipeline to Ghyamah
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
env:
|
||||||
|
GHYAMAH_REGISTRY: app.gitpasha.com
|
||||||
|
GHYAMAH_USERNAME: ${{ github.actor }}
|
||||||
|
ImageName: ${{ env.GHYAMAH_USERNAME }}/my-app
|
||||||
|
jobs:
|
||||||
|
###############################
|
||||||
|
# Test App Code Job
|
||||||
|
#################################
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: npm test
|
||||||
|
|
||||||
|
- name: Build project
|
||||||
|
run: npm run build
|
||||||
|
########################################
|
||||||
|
# build and push docker image to ghyamah registry
|
||||||
|
########################################
|
||||||
|
BuildAndPush:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: test
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Log in to Ghyamah Docker registry
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ${{ env.GHYAMAH_REGISTRY }}
|
||||||
|
username: ${{ env.GHYAMAH_USERNAME }}
|
||||||
|
password: ${{ secrets.GHYAMAH_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Build and Push Docker Image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ env.GHYAMAH_REGISTRY }}/${{ env.ImageName }}:${{ github.sha }}
|
||||||
|
${{ env.GHYAMAH_REGISTRY }}/${{ env.ImageName }}:latest
|
||||||
|
|
||||||
|
#################################
|
||||||
|
# Deploy to Staging and Production Jobs
|
||||||
|
#################################
|
||||||
|
|
||||||
|
deploy-staging:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: BuildAndPush
|
||||||
|
steps:
|
||||||
|
- name: Install Ghyamah CLI
|
||||||
|
run: |
|
||||||
|
curl -sSL https://get.ghaymah.systems/cli/install.sh | sh
|
||||||
|
echo "$HOME/.ghaymah/bin" >> $GITHUB_PATH
|
||||||
|
- name: Log in to Ghyamah
|
||||||
|
run: |
|
||||||
|
ghyamah login --username ${{ env.GHYAMAH_USERNAME }} --password ${{ secrets.GHYAMAH_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Deploy to Staging
|
||||||
|
env:
|
||||||
|
GHYAMAH_TOKEN: ${{ secrets.GHYAMAH_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
ghayamah app update my-staging-app --image ${{ env.GHYAMAH_REGISTRY }}/${{ env.ImageName }}:latest
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Deploy to Production Job with Manual Approval
|
||||||
|
#######################################
|
||||||
|
deploy-production:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: BuildAndPush
|
||||||
|
steps:
|
||||||
|
- name: Wait for Manual Approval
|
||||||
|
uses: trstringer/manual-approval@v1
|
||||||
|
with:
|
||||||
|
secret: ${{ secrets.GHYAMAH_PASSWORD }}
|
||||||
|
issue-title: "Approval Required for Deployment to Production"
|
||||||
|
issue-body: "Please review the changes and approve the deployment to production."
|
||||||
|
- name: Install Ghyamah CLI
|
||||||
|
run: |
|
||||||
|
curl -sSL https://get.ghaymah.systems/cli/install.sh | sh
|
||||||
|
echo "$HOME/.ghaymah/bin" >> $GITHUB_PATH
|
||||||
|
- name: Log in to Ghyamah
|
||||||
|
run: |
|
||||||
|
ghyamah login --username ${{ env.GHYAMAH_USERNAME }} --password ${{ secrets.GHYAMAH_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Deploy to Production
|
||||||
|
env:
|
||||||
|
GHYAMAH_TOKEN: ${{ secrets.GHYAMAH_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
ghayamah app update my-production-app --image ${{ env.GHYAMAH_REGISTRY }}/${{ env.ImageName }}:latest
|
||||||
ثنائية
Q4/GhaymahAPI.png
Normal file
ثنائية
Q4/GhaymahAPI.png
Normal file
ملف ثنائي غير معروض.
|
بعد العرض: | الارتفاع: | الحجم: 1.3 MiB |
307
Q4/README.md
Normal file
307
Q4/README.md
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
# High-Traffic Architecture Design (15,000 Requests/Second)
|
||||||
|
|
||||||
|
This document describes the proposed architecture for handling **15,000 requests per second (RPS)** on the Ghaymah platform. It covers the system architecture, container capacity planning, cold start mitigation strategies, and the use of Ghaymah Block Storage for stateful workloads.
|
||||||
|
|
||||||
|
> **Note**
|
||||||
|
>
|
||||||
|
> The architecture diagram below is a placeholder. Replace it with your architecture image.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Table of Contents
|
||||||
|
|
||||||
|
- [Architecture Overview](#architecture-overview)
|
||||||
|
- [Architecture Diagram](#architecture-diagram)
|
||||||
|
- [Request Flow](#request-flow)
|
||||||
|
- [Container Capacity Planning](#container-capacity-planning)
|
||||||
|
- [Cold Start Strategy](#cold-start-strategy)
|
||||||
|
- [Ghaymah Block Storage for Stateful Workloads](#ghaymah-block-storage-for-stateful-workloads)
|
||||||
|
- [Key Design Decisions](#key-design-decisions)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Architecture Overview
|
||||||
|
|
||||||
|
The system is designed to process **15,000 requests per second** while maintaining high availability, scalability, and fault tolerance.
|
||||||
|
|
||||||
|
The architecture consists of:
|
||||||
|
|
||||||
|
- WAF / CDN
|
||||||
|
- Ghaymah Load Balancer
|
||||||
|
- Auto-scaling application containers
|
||||||
|
- Redis cache cluster
|
||||||
|
- Primary database
|
||||||
|
- Read replicas
|
||||||
|
- Ghaymah Block Storage
|
||||||
|
|
||||||
|
The application containers remain **stateless**, while all persistent data is stored on external block storage.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Architecture Diagram
|
||||||
|
|
||||||
|
The architecture diagram below shows the proposed high-traffic deployment.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Request Flow
|
||||||
|
|
||||||
|
The following sequence illustrates how requests are processed.
|
||||||
|
|
||||||
|
1. Clients send requests to the application.
|
||||||
|
2. The **WAF/CDN** filters malicious traffic and caches static assets.
|
||||||
|
3. Requests are forwarded to the **Ghaymah Load Balancer**.
|
||||||
|
4. The load balancer distributes traffic across healthy application containers.
|
||||||
|
5. Containers first attempt to retrieve data from the **Redis cache**.
|
||||||
|
6. Cache misses are forwarded to the database.
|
||||||
|
7. Read operations are served by database replicas whenever possible.
|
||||||
|
8. Write operations are handled by the primary database.
|
||||||
|
9. All database data is stored on **Ghaymah Block Storage**, ensuring persistence.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# System Components
|
||||||
|
|
||||||
|
| Component | Responsibility |
|
||||||
|
|-----------|----------------|
|
||||||
|
| **Clients** | Generate incoming traffic (15,000 RPS) |
|
||||||
|
| **WAF / CDN** | Security filtering, DDoS protection, static content caching |
|
||||||
|
| **Ghaymah Load Balancer** | Evenly distributes requests across healthy containers |
|
||||||
|
| **Application Containers** | Stateless application processing |
|
||||||
|
| **Redis Cluster** | High-speed caching layer |
|
||||||
|
| **Primary Database** | Handles write operations |
|
||||||
|
| **Read Replicas** | Offload read traffic from the primary database |
|
||||||
|
| **Ghaymah Block Storage** | Persistent storage for stateful workloads |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Container Capacity Planning
|
||||||
|
|
||||||
|
The infrastructure must support **15,000 requests per second**.
|
||||||
|
|
||||||
|
## Assumptions
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
|---------|------:|
|
||||||
|
| Expected Traffic | 15,000 req/s |
|
||||||
|
| Capacity per Container | 500 req/s |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Base Capacity
|
||||||
|
|
||||||
|
```
|
||||||
|
15,000 ÷ 500 = 30 Containers
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Safety Buffer
|
||||||
|
|
||||||
|
To absorb unexpected traffic spikes:
|
||||||
|
|
||||||
|
```
|
||||||
|
30 × 30% = 9 Containers
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Total Required Containers
|
||||||
|
|
||||||
|
```
|
||||||
|
30 + 9 = 39 Containers
|
||||||
|
```
|
||||||
|
|
||||||
|
| Calculation | Result |
|
||||||
|
|-------------|-------:|
|
||||||
|
| Base Containers | 30 |
|
||||||
|
| Safety Margin | 9 |
|
||||||
|
| **Recommended Total** | **39 Containers** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommendation
|
||||||
|
|
||||||
|
During peak traffic periods:
|
||||||
|
|
||||||
|
- Maintain approximately **39 running containers**.
|
||||||
|
- Configure the auto-scaling policy to keep the minimum replica count close to this value.
|
||||||
|
- Scale beyond this threshold during sustained traffic increases.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Cold Start Strategy
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
When a new container starts, it requires time to:
|
||||||
|
|
||||||
|
- Pull the container image
|
||||||
|
- Initialize the runtime
|
||||||
|
- Establish database connections
|
||||||
|
- Load application dependencies
|
||||||
|
|
||||||
|
If traffic is routed before initialization completes, users may experience increased latency or request failures.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommended Mitigations
|
||||||
|
|
||||||
|
### Readiness Probes
|
||||||
|
|
||||||
|
Configure readiness probes so that traffic is routed only after the application is fully initialized.
|
||||||
|
|
||||||
|
Example endpoint:
|
||||||
|
|
||||||
|
```
|
||||||
|
/health/ready
|
||||||
|
```
|
||||||
|
|
||||||
|
Only containers returning **HTTP 200 OK** should receive production traffic.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Pre-Warming
|
||||||
|
|
||||||
|
Initialize expensive resources during startup:
|
||||||
|
|
||||||
|
- Database connections
|
||||||
|
- Redis connections
|
||||||
|
- Configuration loading
|
||||||
|
- Dependency injection
|
||||||
|
- Frequently used libraries
|
||||||
|
|
||||||
|
Avoid performing heavy initialization during the first user request.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Image Optimization
|
||||||
|
|
||||||
|
Reduce startup time by:
|
||||||
|
|
||||||
|
- Using lightweight base images
|
||||||
|
- Removing unnecessary packages
|
||||||
|
- Minimizing image layers
|
||||||
|
- Keeping image sizes as small as possible
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- Alpine Linux
|
||||||
|
- Distroless Images
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Capacity Buffer
|
||||||
|
|
||||||
|
Maintain spare capacity (30% safety margin) so existing containers can absorb traffic while new containers complete startup.
|
||||||
|
|
||||||
|
Benefits:
|
||||||
|
|
||||||
|
- Reduced request latency
|
||||||
|
- Smoother auto-scaling
|
||||||
|
- Improved user experience
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Ghaymah Block Storage for Stateful Workloads
|
||||||
|
|
||||||
|
Containers are **ephemeral** by design.
|
||||||
|
|
||||||
|
If a container is deleted or restarted, its local filesystem is also removed.
|
||||||
|
|
||||||
|
Persistent workloads such as:
|
||||||
|
|
||||||
|
- PostgreSQL
|
||||||
|
- MySQL
|
||||||
|
- MariaDB
|
||||||
|
- MongoDB
|
||||||
|
- Message Brokers
|
||||||
|
|
||||||
|
must store data externally.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. Provision a **Ghaymah Block Storage** volume.
|
||||||
|
2. Attach the volume to the database container.
|
||||||
|
3. Store all database files on the mounted volume.
|
||||||
|
4. If the container fails, the storage remains intact.
|
||||||
|
5. A replacement container automatically reattaches the existing volume.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
### Persistent Data
|
||||||
|
|
||||||
|
Application data survives:
|
||||||
|
|
||||||
|
- Container restarts
|
||||||
|
- Platform upgrades
|
||||||
|
- Node failures
|
||||||
|
- Container replacements
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Compute and Storage Separation
|
||||||
|
|
||||||
|
Containers remain disposable while storage persists independently.
|
||||||
|
|
||||||
|
This allows infrastructure updates without risking data loss.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Faster Recovery
|
||||||
|
|
||||||
|
If the database container crashes:
|
||||||
|
|
||||||
|
1. A replacement container starts.
|
||||||
|
2. The existing block storage volume is attached.
|
||||||
|
3. The database resumes operation with its original data.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### High Performance
|
||||||
|
|
||||||
|
Ghaymah Block Storage provides dedicated storage performance for demanding workloads.
|
||||||
|
|
||||||
|
Benefits include:
|
||||||
|
|
||||||
|
- High IOPS
|
||||||
|
- Low latency
|
||||||
|
- Reliable throughput
|
||||||
|
- Consistent database performance
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Key Design Decisions
|
||||||
|
|
||||||
|
| Area | Decision |
|
||||||
|
|------|----------|
|
||||||
|
| Compute | Stateless application containers |
|
||||||
|
| Scaling | Horizontal auto-scaling |
|
||||||
|
| Load Distribution | Ghaymah Load Balancer |
|
||||||
|
| Caching | Redis Cluster |
|
||||||
|
| Database | Primary + Read Replica architecture |
|
||||||
|
| Storage | Ghaymah Block Storage |
|
||||||
|
| Availability | Multi-container deployment |
|
||||||
|
| Cold Start Mitigation | Readiness probes, pre-warming, optimized images |
|
||||||
|
| Capacity Planning | 39 containers (30 base + 30% buffer) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
|
||||||
|
This architecture is designed to provide:
|
||||||
|
|
||||||
|
- High availability
|
||||||
|
- Horizontal scalability
|
||||||
|
- Fault tolerance
|
||||||
|
- Persistent storage
|
||||||
|
- Fast recovery from failures
|
||||||
|
- Efficient handling of **15,000 requests per second**
|
||||||
|
|
||||||
|
By combining stateless application containers, intelligent load balancing, Redis caching, database replication, and Ghaymah Block Storage, the platform can maintain stable performance during normal operations as well as sudden traffic spikes.
|
||||||
6
Q5/.dockerignore
Normal file
6
Q5/.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
data/
|
||||||
|
*.pyc
|
||||||
|
__pycache__/
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
README.md
|
||||||
40
Q5/Dockerfile
Normal file
40
Q5/Dockerfile
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
FROM python:3.12-alpine
|
||||||
|
|
||||||
|
# Standard-library only — no requirements.txt needed. ca-certificates is
|
||||||
|
# required so ssl.create_default_context() can validate the target's
|
||||||
|
# certificate chain when checking SSL expiry.
|
||||||
|
RUN apk add --no-cache ca-certificates && update-ca-certificates
|
||||||
|
|
||||||
|
# Run as a non-root user
|
||||||
|
RUN addgroup -S monitor && adduser -S monitor -G monitor
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY monitor.py /app/monitor.py
|
||||||
|
COPY Entrypoint.sh /app/entrypoint.sh
|
||||||
|
COPY index.html /app/web/index.html
|
||||||
|
|
||||||
|
RUN chmod +x /app/entrypoint.sh \
|
||||||
|
&& mkdir -p /app/web/data \
|
||||||
|
&& chown -R monitor:monitor /app
|
||||||
|
|
||||||
|
USER monitor
|
||||||
|
|
||||||
|
# Defaults — override any of these at `docker run` / platform deploy time.
|
||||||
|
ENV TARGET_URL="https://mithal.space" \
|
||||||
|
SEARCH_PATH="/search?q=test" \
|
||||||
|
CHECK_INTERVAL=60 \
|
||||||
|
RETENTION_HOURS=24 \
|
||||||
|
REQUEST_TIMEOUT=10 \
|
||||||
|
DATA_FILE="/app/web/data/metrics.json" \
|
||||||
|
WEB_DIR="/app/web" \
|
||||||
|
PORT=8080
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
||||||
|
CMD python3 -c "import urllib.request,sys; sys.exit(0) if urllib.request.urlopen('http://127.0.0.1:${PORT}/', timeout=3).status==200 else sys.exit(1)"
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||||
43
Q5/Entrypoint.sh
Normal file
43
Q5/Entrypoint.sh
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# entrypoint.sh — runs the Python monitoring loop and a static HTTP server
|
||||||
|
# side by side in a single container. POSIX sh so it works on Alpine's
|
||||||
|
# default shell.
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
PORT="${PORT:-8080}"
|
||||||
|
WEB_DIR="${WEB_DIR:-/app/web}"
|
||||||
|
|
||||||
|
echo "[entrypoint] starting monitor loop"
|
||||||
|
python3 /app/monitor.py &
|
||||||
|
MONITOR_PID=$!
|
||||||
|
|
||||||
|
echo "[entrypoint] serving dashboard from ${WEB_DIR} on 0.0.0.0:${PORT}"
|
||||||
|
cd "${WEB_DIR}"
|
||||||
|
python3 -m http.server "${PORT}" --bind 0.0.0.0 &
|
||||||
|
SERVER_PID=$!
|
||||||
|
|
||||||
|
# Forward termination signals to both children and wait for them so the
|
||||||
|
# container shuts down cleanly (e.g. on `docker stop` / platform redeploys).
|
||||||
|
term_handler() {
|
||||||
|
echo "[entrypoint] shutting down..."
|
||||||
|
kill -TERM "$MONITOR_PID" "$SERVER_PID" 2>/dev/null || true
|
||||||
|
wait "$MONITOR_PID" "$SERVER_PID" 2>/dev/null || true
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
trap term_handler TERM INT
|
||||||
|
|
||||||
|
# busybox ash (Alpine's /bin/sh) has no `wait -n`, so poll instead: if
|
||||||
|
# either child dies unexpectedly, bring the whole container down so the
|
||||||
|
# orchestrator (Docker/Kubernetes/Cloud Run/etc.) can restart it.
|
||||||
|
while true; do
|
||||||
|
if ! kill -0 "$MONITOR_PID" 2>/dev/null; then
|
||||||
|
echo "[entrypoint] monitor loop exited unexpectedly — stopping container"
|
||||||
|
term_handler
|
||||||
|
fi
|
||||||
|
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
|
||||||
|
echo "[entrypoint] http server exited unexpectedly — stopping container"
|
||||||
|
term_handler
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
80
Q5/README.md
Normal file
80
Q5/README.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# mithal-space-monitor
|
||||||
|
|
||||||
|
A lightweight, stdlib-only uptime/latency/SSL monitor with a single-page
|
||||||
|
Chart.js dashboard, packaged into one container.
|
||||||
|
|
||||||
|
## What's inside
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `monitor.py` | Standard-library-only Python script. Every `CHECK_INTERVAL` seconds it checks DNS resolution time, HTTP status + latency, search-endpoint latency, and SSL cert expiry, then writes a rolling `RETENTION_HOURS` window to a JSON file. |
|
||||||
|
| `index.html` | Single-page dashboard (HTML/CSS/JS + Chart.js via CDN). Polls the JSON file every 30s and renders 24h uptime %, a 60-minute latency line chart, SSL expiry, and a table of the last 10 checks. |
|
||||||
|
| `entrypoint.sh` | Starts `monitor.py` and `python -m http.server` side by side, forwards signals, and exits the container if either process dies (so the orchestrator restarts it). |
|
||||||
|
| `Dockerfile` | `python:3.12-alpine` base, non-root user, healthcheck, no external Python deps. |
|
||||||
|
|
||||||
|
## Configuration (environment variables)
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `TARGET_URL` | `https://mithal.space` | URL to monitor |
|
||||||
|
| `SEARCH_PATH` | `/search?q=test` | Path appended to the target's origin for the search-latency check |
|
||||||
|
| `CHECK_INTERVAL` | `60` | Seconds between checks |
|
||||||
|
| `RETENTION_HOURS` | `24` | Rolling window kept in the JSON log |
|
||||||
|
| `REQUEST_TIMEOUT` | `10` | Per-request timeout (seconds) |
|
||||||
|
| `PORT` | `8080` | Dashboard HTTP server port |
|
||||||
|
|
||||||
|
## Build & run locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t mithal-space-monitor .
|
||||||
|
|
||||||
|
docker run -d \
|
||||||
|
--name mithal-monitor \
|
||||||
|
-p 8080:8080 \
|
||||||
|
-e TARGET_URL="https://mithal.space" \
|
||||||
|
-e SEARCH_PATH="/search?q=test" \
|
||||||
|
-v mithal_monitor_data:/app/data \
|
||||||
|
mithal-space-monitor
|
||||||
|
|
||||||
|
# open http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
The `-v mithal_monitor_data:/app/data` volume is optional but recommended so
|
||||||
|
your 24h history survives a container restart/redeploy.
|
||||||
|
|
||||||
|
## Push to a registry
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker tag mithal-space-monitor registry.example.com/yourorg/mithal-space-monitor:latest
|
||||||
|
docker push registry.example.com/yourorg/mithal-space-monitor:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
This image is a single process group exposing one HTTP port, so it runs
|
||||||
|
as-is on most container platforms:
|
||||||
|
|
||||||
|
- **Cloud Run / Container Apps / Fly.io**: deploy the image, set `PORT`
|
||||||
|
to match the platform's expected port (Cloud Run injects `PORT`
|
||||||
|
automatically - the entrypoint already respects it), mount a persistent
|
||||||
|
volume if the platform supports one (otherwise history resets on redeploy,
|
||||||
|
which is fine - it just rebuilds over the next `RETENTION_HOURS`).
|
||||||
|
- **Kubernetes**: run as a `Deployment` with 1 replica, a `Service` of type
|
||||||
|
`ClusterIP`/`LoadBalancer`, and optionally a `PersistentVolumeClaim`
|
||||||
|
mounted at `/app/data`. The built-in `HEALTHCHECK` maps naturally to a
|
||||||
|
liveness probe on `GET /`.
|
||||||
|
- **Plain VM / docker-compose**: use the `docker run` command above behind
|
||||||
|
your existing reverse proxy / TLS terminator.
|
||||||
|
|
||||||
|
## Notes & extension points
|
||||||
|
|
||||||
|
- Everything in `monitor.py` uses only the Python standard library
|
||||||
|
(`urllib`, `socket`, `ssl`, `json`) - no `pip install` step, no
|
||||||
|
dependency surface in the image.
|
||||||
|
- Data is written atomically (`write → temp file → os.replace`) so the
|
||||||
|
dashboard never reads a half-written JSON file.
|
||||||
|
- To monitor multiple targets, run one container per target (each with its
|
||||||
|
own `TARGET_URL`/port), or extend `monitor.py` to loop over a list of
|
||||||
|
targets and extend `index.html` with a target selector.
|
||||||
|
- Add basic auth / IP allowlisting at your reverse proxy if the dashboard
|
||||||
|
shouldn't be public.
|
||||||
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>
|
||||||
208
Q5/monitor.py
Normal file
208
Q5/monitor.py
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
monitor.py — Lightweight uptime / latency / SSL monitor.
|
||||||
|
|
||||||
|
Standard-library only (urllib, ssl, socket, json, time). Runs an infinite
|
||||||
|
loop, checks a target URL (and optionally a search endpoint) every
|
||||||
|
CHECK_INTERVAL seconds, and persists a rolling RETENTION_HOURS window of
|
||||||
|
results to a JSON file that the static dashboard reads.
|
||||||
|
|
||||||
|
Configuration is via environment variables so the same image can monitor
|
||||||
|
any site without a rebuild:
|
||||||
|
|
||||||
|
TARGET_URL Full URL to monitor (default: https://mithal.space)
|
||||||
|
SEARCH_PATH Path appended to origin for a (default: /search?q=test)
|
||||||
|
secondary "search" check. Set to "" to disable.
|
||||||
|
CHECK_INTERVAL Seconds between checks (default: 60)
|
||||||
|
RETENTION_HOURS How much history to keep (default: 24)
|
||||||
|
REQUEST_TIMEOUT Per-request timeout, seconds (default: 10)
|
||||||
|
DATA_FILE Where to write the JSON log (default: /app/web/data/metrics.json)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import ssl
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Configuration
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TARGET_URL = os.environ.get("TARGET_URL", "https://mithal.space")
|
||||||
|
SEARCH_PATH = os.environ.get("SEARCH_PATH", "/search?q=test")
|
||||||
|
CHECK_INTERVAL = int(os.environ.get("CHECK_INTERVAL", "60"))
|
||||||
|
RETENTION_HOURS = float(os.environ.get("RETENTION_HOURS", "24"))
|
||||||
|
REQUEST_TIMEOUT = float(os.environ.get("REQUEST_TIMEOUT", "10"))
|
||||||
|
DATA_FILE = os.environ.get("DATA_FILE", "/app/web/data/metrics.json")
|
||||||
|
USER_AGENT = "uptime-monitor/1.0 (+standard-library)"
|
||||||
|
|
||||||
|
_parsed = urlparse(TARGET_URL)
|
||||||
|
HOSTNAME = _parsed.hostname
|
||||||
|
PORT = _parsed.port or (443 if _parsed.scheme == "https" else 80)
|
||||||
|
SEARCH_URL = f"{_parsed.scheme}://{_parsed.netloc}{SEARCH_PATH}" if SEARCH_PATH else None
|
||||||
|
|
||||||
|
|
||||||
|
def now_iso() -> str:
|
||||||
|
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
||||||
|
|
||||||
|
|
||||||
|
def measure_dns(hostname: str):
|
||||||
|
"""Return DNS resolution time in milliseconds, or None on failure."""
|
||||||
|
start = time.perf_counter()
|
||||||
|
try:
|
||||||
|
socket.getaddrinfo(hostname, None)
|
||||||
|
except socket.gaierror as exc:
|
||||||
|
return None, str(exc)
|
||||||
|
elapsed_ms = (time.perf_counter() - start) * 1000
|
||||||
|
return round(elapsed_ms, 2), None
|
||||||
|
|
||||||
|
|
||||||
|
def timed_get(url: str, timeout: float):
|
||||||
|
"""
|
||||||
|
Perform an HTTP GET and return (status_code, latency_ms, error_str).
|
||||||
|
latency_ms measures time-to-first-byte-of-full-response (connect + TLS
|
||||||
|
+ request + response), matching what a real visitor experiences.
|
||||||
|
"""
|
||||||
|
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
|
||||||
|
start = time.perf_counter()
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||||
|
resp.read(1) # confirm the body actually starts streaming
|
||||||
|
status = resp.status
|
||||||
|
latency_ms = (time.perf_counter() - start) * 1000
|
||||||
|
return status, round(latency_ms, 2), None
|
||||||
|
except urllib.error.HTTPError as exc:
|
||||||
|
# Still a "successful" connection from a monitoring standpoint —
|
||||||
|
# the server responded, just with an error status.
|
||||||
|
latency_ms = (time.perf_counter() - start) * 1000
|
||||||
|
return exc.code, round(latency_ms, 2), None
|
||||||
|
except (urllib.error.URLError, socket.timeout, TimeoutError) as exc:
|
||||||
|
latency_ms = (time.perf_counter() - start) * 1000
|
||||||
|
return None, round(latency_ms, 2), str(getattr(exc, "reason", exc))
|
||||||
|
|
||||||
|
|
||||||
|
def measure_ssl_expiry(hostname: str, port: int, timeout: float):
|
||||||
|
"""Return (days_remaining, error_str) for the TLS certificate."""
|
||||||
|
if not hostname:
|
||||||
|
return None, "no hostname"
|
||||||
|
try:
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
with socket.create_connection((hostname, port), timeout=timeout) as sock:
|
||||||
|
with ctx.wrap_socket(sock, server_hostname=hostname) as ssock:
|
||||||
|
cert = ssock.getpeercert()
|
||||||
|
not_after = cert.get("notAfter")
|
||||||
|
if not not_after:
|
||||||
|
return None, "no notAfter field in certificate"
|
||||||
|
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)).total_seconds() / 86400
|
||||||
|
return round(days_remaining, 1), None
|
||||||
|
except Exception as exc: # noqa: BLE001 — monitoring must never crash the loop
|
||||||
|
return None, str(exc)
|
||||||
|
|
||||||
|
|
||||||
|
def run_check() -> dict:
|
||||||
|
dns_ms, dns_err = measure_dns(HOSTNAME)
|
||||||
|
status, latency_ms, http_err = timed_get(TARGET_URL, REQUEST_TIMEOUT)
|
||||||
|
|
||||||
|
search_status, search_latency_ms, search_err = None, None, None
|
||||||
|
if SEARCH_URL:
|
||||||
|
search_status, search_latency_ms, search_err = timed_get(SEARCH_URL, REQUEST_TIMEOUT)
|
||||||
|
|
||||||
|
ssl_days, ssl_err = (None, None)
|
||||||
|
if _parsed.scheme == "https":
|
||||||
|
ssl_days, ssl_err = measure_ssl_expiry(HOSTNAME, PORT, REQUEST_TIMEOUT)
|
||||||
|
|
||||||
|
success = status is not None and 200 <= status < 400
|
||||||
|
|
||||||
|
return {
|
||||||
|
"timestamp": now_iso(),
|
||||||
|
"target": TARGET_URL,
|
||||||
|
"http_status": status,
|
||||||
|
"success": success,
|
||||||
|
"latency_ms": latency_ms,
|
||||||
|
"dns_ms": dns_ms,
|
||||||
|
"search_status": search_status,
|
||||||
|
"search_latency_ms": search_latency_ms,
|
||||||
|
"ssl_days_remaining": ssl_days,
|
||||||
|
"error": http_err or dns_err or ssl_err or search_err,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def load_history(path: str) -> list:
|
||||||
|
try:
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
return data.get("checks", []) if isinstance(data, dict) else data
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def prune(history: list, retention_hours: float) -> list:
|
||||||
|
cutoff = time.time() - retention_hours * 3600
|
||||||
|
pruned = []
|
||||||
|
for entry in history:
|
||||||
|
try:
|
||||||
|
ts = datetime.fromisoformat(entry["timestamp"]).timestamp()
|
||||||
|
except (KeyError, ValueError):
|
||||||
|
continue
|
||||||
|
if ts >= cutoff:
|
||||||
|
pruned.append(entry)
|
||||||
|
return pruned
|
||||||
|
|
||||||
|
|
||||||
|
def save_history(path: str, history: list) -> None:
|
||||||
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||||
|
payload = {
|
||||||
|
"target": TARGET_URL,
|
||||||
|
"search_url": SEARCH_URL,
|
||||||
|
"updated_at": now_iso(),
|
||||||
|
"check_interval_seconds": CHECK_INTERVAL,
|
||||||
|
"retention_hours": RETENTION_HOURS,
|
||||||
|
"checks": history,
|
||||||
|
}
|
||||||
|
tmp_path = f"{path}.tmp"
|
||||||
|
with open(tmp_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(payload, f, indent=2)
|
||||||
|
os.replace(tmp_path, path) # atomic write so the dashboard never reads a half-written file
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
print(f"[monitor] target={TARGET_URL} interval={CHECK_INTERVAL}s "
|
||||||
|
f"retention={RETENTION_HOURS}h data_file={DATA_FILE}", flush=True)
|
||||||
|
|
||||||
|
history = load_history(DATA_FILE)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
cycle_start = time.time()
|
||||||
|
result = run_check()
|
||||||
|
history.append(result)
|
||||||
|
history = prune(history, RETENTION_HOURS)
|
||||||
|
save_history(DATA_FILE, history)
|
||||||
|
|
||||||
|
status_str = result["http_status"] if result["http_status"] is not None else "ERR"
|
||||||
|
print(
|
||||||
|
f"[monitor] {result['timestamp']} status={status_str} "
|
||||||
|
f"latency={result['latency_ms']}ms dns={result['dns_ms']}ms "
|
||||||
|
f"ssl_days={result['ssl_days_remaining']} "
|
||||||
|
f"search_latency={result['search_latency_ms']}ms "
|
||||||
|
f"{'error=' + result['error'] if result['error'] else 'ok'}",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
elapsed = time.time() - cycle_start
|
||||||
|
time.sleep(max(0, CHECK_INTERVAL - elapsed))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
sys.exit(0)
|
||||||
المرجع في مشكلة جديدة
حظر مستخدم