diff --git a/Q1/.dockerignore b/Q1/.dockerignore new file mode 100644 index 0000000..4adcd8c --- /dev/null +++ b/Q1/.dockerignore @@ -0,0 +1,6 @@ +data/ +*.pyc +__pycache__/ +.git +.gitignore +README.md diff --git a/Q1/Dockerfile b/Q1/Dockerfile new file mode 100644 index 0000000..6e3252e --- /dev/null +++ b/Q1/Dockerfile @@ -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"] \ No newline at end of file diff --git a/Q1/index.html b/Q1/index.html new file mode 100644 index 0000000..ab0818e --- /dev/null +++ b/Q1/index.html @@ -0,0 +1,60 @@ + + + + + + لوحة مراقبة الـ API + + + + +
+

مراقبة النظام

+ +
+ الحالة: جاري التحميل... +
+
+ زمن الاستجابة: 0 ms +
+
+ عدد الطلبات الإجمالي: 0 +
+
+ + + + \ No newline at end of file diff --git a/Q1/monitor.sh b/Q1/monitor.sh new file mode 100755 index 0000000..ad9eb65 --- /dev/null +++ b/Q1/monitor.sh @@ -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 \ No newline at end of file diff --git a/Q1/server.js b/Q1/server.js new file mode 100644 index 0000000..5d007da --- /dev/null +++ b/Q1/server.js @@ -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}`); +}); \ No newline at end of file diff --git a/Q2/readme,md b/Q2/readme,md new file mode 100644 index 0000000..33863e4 --- /dev/null +++ b/Q2/readme,md @@ -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 | +``` \ No newline at end of file diff --git a/Q3-CICD/README.md b/Q3-CICD/README.md new file mode 100644 index 0000000..3e9c476 --- /dev/null +++ b/Q3-CICD/README.md @@ -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 | \ No newline at end of file diff --git a/Q3-CICD/workflow.yml b/Q3-CICD/workflow.yml new file mode 100644 index 0000000..1c616b2 --- /dev/null +++ b/Q3-CICD/workflow.yml @@ -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 \ No newline at end of file diff --git a/Q4/GhaymahAPI.png b/Q4/GhaymahAPI.png new file mode 100644 index 0000000..d7a40dd Binary files /dev/null and b/Q4/GhaymahAPI.png differ diff --git a/Q4/README.md b/Q4/README.md new file mode 100644 index 0000000..9977b4b --- /dev/null +++ b/Q4/README.md @@ -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. + +![Architecture Diagram](./GhaymahAPI.png) + +--- + +# 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. \ No newline at end of file diff --git a/Q5/.dockerignore b/Q5/.dockerignore new file mode 100644 index 0000000..4adcd8c --- /dev/null +++ b/Q5/.dockerignore @@ -0,0 +1,6 @@ +data/ +*.pyc +__pycache__/ +.git +.gitignore +README.md diff --git a/Q5/Dockerfile b/Q5/Dockerfile new file mode 100644 index 0000000..e19b0c2 --- /dev/null +++ b/Q5/Dockerfile @@ -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"] \ No newline at end of file diff --git a/Q5/Entrypoint.sh b/Q5/Entrypoint.sh new file mode 100644 index 0000000..79605aa --- /dev/null +++ b/Q5/Entrypoint.sh @@ -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 \ No newline at end of file diff --git a/Q5/README.md b/Q5/README.md new file mode 100644 index 0000000..1d29524 --- /dev/null +++ b/Q5/README.md @@ -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. \ No newline at end of file diff --git a/Q5/index.html b/Q5/index.html new file mode 100644 index 0000000..40f6ccf --- /dev/null +++ b/Q5/index.html @@ -0,0 +1,395 @@ + + + + + +Site Watch — Status + + + + + + +
+ +
+
+
+ Uptime & Performance + +
+
+ + CHECKING… +
+
+ + + + + +
+ Checks every + Window + Last check +
+
+ +
+
+ Uptime · 24h + + — checks recorded +
+
+ Latency · latest + + HTTP response time +
+
+ DNS resolve + + Latest lookup time +
+
+ SSL expires in + + Certificate validity +
+
+ +
+
+

Latency — last 60 minutes

+ site vs. search endpoint, ms +
+
+
+ +
+
+

Recent checks

+ last 10 +
+ + + + + + + + + + + + + + +
Time (UTC)StatusLatencyDNSSearchSSL days
Waiting for first data…
+
+ + +
+ + + + \ No newline at end of file diff --git a/Q5/monitor.py b/Q5/monitor.py new file mode 100644 index 0000000..3cfcea2 --- /dev/null +++ b/Q5/monitor.py @@ -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) \ No newline at end of file