diff --git a/common-qabilah/qabilah-profile.txt b/common-qabilah/qabilah-profile.txt new file mode 100644 index 0000000..8ef5283 --- /dev/null +++ b/common-qabilah/qabilah-profile.txt @@ -0,0 +1 @@ +https://qabilah.com/profile/moustafamedhet97/posts diff --git a/q1-deploy-monitor/Dockerfile b/q1-deploy-monitor/Dockerfile new file mode 100644 index 0000000..06f1b97 --- /dev/null +++ b/q1-deploy-monitor/Dockerfile @@ -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"] diff --git a/q1-deploy-monitor/app.py b/q1-deploy-monitor/app.py new file mode 100644 index 0000000..59c70c1 --- /dev/null +++ b/q1-deploy-monitor/app.py @@ -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) diff --git a/q1-deploy-monitor/health-check.sh b/q1-deploy-monitor/health-check.sh new file mode 100755 index 0000000..d9af605 --- /dev/null +++ b/q1-deploy-monitor/health-check.sh @@ -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 diff --git a/q1-deploy-monitor/health.log b/q1-deploy-monitor/health.log new file mode 100644 index 0000000..ccb5724 --- /dev/null +++ b/q1-deploy-monitor/health.log @@ -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 diff --git a/q1-deploy-monitor/requirements.txt b/q1-deploy-monitor/requirements.txt new file mode 100644 index 0000000..22ac75b --- /dev/null +++ b/q1-deploy-monitor/requirements.txt @@ -0,0 +1 @@ +Flask==3.1.0 diff --git a/q1-deploy-monitor/templates/dashboard.html b/q1-deploy-monitor/templates/dashboard.html new file mode 100644 index 0000000..d8a15a1 --- /dev/null +++ b/q1-deploy-monitor/templates/dashboard.html @@ -0,0 +1,151 @@ + + + + + + Ghaymah API Dashboard + + + + + + +
+ +

🚀 Ghaymah SRE Dashboard

+ +
+ +
+
Status
+
Loading...
+
+ +
+
Response Time
+
0 ms
+
+ +
+
Requests
+
0
+
+ +
+ + + +
+ + + + diff --git a/q2-postmortem/postmortem-report.md b/q2-postmortem/postmortem-report.md new file mode 100644 index 0000000..9613511 --- /dev/null +++ b/q2-postmortem/postmortem-report.md @@ -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. diff --git a/q3-cicd/Dockerfile b/q3-cicd/Dockerfile new file mode 100644 index 0000000..06f1b97 --- /dev/null +++ b/q3-cicd/Dockerfile @@ -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"] diff --git a/q3-cicd/README.md b/q3-cicd/README.md new file mode 100644 index 0000000..faf4408 --- /dev/null +++ b/q3-cicd/README.md @@ -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://: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://: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. + +--- + diff --git a/q3-cicd/app.py b/q3-cicd/app.py new file mode 100644 index 0000000..59c70c1 --- /dev/null +++ b/q3-cicd/app.py @@ -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) diff --git a/q3-cicd/requirements.txt b/q3-cicd/requirements.txt new file mode 100644 index 0000000..22ac75b --- /dev/null +++ b/q3-cicd/requirements.txt @@ -0,0 +1 @@ +Flask==3.1.0 diff --git a/q3-cicd/templates/dashboard.html b/q3-cicd/templates/dashboard.html new file mode 100644 index 0000000..d230d4f --- /dev/null +++ b/q3-cicd/templates/dashboard.html @@ -0,0 +1,151 @@ + + + + + + Ghaymah SRE Dashboard + + + + + + +
+ +

🚀 Ghaymah API Dashboard

+ +
+ +
+
Status
+
Loading...
+
+ +
+
Response Time
+
0 ms
+
+ +
+
Requests
+
0
+
+ +
+ + + +
+ + + + diff --git a/q3-cicd/workflow.yml b/q3-cicd/workflow.yml new file mode 100644 index 0000000..2da6652 --- /dev/null +++ b/q3-cicd/workflow.yml @@ -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 diff --git a/q4-scalability/calculations.md b/q4-scalability/calculations.md new file mode 100644 index 0000000..38e876f --- /dev/null +++ b/q4-scalability/calculations.md @@ -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 | diff --git a/q4-scalability/scalable-architecture.png b/q4-scalability/scalable-architecture.png new file mode 100644 index 0000000..f3e6359 Binary files /dev/null and b/q4-scalability/scalable-architecture.png differ diff --git a/q5-mithal-monitor/Dockerfile b/q5-mithal-monitor/Dockerfile new file mode 100644 index 0000000..108b5cf --- /dev/null +++ b/q5-mithal-monitor/Dockerfile @@ -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"] diff --git a/q5-mithal-monitor/app.js b/q5-mithal-monitor/app.js new file mode 100644 index 0000000..21b1cdf --- /dev/null +++ b/q5-mithal-monitor/app.js @@ -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 += ` + + + + ${row[0]} + + + + ${status ? "UP" : "DOWN"} + + + + ${row[1]} ms + + ${row[3]} ms + + ${row[6]} ms + + ${row[5]} Days + + + + `; + + }); + + } + + 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(); + diff --git a/q5-mithal-monitor/index.html b/q5-mithal-monitor/index.html new file mode 100644 index 0000000..686788e --- /dev/null +++ b/q5-mithal-monitor/index.html @@ -0,0 +1,84 @@ + + + + + + + + + + Mithal Monitoring Dashboard + + + + + + + + + +
+ +

Mithal Monitoring Dashboard

+ +
+ +
+ +

Uptime (24h)

+ +

0%

+ +
+ +
+ +

SSL Status

+ +

Loading...

+ +
+ +
+ +
+ + + +
+ +
+ +

Last 10 Checks

+ + + + + + + + + + + + + + + + + + + + + + +
TimeStatusLatencyDNSSearchSSL Days
+ +
+ +
+ + + + + diff --git a/q5-mithal-monitor/metrics.csv b/q5-mithal-monitor/metrics.csv new file mode 100644 index 0000000..f30cbd0 --- /dev/null +++ b/q5-mithal-monitor/metrics.csv @@ -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 diff --git a/q5-mithal-monitor/monitor.py b/q5-mithal-monitor/monitor.py new file mode 100644 index 0000000..86519e4 --- /dev/null +++ b/q5-mithal-monitor/monitor.py @@ -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() + diff --git a/q5-mithal-monitor/monitoring-workflow.yml b/q5-mithal-monitor/monitoring-workflow.yml new file mode 100644 index 0000000..2833895 --- /dev/null +++ b/q5-mithal-monitor/monitoring-workflow.yml @@ -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 diff --git a/q5-mithal-monitor/requirements.txt b/q5-mithal-monitor/requirements.txt new file mode 100644 index 0000000..c966d64 --- /dev/null +++ b/q5-mithal-monitor/requirements.txt @@ -0,0 +1,3 @@ +requests +pandas +dnspython diff --git a/q5-mithal-monitor/style.css b/q5-mithal-monitor/style.css new file mode 100644 index 0000000..a214e42 --- /dev/null +++ b/q5-mithal-monitor/style.css @@ -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; + }