adding task files
هذا الالتزام موجود في:
1
common-qabilah/qabilah-profile.txt
Normal file
1
common-qabilah/qabilah-profile.txt
Normal file
@@ -0,0 +1 @@
|
||||
https://qabilah.com/profile/moustafamedhet97/posts
|
||||
16
q1-deploy-monitor/Dockerfile
Normal file
16
q1-deploy-monitor/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["python", "app.py"]
|
||||
70
q1-deploy-monitor/app.py
Normal file
70
q1-deploy-monitor/app.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from flask import Flask, jsonify, render_template, request
|
||||
import time
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# Application start time
|
||||
start_time = time.time()
|
||||
|
||||
# Metrics
|
||||
request_count = 0
|
||||
last_response_time = 0
|
||||
|
||||
|
||||
@app.before_request
|
||||
def before_request():
|
||||
request.start_time = time.perf_counter()
|
||||
|
||||
|
||||
@app.after_request
|
||||
def after_request(response):
|
||||
global request_count
|
||||
global last_response_time
|
||||
|
||||
request_count += 1
|
||||
last_response_time = round(
|
||||
(time.perf_counter() - request.start_time) * 1000, 2
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def home():
|
||||
return render_template("dashboard.html")
|
||||
|
||||
|
||||
@app.route("/health")
|
||||
def health():
|
||||
return jsonify({
|
||||
"status": "UP",
|
||||
"message": "Application is healthy",
|
||||
"uptime_seconds": round(time.time() - start_time, 2),
|
||||
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"version": "1.0.0"
|
||||
}), 200
|
||||
|
||||
|
||||
@app.route("/metrics")
|
||||
def metrics():
|
||||
return jsonify({
|
||||
"status": "UP",
|
||||
"requests": request_count,
|
||||
"response_time_ms": last_response_time,
|
||||
"uptime_seconds": round(time.time() - start_time, 2)
|
||||
})
|
||||
|
||||
|
||||
@app.route("/api/info")
|
||||
def api_info():
|
||||
return jsonify({
|
||||
"application": "Ghaymah SRE API",
|
||||
"language": "Python",
|
||||
"framework": "Flask",
|
||||
"version": "1.0.0",
|
||||
"author": "Moustafa Medhat"
|
||||
})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8080)
|
||||
30
q1-deploy-monitor/health-check.sh
Executable file
30
q1-deploy-monitor/health-check.sh
Executable file
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
|
||||
URL="http://localhost:8080/health"
|
||||
LOG_FILE="/home/ec2-user/projects/ghaymah-exam-moustafa-medhat-sre/q1-deploy-monitor/health.log"
|
||||
|
||||
touch "$LOG_FILE"
|
||||
|
||||
echo "===== Health Monitor Started =====" >> "$LOG_FILE"
|
||||
|
||||
while true
|
||||
do
|
||||
START=$(date +%s%3N)
|
||||
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$URL")
|
||||
|
||||
END=$(date +%s%3N)
|
||||
RESPONSE_TIME=$((END - START))
|
||||
|
||||
TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
|
||||
|
||||
if [ "$HTTP_CODE" -eq 200 ]; then
|
||||
STATUS="UP"
|
||||
else
|
||||
STATUS="DOWN"
|
||||
fi
|
||||
|
||||
echo "[$TIMESTAMP] Status=$STATUS | HTTP=$HTTP_CODE | Response=${RESPONSE_TIME}ms" >> "$LOG_FILE"
|
||||
|
||||
sleep 30
|
||||
done
|
||||
29
q1-deploy-monitor/health.log
Normal file
29
q1-deploy-monitor/health.log
Normal file
@@ -0,0 +1,29 @@
|
||||
[2026-07-29 20:25:07] Status=DOWN | HTTP=000 | Response=6ms
|
||||
[2026-07-29 20:25:37] Status=DOWN | HTTP=000 | Response=7ms
|
||||
[2026-07-29 20:26:07] Status=DOWN | HTTP=000 | Response=6ms
|
||||
[2026-07-29 20:26:37] Status=DOWN | HTTP=000 | Response=7ms
|
||||
[2026-07-29 20:27:07] Status=DOWN | HTTP=000 | Response=6ms
|
||||
[2026-07-29 20:27:37] Status=DOWN | HTTP=000 | Response=7ms
|
||||
[2026-07-29 20:28:07] Status=DOWN | HTTP=000 | Response=7ms
|
||||
[2026-07-29 20:28:37] Status=DOWN | HTTP=000 | Response=7ms
|
||||
[2026-07-29 20:29:07] Status=DOWN | HTTP=000 | Response=6ms
|
||||
[2026-07-29 20:29:37] Status=DOWN | HTTP=000 | Response=7ms
|
||||
[2026-07-29 20:30:07] Status=DOWN | HTTP=000 | Response=7ms
|
||||
[2026-07-29 20:30:37] Status=DOWN | HTTP=000 | Response=7ms
|
||||
[2026-07-29 20:31:07] Status=DOWN | HTTP=000 | Response=6ms
|
||||
[2026-07-29 20:31:37] Status=DOWN | HTTP=000 | Response=7ms
|
||||
[2026-07-29 20:32:07] Status=DOWN | HTTP=000 | Response=7ms
|
||||
[2026-07-29 20:32:37] Status=DOWN | HTTP=000 | Response=7ms
|
||||
[2026-07-29 20:33:07] Status=DOWN | HTTP=000 | Response=7ms
|
||||
[2026-07-29 20:33:37] Status=DOWN | HTTP=000 | Response=6ms
|
||||
[2026-07-29 20:34:07] Status=DOWN | HTTP=000 | Response=7ms
|
||||
[2026-07-29 20:34:38] Status=DOWN | HTTP=000 | Response=7ms
|
||||
[2026-07-29 20:35:08] Status=DOWN | HTTP=000 | Response=6ms
|
||||
[2026-07-29 20:35:38] Status=DOWN | HTTP=000 | Response=8ms
|
||||
[2026-07-29 20:36:08] Status=DOWN | HTTP=000 | Response=7ms
|
||||
[2026-07-29 20:36:38] Status=DOWN | HTTP=000 | Response=7ms
|
||||
[2026-07-29 20:37:08] Status=DOWN | HTTP=000 | Response=6ms
|
||||
[2026-07-29 20:37:38] Status=DOWN | HTTP=000 | Response=7ms
|
||||
[2026-07-29 20:38:08] Status=DOWN | HTTP=000 | Response=6ms
|
||||
[2026-07-29 20:38:38] Status=DOWN | HTTP=000 | Response=7ms
|
||||
[2026-07-29 20:39:08] Status=DOWN | HTTP=000 | Response=7ms
|
||||
1
q1-deploy-monitor/requirements.txt
Normal file
1
q1-deploy-monitor/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
Flask==3.1.0
|
||||
151
q1-deploy-monitor/templates/dashboard.html
Normal file
151
q1-deploy-monitor/templates/dashboard.html
Normal file
@@ -0,0 +1,151 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ghaymah API Dashboard</title>
|
||||
|
||||
<style>
|
||||
*{
|
||||
margin:0;
|
||||
padding:0;
|
||||
box-sizing:border-box;
|
||||
font-family:Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
body{
|
||||
background:#f5f7fb;
|
||||
}
|
||||
|
||||
.container{
|
||||
width:900px;
|
||||
margin:40px auto;
|
||||
}
|
||||
|
||||
h1{
|
||||
text-align:center;
|
||||
margin-bottom:30px;
|
||||
color:#2c3e50;
|
||||
}
|
||||
|
||||
.cards{
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
gap:20px;
|
||||
}
|
||||
|
||||
.card{
|
||||
flex:1;
|
||||
background:white;
|
||||
border-radius:10px;
|
||||
padding:25px;
|
||||
text-align:center;
|
||||
box-shadow:0 5px 15px rgba(0,0,0,.1);
|
||||
}
|
||||
|
||||
.title{
|
||||
color:#666;
|
||||
font-size:18px;
|
||||
margin-bottom:15px;
|
||||
}
|
||||
|
||||
.value{
|
||||
font-size:34px;
|
||||
font-weight:bold;
|
||||
}
|
||||
|
||||
.up{
|
||||
color:green;
|
||||
}
|
||||
|
||||
.down{
|
||||
color:red;
|
||||
}
|
||||
|
||||
.footer{
|
||||
margin-top:35px;
|
||||
text-align:center;
|
||||
color:#777;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
|
||||
<h1>🚀 Ghaymah SRE Dashboard</h1>
|
||||
|
||||
<div class="cards">
|
||||
|
||||
<div class="card">
|
||||
<div class="title">Status</div>
|
||||
<div id="status" class="value">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="title">Response Time</div>
|
||||
<div id="response" class="value">0 ms</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="title">Requests</div>
|
||||
<div id="requests" class="value">0</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
Last Update:
|
||||
<span id="time">--</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
async function loadMetrics(){
|
||||
|
||||
try{
|
||||
|
||||
const response = await fetch('/metrics');
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const status=document.getElementById("status");
|
||||
|
||||
status.innerHTML=data.status;
|
||||
|
||||
status.className="value";
|
||||
|
||||
if(data.status==="UP")
|
||||
status.classList.add("up");
|
||||
else
|
||||
status.classList.add("down");
|
||||
|
||||
document.getElementById("response").innerHTML=data.response_time_ms+" ms";
|
||||
|
||||
document.getElementById("requests").innerHTML=data.requests;
|
||||
|
||||
document.getElementById("time").innerHTML=
|
||||
new Date().toLocaleTimeString();
|
||||
|
||||
}
|
||||
|
||||
catch(error){
|
||||
|
||||
document.getElementById("status").innerHTML="DOWN";
|
||||
|
||||
document.getElementById("status").className="value down";
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
loadMetrics();
|
||||
|
||||
setInterval(loadMetrics,30000);
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
120
q2-postmortem/postmortem-report.md
Normal file
120
q2-postmortem/postmortem-report.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# Postmortem Report
|
||||
|
||||
## Incident Summary
|
||||
|
||||
**Incident Title:** Repeated OOMKilled Causing Application Downtime
|
||||
|
||||
**Date:** 2026-07-28
|
||||
|
||||
**Duration:** 45 minutes
|
||||
|
||||
**Severity:** High (SEV-1)
|
||||
|
||||
**Impact:**
|
||||
The application became unavailable for 45 minutes because the container was repeatedly terminated with the `OOMKilled` status. During the outage, users were unable to access the application, resulting in service disruption.
|
||||
|
||||
---
|
||||
|
||||
# Timeline
|
||||
|
||||
| Time | Event |
|
||||
|------|-------|
|
||||
| 14:00 | Increased traffic caused memory usage to rise. |
|
||||
| 14:05 | Container exceeded its memory limit and was terminated (OOMKilled). |
|
||||
| 14:06 | Kubernetes restarted the container automatically. |
|
||||
| 14:10 - 14:40 | Continuous restart loop occurred due to repeated OOMKilled events. |
|
||||
| 14:25 | Monitoring system generated high memory usage alerts. |
|
||||
| 14:35 | SRE team began investigating pod events and resource metrics. |
|
||||
| 14:45 | Memory limit was increased and the deployment was restarted. |
|
||||
| 14:45 | Application recovered successfully. |
|
||||
|
||||
---
|
||||
|
||||
# Root Cause Analysis
|
||||
|
||||
### Primary Root Cause
|
||||
|
||||
The application exceeded the configured memory limit. When the container reached the limit, the Linux Out Of Memory (OOM) Killer terminated the process.
|
||||
|
||||
### Contributing Factors
|
||||
|
||||
- Memory limits were configured too low.
|
||||
- No Horizontal Pod Autoscaler (HPA) was configured.
|
||||
- Monitoring alerts were triggered after the application became unstable.
|
||||
- No load testing had been performed before production deployment.
|
||||
|
||||
---
|
||||
|
||||
# Resolution
|
||||
|
||||
The following actions restored the service:
|
||||
|
||||
- Increased container memory limits.
|
||||
- Restarted the deployment.
|
||||
- Verified application health using `/health`.
|
||||
- Confirmed stable memory usage.
|
||||
|
||||
---
|
||||
|
||||
# Preventive Actions
|
||||
|
||||
### Immediate
|
||||
|
||||
- Increase memory requests and limits.
|
||||
- Add memory usage alerts.
|
||||
- Perform load testing before deployment.
|
||||
|
||||
### Long-term
|
||||
|
||||
- Configure Horizontal Pod Autoscaler (HPA).
|
||||
- Implement automatic scaling based on CPU and Memory.
|
||||
- Enable centralized logging.
|
||||
- Create SLOs and alerting rules.
|
||||
- Review resource requests for every deployment.
|
||||
|
||||
---
|
||||
|
||||
# Auto-Scaling Policy
|
||||
|
||||
To prevent similar incidents:
|
||||
|
||||
- Minimum Replicas: **2**
|
||||
- Maximum Replicas: **10**
|
||||
- Target CPU Utilization: **70%**
|
||||
- Target Memory Utilization: **75%**
|
||||
- Scale Up immediately when thresholds are exceeded.
|
||||
- Scale Down gradually after 5 minutes of stable utilization.
|
||||
- Configure PodDisruptionBudget to maintain availability.
|
||||
|
||||
---
|
||||
|
||||
# Early Detection Using Ghaymah Monitoring
|
||||
|
||||
The issue can be detected early by monitoring:
|
||||
|
||||
- Container Memory Usage
|
||||
- Pod Restart Count
|
||||
- OOMKilled Events
|
||||
- Container Health Checks
|
||||
- Response Time
|
||||
- Error Rate (5xx)
|
||||
- Request Rate
|
||||
- Application Availability
|
||||
|
||||
Recommended alerts:
|
||||
|
||||
- Memory Usage > 80%
|
||||
- Restart Count > 3 within 5 minutes
|
||||
- Health Check Failure
|
||||
- Response Time > 500 ms
|
||||
- Availability below 99%
|
||||
|
||||
---
|
||||
|
||||
# Lessons Learned
|
||||
|
||||
- Configure appropriate resource limits.
|
||||
- Enable proactive monitoring and alerting.
|
||||
- Test the application under expected production load.
|
||||
- Implement autoscaling before deploying production workloads.
|
||||
- Continuously review resource utilization trends.
|
||||
16
q3-cicd/Dockerfile
Normal file
16
q3-cicd/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["python", "app.py"]
|
||||
254
q3-cicd/README.md
Normal file
254
q3-cicd/README.md
Normal file
@@ -0,0 +1,254 @@
|
||||
# Ghaymah SRE - CI/CD Pipeline Project
|
||||
|
||||
## Overview
|
||||
|
||||
This project demonstrates a complete CI/CD pipeline using **GitHub Actions**, **Docker**, **Docker Hub**, and **AWS EC2**.
|
||||
|
||||
The pipeline automatically builds a Docker image, pushes it to Docker Hub, waits for manual approval before production deployment, and deploys the application to an EC2 instance.
|
||||
|
||||
---
|
||||
|
||||
# Project Architecture
|
||||
|
||||
Developer
|
||||
│
|
||||
▼
|
||||
GitHub Repository
|
||||
│
|
||||
▼
|
||||
GitHub Actions
|
||||
│
|
||||
▼
|
||||
Build Docker Image
|
||||
│
|
||||
▼
|
||||
Push Image to Docker Hub
|
||||
│
|
||||
▼
|
||||
Manual Approval
|
||||
│
|
||||
▼
|
||||
Deploy to AWS EC2
|
||||
│
|
||||
▼
|
||||
Docker Container
|
||||
│
|
||||
▼
|
||||
Running Flask Application
|
||||
|
||||
---
|
||||
|
||||
# Technologies Used
|
||||
|
||||
- Git
|
||||
- GitHub
|
||||
- GitHub Actions
|
||||
- Docker
|
||||
- Docker Hub
|
||||
- AWS EC2 (Amazon Linux 2023)
|
||||
- Python Flask
|
||||
|
||||
---
|
||||
|
||||
# Project Structure
|
||||
|
||||
```
|
||||
.
|
||||
├── .github
|
||||
│ └── workflows
|
||||
│ └── docker-build-push.yml
|
||||
├── Dockerfile
|
||||
├── requirements.txt
|
||||
├── app.py
|
||||
├── templates/
|
||||
├── static/
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# CI/CD Workflow
|
||||
|
||||
The GitHub Actions workflow performs the following steps:
|
||||
|
||||
1. Trigger on every push to the main branch.
|
||||
2. Checkout the repository.
|
||||
3. Log in to Docker Hub using GitHub Secrets.
|
||||
4. Build the Docker image.
|
||||
5. Push the image to Docker Hub.
|
||||
6. Wait for manual approval using the Production Environment.
|
||||
7. Deploy the latest image to the EC2 server.
|
||||
8. Verify that the application is running successfully.
|
||||
|
||||
---
|
||||
|
||||
# GitHub Secrets
|
||||
|
||||
The following repository secrets were configured:
|
||||
|
||||
| Secret | Description |
|
||||
|---------|-------------|
|
||||
| DOCKER_USERNAME | Docker Hub username |
|
||||
| DOCKER_TOKEN | Docker Hub Access Token |
|
||||
|
||||
---
|
||||
|
||||
# Manual Approval
|
||||
|
||||
A GitHub Environment named **production** was created.
|
||||
|
||||
Deployment to production requires manual approval before execution.
|
||||
|
||||
This prevents accidental deployments and provides an additional safety layer.
|
||||
|
||||
---
|
||||
|
||||
# Docker Image
|
||||
|
||||
The application image is stored in Docker Hub.
|
||||
|
||||
Image name:
|
||||
|
||||
```
|
||||
moustafamedhat97/ghaymah-api
|
||||
```
|
||||
|
||||
Each build is tagged using the Git commit SHA.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
moustafamedhat97/ghaymah-api:0367751868ea7532b1b22f744baa5e49851158f6
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Deployment
|
||||
|
||||
The deployment process performs the following:
|
||||
|
||||
- Pull latest Docker image
|
||||
- Stop old container (if exists)
|
||||
- Remove old container
|
||||
- Start new container
|
||||
- Expose port 8080
|
||||
- Restart application
|
||||
|
||||
Deployment command:
|
||||
|
||||
```bash
|
||||
docker pull moustafamedhat97/ghaymah-api:latest
|
||||
|
||||
docker stop ghaymah-api || true
|
||||
|
||||
docker rm ghaymah-api || true
|
||||
|
||||
docker run -d \
|
||||
--name ghaymah-api \
|
||||
-p 8080:8080 \
|
||||
moustafamedhat97/ghaymah-api:latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Application Health Check
|
||||
|
||||
The application exposes a health endpoint:
|
||||
|
||||
```
|
||||
GET /health
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
http://<EC2-Public-IP>:8080/health
|
||||
```
|
||||
|
||||
Expected response:
|
||||
|
||||
```json
|
||||
{
|
||||
"status":"UP"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Dashboard
|
||||
|
||||
The deployed application provides a dashboard displaying:
|
||||
|
||||
- Application Status
|
||||
- Response Time
|
||||
- Total Requests
|
||||
- Last Update Time
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
http://<EC2-Public-IP>:8080
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Difference Between Staging and Production
|
||||
|
||||
## Staging
|
||||
|
||||
- Used for testing before release.
|
||||
- Mirrors the production environment.
|
||||
- Safe for validation and QA.
|
||||
- Can contain test data.
|
||||
|
||||
## Production
|
||||
|
||||
- Live environment.
|
||||
- Used by end users.
|
||||
- Requires high availability.
|
||||
- Requires manual approval before deployment.
|
||||
|
||||
---
|
||||
|
||||
# Ghaymah CLI Integration
|
||||
|
||||
The original requirement requested deployment using the Ghaymah CLI.
|
||||
|
||||
Since the CLI was not available in the execution environment, deployment was simulated using Docker commands.
|
||||
|
||||
The integration steps would normally be:
|
||||
|
||||
1. Install Ghaymah CLI.
|
||||
2. Authenticate using:
|
||||
|
||||
```bash
|
||||
ghaymah login
|
||||
```
|
||||
|
||||
3. Configure the target project.
|
||||
|
||||
4. Deploy the application:
|
||||
|
||||
```bash
|
||||
ghaymah deploy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Result
|
||||
|
||||
The CI/CD pipeline was successfully implemented.
|
||||
|
||||
Achievements:
|
||||
|
||||
- Docker image successfully built.
|
||||
- Image pushed to Docker Hub.
|
||||
- Manual approval configured.
|
||||
- Application deployed to AWS EC2.
|
||||
- Docker container started successfully.
|
||||
- Flask application became accessible.
|
||||
- Health checks passed.
|
||||
- Dashboard successfully displayed through the browser.
|
||||
|
||||
---
|
||||
|
||||
70
q3-cicd/app.py
Normal file
70
q3-cicd/app.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from flask import Flask, jsonify, render_template, request
|
||||
import time
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# Application start time
|
||||
start_time = time.time()
|
||||
|
||||
# Metrics
|
||||
request_count = 0
|
||||
last_response_time = 0
|
||||
|
||||
|
||||
@app.before_request
|
||||
def before_request():
|
||||
request.start_time = time.perf_counter()
|
||||
|
||||
|
||||
@app.after_request
|
||||
def after_request(response):
|
||||
global request_count
|
||||
global last_response_time
|
||||
|
||||
request_count += 1
|
||||
last_response_time = round(
|
||||
(time.perf_counter() - request.start_time) * 1000, 2
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def home():
|
||||
return render_template("dashboard.html")
|
||||
|
||||
|
||||
@app.route("/health")
|
||||
def health():
|
||||
return jsonify({
|
||||
"status": "UP",
|
||||
"message": "Application is healthy",
|
||||
"uptime_seconds": round(time.time() - start_time, 2),
|
||||
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"version": "1.0.0"
|
||||
}), 200
|
||||
|
||||
|
||||
@app.route("/metrics")
|
||||
def metrics():
|
||||
return jsonify({
|
||||
"status": "UP",
|
||||
"requests": request_count,
|
||||
"response_time_ms": last_response_time,
|
||||
"uptime_seconds": round(time.time() - start_time, 2)
|
||||
})
|
||||
|
||||
|
||||
@app.route("/api/info")
|
||||
def api_info():
|
||||
return jsonify({
|
||||
"application": "Ghaymah SRE API",
|
||||
"language": "Python",
|
||||
"framework": "Flask",
|
||||
"version": "1.0.0",
|
||||
"author": "Moustafa Medhat"
|
||||
})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8080)
|
||||
1
q3-cicd/requirements.txt
Normal file
1
q3-cicd/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
Flask==3.1.0
|
||||
151
q3-cicd/templates/dashboard.html
Normal file
151
q3-cicd/templates/dashboard.html
Normal file
@@ -0,0 +1,151 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ghaymah SRE Dashboard</title>
|
||||
|
||||
<style>
|
||||
*{
|
||||
margin:0;
|
||||
padding:0;
|
||||
box-sizing:border-box;
|
||||
font-family:Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
body{
|
||||
background:#f5f7fb;
|
||||
}
|
||||
|
||||
.container{
|
||||
width:900px;
|
||||
margin:40px auto;
|
||||
}
|
||||
|
||||
h1{
|
||||
text-align:center;
|
||||
margin-bottom:30px;
|
||||
color:#2c3e50;
|
||||
}
|
||||
|
||||
.cards{
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
gap:20px;
|
||||
}
|
||||
|
||||
.card{
|
||||
flex:1;
|
||||
background:white;
|
||||
border-radius:10px;
|
||||
padding:25px;
|
||||
text-align:center;
|
||||
box-shadow:0 5px 15px rgba(0,0,0,.1);
|
||||
}
|
||||
|
||||
.title{
|
||||
color:#666;
|
||||
font-size:18px;
|
||||
margin-bottom:15px;
|
||||
}
|
||||
|
||||
.value{
|
||||
font-size:34px;
|
||||
font-weight:bold;
|
||||
}
|
||||
|
||||
.up{
|
||||
color:green;
|
||||
}
|
||||
|
||||
.down{
|
||||
color:red;
|
||||
}
|
||||
|
||||
.footer{
|
||||
margin-top:35px;
|
||||
text-align:center;
|
||||
color:#777;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
|
||||
<h1>🚀 Ghaymah API Dashboard</h1>
|
||||
|
||||
<div class="cards">
|
||||
|
||||
<div class="card">
|
||||
<div class="title">Status</div>
|
||||
<div id="status" class="value">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="title">Response Time</div>
|
||||
<div id="response" class="value">0 ms</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="title">Requests</div>
|
||||
<div id="requests" class="value">0</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
Last Update:
|
||||
<span id="time">--</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
async function loadMetrics(){
|
||||
|
||||
try{
|
||||
|
||||
const response = await fetch('/metrics');
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const status=document.getElementById("status");
|
||||
|
||||
status.innerHTML=data.status;
|
||||
|
||||
status.className="value";
|
||||
|
||||
if(data.status==="UP")
|
||||
status.classList.add("up");
|
||||
else
|
||||
status.classList.add("down");
|
||||
|
||||
document.getElementById("response").innerHTML=data.response_time_ms+" ms";
|
||||
|
||||
document.getElementById("requests").innerHTML=data.requests;
|
||||
|
||||
document.getElementById("time").innerHTML=
|
||||
new Date().toLocaleTimeString();
|
||||
|
||||
}
|
||||
|
||||
catch(error){
|
||||
|
||||
document.getElementById("status").innerHTML="DOWN";
|
||||
|
||||
document.getElementById("status").className="value down";
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
loadMetrics();
|
||||
|
||||
setInterval(loadMetrics,30000);
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
59
q3-cicd/workflow.yml
Normal file
59
q3-cicd/workflow.yml
Normal file
@@ -0,0 +1,59 @@
|
||||
name: Build, Push and Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and Push Docker Image
|
||||
runs-on: self-hosted
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Build Docker Image
|
||||
run: |
|
||||
docker build -t ${{ secrets.DOCKER_USERNAME }}/ghaymah-api:${{ github.sha }} .
|
||||
|
||||
- name: Push Docker Image
|
||||
run: |
|
||||
docker push ${{ secrets.DOCKER_USERNAME }}/ghaymah-api:${{ github.sha }}
|
||||
|
||||
deploy-production:
|
||||
name: Deploy to Production
|
||||
needs: build
|
||||
runs-on: self-hosted
|
||||
|
||||
environment:
|
||||
name: production
|
||||
|
||||
steps:
|
||||
- name: Pull Latest Image
|
||||
run: |
|
||||
docker pull ${{ secrets.DOCKER_USERNAME }}/ghaymah-api:${{ github.sha }}
|
||||
|
||||
- name: Stop Existing Container
|
||||
run: |
|
||||
docker stop ghaymah-api || true
|
||||
docker rm ghaymah-api || true
|
||||
|
||||
- name: Run New Container
|
||||
run: |
|
||||
docker run -d \
|
||||
--name ghaymah-api \
|
||||
--restart unless-stopped \
|
||||
-p 8080:8080 \
|
||||
${{ secrets.DOCKER_USERNAME }}/ghaymah-api:${{ github.sha }}
|
||||
|
||||
- name: Verify Deployment
|
||||
run: |
|
||||
docker ps
|
||||
74
q4-scalability/calculations.md
Normal file
74
q4-scalability/calculations.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Scalability and Load Balancing
|
||||
|
||||
## 1. Container Calculation
|
||||
|
||||
### Given
|
||||
|
||||
- Expected traffic: **15,000 requests/second**
|
||||
- One container capacity: **500 requests/second**
|
||||
- Safety margin: **30%**
|
||||
|
||||
### Step 1: Base Number of Containers
|
||||
|
||||
15000 / 500 = **30 containers**
|
||||
|
||||
### Step 2: Add Safety Margin
|
||||
|
||||
30 × 1.30 = **39 containers**
|
||||
|
||||
### Final Result
|
||||
|
||||
**39 containers** are required to safely handle 15,000 requests per second while maintaining a 30% capacity margin for traffic spikes and failover scenarios.
|
||||
|
||||
---
|
||||
|
||||
# 2. Cold Start Strategy
|
||||
|
||||
To minimize startup latency for new containers:
|
||||
|
||||
- Keep a small pool of warm containers ready to receive traffic.
|
||||
- Use Kubernetes Horizontal Pod Autoscaler (HPA).
|
||||
- Pre-pull container images on worker nodes.
|
||||
- Configure Readiness Probes before accepting requests.
|
||||
- Use Rolling Updates for zero-downtime deployments.
|
||||
- Scale based on CPU utilization and request rate.
|
||||
|
||||
---
|
||||
|
||||
# 3. Ghaymah Block Storage for Stateful Workloads
|
||||
|
||||
Containers are ephemeral and lose local storage when restarted. Stateful workloads require persistent storage.
|
||||
|
||||
Ghaymah Block Storage provides persistent volumes that remain attached to applications even after pod or container recreation.
|
||||
|
||||
Typical stateful workloads include:
|
||||
|
||||
- PostgreSQL
|
||||
- MySQL
|
||||
- MongoDB
|
||||
- Redis Persistence
|
||||
- Elasticsearch
|
||||
- Jenkins
|
||||
- Kafka
|
||||
- RabbitMQ
|
||||
|
||||
Benefits:
|
||||
|
||||
- Persistent storage
|
||||
- High performance
|
||||
- Volume reattachment after restart
|
||||
- Snapshot support
|
||||
- Backup and disaster recovery
|
||||
- Independent lifecycle from containers
|
||||
|
||||
---
|
||||
|
||||
# Summary
|
||||
|
||||
| Requirement | Solution |
|
||||
|------------|----------|
|
||||
| Incoming Traffic | 15,000 req/s |
|
||||
| Container Capacity | 500 req/s |
|
||||
| Safety Margin | 30% |
|
||||
| Required Containers | **39 Containers** |
|
||||
| Cold Start | Warm Pool, HPA, Image Pre-Pulling, Readiness Probes |
|
||||
ثنائية
q4-scalability/scalable-architecture.png
Normal file
ثنائية
q4-scalability/scalable-architecture.png
Normal file
ملف ثنائي غير معروض.
|
بعد العرض: | الارتفاع: | الحجم: 1.3 MiB |
21
q5-mithal-monitor/Dockerfile
Normal file
21
q5-mithal-monitor/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
FROM python:3.11-slim AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install --no-cache-dir \
|
||||
--prefix=/install \
|
||||
-r requirements.txt
|
||||
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /install /usr/local
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["python3", "-m", "http.server", "8000", "--directory", "/app"]
|
||||
230
q5-mithal-monitor/app.js
Normal file
230
q5-mithal-monitor/app.js
Normal file
@@ -0,0 +1,230 @@
|
||||
let chart = null;
|
||||
|
||||
async function loadData() {
|
||||
|
||||
try {
|
||||
|
||||
const response = await fetch("../metrics.csv?t=" + Date.now());
|
||||
|
||||
const text = await response.text();
|
||||
|
||||
let rows = text.trim().split("\n");
|
||||
|
||||
if (rows.length <= 1) return;
|
||||
|
||||
// Remove CSV header
|
||||
rows.shift();
|
||||
|
||||
const data = rows
|
||||
.map(row => row.split(","))
|
||||
.filter(row => row.length >= 7);
|
||||
|
||||
const history = document.getElementById("history");
|
||||
history.innerHTML = "";
|
||||
|
||||
const now = new Date();
|
||||
|
||||
// ==========================
|
||||
// Uptime (24h)
|
||||
// ==========================
|
||||
|
||||
const last24 = data.filter(row => {
|
||||
|
||||
const t = new Date(row[0].replace(" ", "T"));
|
||||
|
||||
return (now - t) <= (24 * 60 * 60 * 1000);
|
||||
|
||||
});
|
||||
|
||||
const up = last24.filter(row => row[2] === "200").length;
|
||||
|
||||
const uptime =
|
||||
last24.length === 0
|
||||
? 0
|
||||
: ((up / last24.length) * 100).toFixed(2);
|
||||
|
||||
document.getElementById("uptime").textContent = uptime + "%";
|
||||
|
||||
// ==========================
|
||||
// SSL Status
|
||||
// ==========================
|
||||
|
||||
const latest = data[data.length - 1];
|
||||
|
||||
document.getElementById("ssl").textContent =
|
||||
latest[5] + " Days Remaining";
|
||||
|
||||
// ==========================
|
||||
// Chart
|
||||
// ==========================
|
||||
|
||||
let labels = [];
|
||||
let values = [];
|
||||
|
||||
data.forEach(row => {
|
||||
|
||||
const latency = parseFloat(row[1]);
|
||||
|
||||
// Ignore abnormal values
|
||||
if (latency <= 500) {
|
||||
|
||||
labels.push(row[0].split(" ")[1]);
|
||||
|
||||
values.push(latency);
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
drawChart(labels, values);
|
||||
|
||||
// ==========================
|
||||
// Last 10 Checks
|
||||
// ==========================
|
||||
|
||||
data.slice(-10).reverse().forEach(row => {
|
||||
|
||||
const status = row[2] === "200";
|
||||
|
||||
history.innerHTML += `
|
||||
|
||||
<tr>
|
||||
|
||||
<td>${row[0]}</td>
|
||||
|
||||
<td class="${status ? "up" : "down"}">
|
||||
|
||||
${status ? "UP" : "DOWN"}
|
||||
|
||||
</td>
|
||||
|
||||
<td>${row[1]} ms</td>
|
||||
|
||||
<td>${row[3]} ms</td>
|
||||
|
||||
<td>${row[6]} ms</td>
|
||||
|
||||
<td>${row[5]} Days</td>
|
||||
|
||||
</tr>
|
||||
|
||||
`;
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
catch (err) {
|
||||
|
||||
console.error("Dashboard Error:", err);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function drawChart(labels, values) {
|
||||
|
||||
const canvas = document.getElementById("latencyChart");
|
||||
|
||||
if (!canvas) return;
|
||||
|
||||
if (chart) {
|
||||
|
||||
chart.destroy();
|
||||
|
||||
}
|
||||
|
||||
chart = new Chart(canvas, {
|
||||
|
||||
type: "line",
|
||||
|
||||
data: {
|
||||
|
||||
labels: labels,
|
||||
|
||||
datasets: [
|
||||
|
||||
{
|
||||
|
||||
label: "Latency (ms)",
|
||||
|
||||
data: values,
|
||||
|
||||
borderColor: "#3498db",
|
||||
|
||||
backgroundColor: "rgba(52,152,219,0.15)",
|
||||
|
||||
borderWidth: 2,
|
||||
|
||||
fill: true,
|
||||
|
||||
tension: 0.3,
|
||||
|
||||
pointRadius: 3,
|
||||
|
||||
pointHoverRadius: 5
|
||||
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
},
|
||||
|
||||
options: {
|
||||
|
||||
responsive: true,
|
||||
|
||||
maintainAspectRatio: false,
|
||||
|
||||
animation: false,
|
||||
|
||||
plugins: {
|
||||
|
||||
legend: {
|
||||
|
||||
display: true
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
scales: {
|
||||
|
||||
y: {
|
||||
|
||||
min: 0,
|
||||
|
||||
max: 250,
|
||||
|
||||
title: {
|
||||
|
||||
display: true,
|
||||
|
||||
text: "Milliseconds"
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
x: {
|
||||
|
||||
title: {
|
||||
|
||||
display: true,
|
||||
|
||||
text: "Time"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
loadData();
|
||||
|
||||
84
q5-mithal-monitor/index.html
Normal file
84
q5-mithal-monitor/index.html
Normal file
@@ -0,0 +1,84 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>Mithal Monitoring Dashboard</title>
|
||||
|
||||
<link rel="stylesheet" href="style.css?v=3">
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
|
||||
<h1>Mithal Monitoring Dashboard</h1>
|
||||
|
||||
<div class="cards">
|
||||
|
||||
<div class="card">
|
||||
|
||||
<h3>Uptime (24h)</h3>
|
||||
|
||||
<h2 id="uptime">0%</h2>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
|
||||
<h3>SSL Status</h3>
|
||||
|
||||
<h2 id="ssl">Loading...</h2>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
|
||||
<canvas id="latencyChart"></canvas>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
|
||||
<h2>Last 10 Checks</h2>
|
||||
|
||||
<table>
|
||||
|
||||
<thead>
|
||||
|
||||
<tr>
|
||||
|
||||
<th>Time</th>
|
||||
<th>Status</th>
|
||||
<th>Latency</th>
|
||||
<th>DNS</th>
|
||||
<th>Search</th>
|
||||
<th>SSL Days</th>
|
||||
|
||||
</tr>
|
||||
|
||||
</thead>
|
||||
|
||||
<tbody id="history">
|
||||
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="app.js?v=3"></script>
|
||||
|
||||
</body>
|
||||
|
||||
53
q5-mithal-monitor/metrics.csv
Normal file
53
q5-mithal-monitor/metrics.csv
Normal file
@@ -0,0 +1,53 @@
|
||||
Timestamp,Latency(ms),Status,DNS(ms),SSL Expiry,Days Left,Search(ms)
|
||||
2026-07-29 12:29:42,86.56,200,1.24,2026-09-15,48,77.74
|
||||
2026-07-29 13:02:47,115.84,200,0.63,2026-09-15,48,76.28
|
||||
2026-07-29 13:03:48,77.67,200,0.35,2026-09-15,48,75.12
|
||||
2026-07-29 13:04:48,81.49,200,0.94,2026-09-15,48,83.27
|
||||
2026-07-29 13:05:48,76.87,200,0.88,2026-09-15,48,79.01
|
||||
2026-07-29 13:06:48,105.08,200,0.47,2026-09-15,48,75.93
|
||||
2026-07-29 13:07:49,91.23,200,0.39,2026-09-15,48,74.53
|
||||
2026-07-29 13:08:49,87.99,200,0.38,2026-09-15,48,86.44
|
||||
2026-07-29 13:09:49,78.44,200,0.6,2026-09-15,48,78.73
|
||||
2026-07-29 13:10:50,75.26,200,0.45,2026-09-15,47,75.22
|
||||
2026-07-29 13:11:50,78.65,200,0.4,2026-09-15,47,78.07
|
||||
2026-07-29 13:12:50,82.63,200,0.31,2026-09-15,47,98.73
|
||||
2026-07-29 13:13:50,76.69,200,0.38,2026-09-15,47,76.14
|
||||
2026-07-29 13:14:51,78.32,200,0.29,2026-09-15,47,79.26
|
||||
2026-07-29 13:15:51,78.01,200,0.34,2026-09-15,47,77.86
|
||||
2026-07-29 13:16:51,78.61,200,2.42,2026-09-15,47,75.91
|
||||
2026-07-29 13:17:51,112.39,200,0.39,2026-09-15,47,78.11
|
||||
2026-07-29 13:18:52,76.81,200,3.9,2026-09-15,47,79.26
|
||||
2026-07-29 13:19:52,76.79,200,0.29,2026-09-15,47,81.39
|
||||
2026-07-29 13:20:52,81.92,200,0.38,2026-09-15,47,81.31
|
||||
2026-07-29 13:21:52,79.67,200,0.39,2026-09-15,47,75.22
|
||||
2026-07-29 13:22:53,91.11,200,0.35,2026-09-15,47,77.56
|
||||
2026-07-29 13:23:53,85.6,200,4.12,2026-09-15,47,75.11
|
||||
2026-07-29 13:24:53,79.27,200,0.36,2026-09-15,47,80.63
|
||||
2026-07-29 13:25:53,77.88,200,0.32,2026-09-15,47,82.34
|
||||
2026-07-29 13:26:54,79.35,200,0.49,2026-09-15,47,74.78
|
||||
2026-07-29 13:27:54,96.65,200,0.37,2026-09-15,47,75.99
|
||||
2026-07-29 13:28:54,81.67,200,1.55,2026-09-15,47,72.96
|
||||
2026-07-29 13:29:54,96.08,200,4.82,2026-09-15,47,77.69
|
||||
2026-07-29 13:30:56,931.52,200,0.33,2026-09-15,47,83.28
|
||||
2026-07-29 13:31:56,80.81,200,1.11,2026-09-15,47,79.18
|
||||
2026-07-29 13:32:56,156.07,200,0.5,2026-09-15,47,77.07
|
||||
2026-07-29 13:35:19,84.33,200,1.81,2026-09-15,47,89.93
|
||||
2026-07-29 13:36:20,82.3,200,0.45,2026-09-15,47,76.14
|
||||
2026-07-29 13:37:20,81.27,200,0.38,2026-09-15,47,78.65
|
||||
2026-07-29 13:38:20,176.09,200,0.53,2026-09-15,47,102.99
|
||||
2026-07-29 13:46:29,92.47,200,0.31,2026-09-15,47,73.5
|
||||
2026-07-29 13:47:29,79.73,200,0.32,2026-09-15,47,79.24
|
||||
2026-07-29 13:48:29,76.52,200,0.64,2026-09-15,47,79.0
|
||||
2026-07-29 13:49:29,80.01,200,0.55,2026-09-15,47,78.53
|
||||
2026-07-29 13:50:30,78.15,200,1.62,2026-09-15,47,75.87
|
||||
2026-07-29 13:51:30,135.51,200,0.43,2026-09-15,47,79.1
|
||||
2026-07-29 13:52:30,75.61,200,0.36,2026-09-15,47,77.5
|
||||
2026-07-29 13:53:31,77.33,200,0.38,2026-09-15,47,120.55
|
||||
2026-07-29 13:54:31,77.05,200,0.57,2026-09-15,47,77.96
|
||||
2026-07-29 13:55:31,78.7,200,0.4,2026-09-15,47,72.49
|
||||
2026-07-29 13:56:31,100.53,200,0.36,2026-09-15,47,76.88
|
||||
2026-07-29 13:57:32,82.96,200,0.38,2026-09-15,47,86.56
|
||||
2026-07-29 13:58:32,96.16,200,0.45,2026-09-15,47,74.9
|
||||
2026-07-29 13:59:32,79.9,200,0.32,2026-09-15,47,74.98
|
||||
2026-07-29 14:00:32,80.24,200,1.81,2026-09-15,47,73.09
|
||||
2026-07-29 14:01:33,130.87,200,0.35,2026-09-15,47,76.39
|
||||
|
142
q5-mithal-monitor/monitor.py
Normal file
142
q5-mithal-monitor/monitor.py
Normal file
@@ -0,0 +1,142 @@
|
||||
import requests
|
||||
import socket
|
||||
import ssl
|
||||
import time
|
||||
import csv
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
HOST = "mithal.space"
|
||||
URL = f"https://{HOST}"
|
||||
SEARCH_URL = f"{URL}/?q=test"
|
||||
|
||||
CSV_FILE = "metrics.csv"
|
||||
|
||||
|
||||
def check_latency():
|
||||
start = time.time()
|
||||
|
||||
response = requests.get(URL, timeout=10)
|
||||
|
||||
latency = round((time.time() - start) * 1000, 2)
|
||||
|
||||
return latency, response.status_code
|
||||
|
||||
|
||||
def check_dns():
|
||||
|
||||
start = time.time()
|
||||
|
||||
socket.gethostbyname(HOST)
|
||||
|
||||
dns_time = round((time.time() - start) * 1000, 2)
|
||||
|
||||
return dns_time
|
||||
|
||||
|
||||
def check_ssl():
|
||||
|
||||
context = ssl.create_default_context()
|
||||
|
||||
with context.wrap_socket(
|
||||
socket.socket(),
|
||||
server_hostname=HOST
|
||||
) as s:
|
||||
|
||||
s.settimeout(10)
|
||||
|
||||
s.connect((HOST, 443))
|
||||
|
||||
cert = s.getpeercert()
|
||||
|
||||
expire = cert["notAfter"]
|
||||
|
||||
expire_date = datetime.strptime(
|
||||
expire,
|
||||
"%b %d %H:%M:%S %Y %Z"
|
||||
)
|
||||
|
||||
remaining = (expire_date - datetime.utcnow()).days
|
||||
|
||||
return expire_date.strftime("%Y-%m-%d"), remaining
|
||||
|
||||
|
||||
def check_search():
|
||||
|
||||
start = time.time()
|
||||
|
||||
requests.get(SEARCH_URL, timeout=10)
|
||||
|
||||
search_latency = round(
|
||||
(time.time() - start) * 1000,
|
||||
2
|
||||
)
|
||||
|
||||
return search_latency
|
||||
|
||||
|
||||
def save_csv(data):
|
||||
|
||||
file_exists = os.path.isfile(CSV_FILE)
|
||||
|
||||
with open(CSV_FILE, "a", newline="") as file:
|
||||
|
||||
writer = csv.writer(file)
|
||||
|
||||
if not file_exists:
|
||||
|
||||
writer.writerow([
|
||||
"Timestamp",
|
||||
"Latency(ms)",
|
||||
"Status",
|
||||
"DNS(ms)",
|
||||
"SSL Expiry",
|
||||
"Days Left",
|
||||
"Search(ms)"
|
||||
])
|
||||
|
||||
writer.writerow(data)
|
||||
|
||||
|
||||
def monitor():
|
||||
|
||||
while True:
|
||||
|
||||
try:
|
||||
|
||||
latency, status = check_latency()
|
||||
|
||||
dns = check_dns()
|
||||
|
||||
ssl_date, ssl_days = check_ssl()
|
||||
|
||||
search = check_search()
|
||||
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
row = [
|
||||
now,
|
||||
latency,
|
||||
status,
|
||||
dns,
|
||||
ssl_date,
|
||||
ssl_days,
|
||||
search
|
||||
]
|
||||
|
||||
save_csv(row)
|
||||
|
||||
print(row)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
print("Monitoring Error:", e)
|
||||
|
||||
print("Waiting 60 seconds...\n")
|
||||
|
||||
time.sleep(60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
monitor()
|
||||
|
||||
59
q5-mithal-monitor/monitoring-workflow.yml
Normal file
59
q5-mithal-monitor/monitoring-workflow.yml
Normal file
@@ -0,0 +1,59 @@
|
||||
name: Build, Push and Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and Push Docker Image
|
||||
runs-on: self-hosted
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Build Docker Image
|
||||
run: |
|
||||
docker build -t ${{ secrets.DOCKER_USERNAME }}/mithal-monitor:${{ github.sha }} .
|
||||
|
||||
- name: Push Docker Image
|
||||
run: |
|
||||
docker push ${{ secrets.DOCKER_USERNAME }}/mithal-monitor:${{ github.sha }}
|
||||
|
||||
deploy-production:
|
||||
name: Deploy to Production
|
||||
needs: build
|
||||
runs-on: self-hosted
|
||||
|
||||
environment:
|
||||
name: production
|
||||
|
||||
steps:
|
||||
- name: Pull Latest Image
|
||||
run: |
|
||||
docker pull ${{ secrets.DOCKER_USERNAME }}/mithal-monitor:${{ github.sha }}
|
||||
|
||||
- name: Stop Existing Container
|
||||
run: |
|
||||
docker stop mithal-monitor || true
|
||||
docker rm mithal-monitor || true
|
||||
|
||||
- name: Run New Container
|
||||
run: |
|
||||
docker run -d \
|
||||
--name mithal-monitor \
|
||||
--restart unless-stopped \
|
||||
-p 8000:8000 \
|
||||
${{ secrets.DOCKER_USERNAME }}/mithal-monitor:${{ github.sha }}
|
||||
|
||||
- name: Verify Deployment
|
||||
run: |
|
||||
docker ps
|
||||
3
q5-mithal-monitor/requirements.txt
Normal file
3
q5-mithal-monitor/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
requests
|
||||
pandas
|
||||
dnspython
|
||||
121
q5-mithal-monitor/style.css
Normal file
121
q5-mithal-monitor/style.css
Normal file
@@ -0,0 +1,121 @@
|
||||
*{
|
||||
margin:0;
|
||||
padding:0;
|
||||
box-sizing:border-box;
|
||||
font-family:Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
body{
|
||||
background:#f4f7fb;
|
||||
color:#333;
|
||||
}
|
||||
|
||||
.container{
|
||||
width:95%;
|
||||
max-width:1200px;
|
||||
margin:30px auto;
|
||||
}
|
||||
|
||||
h1{
|
||||
text-align:center;
|
||||
margin-bottom:30px;
|
||||
color:#2c3e50;
|
||||
}
|
||||
|
||||
.cards{
|
||||
display:flex;
|
||||
gap:20px;
|
||||
justify-content:center;
|
||||
margin-bottom:30px;
|
||||
flex-wrap:wrap;
|
||||
}
|
||||
|
||||
.card{
|
||||
background:#fff;
|
||||
width:280px;
|
||||
padding:25px;
|
||||
border-radius:12px;
|
||||
text-align:center;
|
||||
box-shadow:0 5px 15px rgba(0,0,0,.08);
|
||||
}
|
||||
|
||||
.card h3{
|
||||
color:#666;
|
||||
margin-bottom:15px;
|
||||
}
|
||||
|
||||
.card h2{
|
||||
color:#2c3e50;
|
||||
font-size:32px;
|
||||
}
|
||||
|
||||
.chart-container{
|
||||
background:#fff;
|
||||
padding:20px;
|
||||
border-radius:12px;
|
||||
box-shadow:0 5px 15px rgba(0,0,0,.08);
|
||||
margin-bottom:30px;
|
||||
height:450px;
|
||||
}
|
||||
|
||||
.table-container{
|
||||
background:#fff;
|
||||
padding:20px;
|
||||
border-radius:12px;
|
||||
box-shadow:0 5px 15px rgba(0,0,0,.08);
|
||||
}
|
||||
|
||||
.table-container h2{
|
||||
margin-bottom:20px;
|
||||
}
|
||||
|
||||
table{
|
||||
width:100%;
|
||||
border-collapse:collapse;
|
||||
}
|
||||
|
||||
th{
|
||||
background:#3498db;
|
||||
color:white;
|
||||
padding:12px;
|
||||
}
|
||||
|
||||
td{
|
||||
padding:12px;
|
||||
border-bottom:1px solid #ddd;
|
||||
text-align:center;
|
||||
}
|
||||
|
||||
tr:hover{
|
||||
background:#f8f8f8;
|
||||
}
|
||||
|
||||
.up{
|
||||
color:green;
|
||||
font-weight:bold;
|
||||
}
|
||||
|
||||
.down{
|
||||
color:red;
|
||||
font-weight:bold;
|
||||
}
|
||||
|
||||
canvas{
|
||||
width:100% !important;
|
||||
height:100% !important;
|
||||
}
|
||||
|
||||
@media(max-width:768px){
|
||||
|
||||
.cards{
|
||||
flex-direction:column;
|
||||
align-items:center;
|
||||
}
|
||||
|
||||
table{
|
||||
font-size:14px;
|
||||
}
|
||||
|
||||
.chart-container{
|
||||
height:350px;
|
||||
}
|
||||
المرجع في مشكلة جديدة
حظر مستخدم