commit 7827c9aee2a6e923612f0ab3f6cc6a1b2af67dac Author: Ubuntu Date: Mon Jul 27 10:35:23 2026 +0000 Q1: deploy and monitor app diff --git a/@ b/@ new file mode 100644 index 0000000..5fd9d15 --- /dev/null +++ b/@ @@ -0,0 +1,51 @@ +""" +Simple API app for Ghaymah SRE exam - Q1 +Provides: + GET / -> basic info + GET /health -> health check endpoint (used by monitoring) + GET /metrics -> simple JSON metrics (request count, uptime) +""" +import time +from flask import Flask, jsonify + +app = Flask(__name__) + +START_TIME = time.time() +REQUEST_COUNT = 0 + + +@app.before_request +def count_requests(): + global REQUEST_COUNT + REQUEST_COUNT += 1 + + +@app.route("/") +def index(): + return jsonify({ + "service": "ghaymah-exam-api", + "message": "API is running" + }) + + +@app.route("/health") +def health(): + """Used by ghaymah.systems platform + our monitoring script.""" + return jsonify({ + "status": "healthy", + "uptime_seconds": round(time.time() - START_TIME, 2) + }), 200 + + +@app.route("/metrics") +def metrics(): + return jsonify({ + "uptime_seconds": round(time.time() - START_TIME, 2), + "total_requests": REQUEST_COUNT + }) + + +if __name__ == "__main__": + # 0.0.0.0 required so the container's port is reachable externally + app.run(host="0.0.0.0", port=5000) + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0852b46 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM python:3.12-slim + +WORKDIR /app + +# Install dependencies first (layer caching) +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy app code +COPY app.py . + +EXPOSE 5000 + +# Basic container-level health check (Docker/most platforms respect this) +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:5000/health')" || exit 1 + +# gunicorn for production-grade serving instead of Flask dev server +CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "app:app"] + diff --git a/app.py b/app.py new file mode 100644 index 0000000..c6bba4b --- /dev/null +++ b/app.py @@ -0,0 +1,60 @@ +""" +Simple API app for Ghaymah SRE exam - Q1 +Provides: + GET / -> basic info + GET /health -> health check endpoint (used by monitoring) + GET /metrics -> simple JSON metrics (request count, uptime) +""" +import time +from flask import Flask, jsonify + +app = Flask(__name__) + + +@app.after_request +def add_cors_headers(response): + # Allows the dashboard (served from a different origin, e.g. file:// + # or another host) to call this API from the browser. + response.headers["Access-Control-Allow-Origin"] = "*" + response.headers["Access-Control-Allow-Methods"] = "GET, OPTIONS" + response.headers["Access-Control-Allow-Headers"] = "Content-Type" + return response + +START_TIME = time.time() +REQUEST_COUNT = 0 + + +@app.before_request +def count_requests(): + global REQUEST_COUNT + REQUEST_COUNT += 1 + + +@app.route("/") +def index(): + return jsonify({ + "service": "ghaymah-exam-api", + "message": "API is running" + }) + + +@app.route("/health") +def health(): + """Used by ghaymah.systems platform + our monitoring script.""" + return jsonify({ + "status": "healthy", + "uptime_seconds": round(time.time() - START_TIME, 2) + }), 200 + + +@app.route("/metrics") +def metrics(): + return jsonify({ + "uptime_seconds": round(time.time() - START_TIME, 2), + "total_requests": REQUEST_COUNT + }) + + +if __name__ == "__main__": + # 0.0.0.0 required so the container's port is reachable externally + app.run(host="0.0.0.0", port=5000) diff --git a/dashboard.html b/dashboard.html new file mode 100644 index 0000000..a8857f3 --- /dev/null +++ b/dashboard.html @@ -0,0 +1,211 @@ + + + + + +Monitoring Dashboard - Ghaymah Exam Q1 + + + + +

لوحة مراقبة التطبيق

+
Ghaymah SRE Exam — Q1 Monitoring Dashboard
+ +
+ + +
+ +
+
+
الحالة (Status)
+
+
+
+
زمن الاستجابة (Response Time)
+
— ms
+
+
+
عدد الطلبات (Total Requests)
+
+
+
+
وقت التشغيل (Uptime)
+
+
+
+ +
+ + + + + +
الوقتالحالةزمن الاستجابة
+
+ + + + + + diff --git a/health-check.py b/health-check.py new file mode 100644 index 0000000..1af2d05 --- /dev/null +++ b/health-check.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +""" +Monitoring script for Q1 - Ghaymah SRE exam. +Checks the deployed app's /health endpoint every 30 seconds, +logs status + response time to a CSV file, and prints live status +to the console. + +Usage: + python3 health-check.py https://your-app-url.ghaymah.systems +""" +import sys +import time +import csv +import os +from datetime import datetime, timezone +import urllib.request +import urllib.error + +CHECK_INTERVAL_SECONDS = 30 +LOG_FILE = "monitor-log.csv" + + +def check_health(url: str) -> dict: + endpoint = url.rstrip("/") + "/health" + start = time.time() + try: + with urllib.request.urlopen(endpoint, timeout=10) as response: + elapsed_ms = round((time.time() - start) * 1000, 2) + status_code = response.getcode() + return { + "timestamp": datetime.now(timezone.utc).isoformat(), + "status": "UP" if status_code == 200 else "DEGRADED", + "status_code": status_code, + "response_time_ms": elapsed_ms, + } + except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError) as e: + elapsed_ms = round((time.time() - start) * 1000, 2) + return { + "timestamp": datetime.now(timezone.utc).isoformat(), + "status": "DOWN", + "status_code": None, + "response_time_ms": elapsed_ms, + "error": str(e), + } + + +def log_result(result: dict): + file_exists = os.path.isfile(LOG_FILE) + with open(LOG_FILE, "a", newline="") as f: + fieldnames = ["timestamp", "status", "status_code", "response_time_ms", "error"] + writer = csv.DictWriter(f, fieldnames=fieldnames) + if not file_exists: + writer.writeheader() + writer.writerow({**{"error": ""}, **result}) + + +def main(): + if len(sys.argv) < 2: + print("Usage: python3 health-check.py ") + sys.exit(1) + + url = sys.argv[1] + print(f"Monitoring {url}/health every {CHECK_INTERVAL_SECONDS}s. Logging to {LOG_FILE}. Ctrl+C to stop.") + + try: + while True: + result = check_health(url) + log_result(result) + print(f"[{result['timestamp']}] {result['status']} " + f"({result['response_time_ms']}ms)" + + (f" - {result.get('error')}" if result.get("error") else "")) + time.sleep(CHECK_INTERVAL_SECONDS) + except KeyboardInterrupt: + print("\nMonitoring stopped.") + + +if __name__ == "__main__": + main() + diff --git a/install-docker.sh b/install-docker.sh new file mode 100755 index 0000000..777f5fc --- /dev/null +++ b/install-docker.sh @@ -0,0 +1,36 @@ +#!/bin/bash +set -e + +echo ">>> Updating packages..." +sudo apt update + +echo ">>> Installing prerequisites..." +sudo apt install -y ca-certificates curl gnupg + +echo ">>> Setting up Docker keyring..." +sudo install -m 0755 -d /etc/apt/keyrings +curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg +sudo chmod a+r /etc/apt/keyrings/docker.gpg + +echo ">>> Adding Docker repository..." +echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | \ + sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + +echo ">>> Installing Docker..." +sudo apt update +sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + +echo ">>> Starting Docker service..." +sudo systemctl start docker +sudo systemctl enable docker + +echo ">>> Adding current user to docker group..." +sudo usermod -aG docker $USER + +echo "" +echo "==========================================" +echo "Docker installed successfully." +echo "IMPORTANT: run 'newgrp docker' OR log out/in" +echo "then run: docker --version" +echo "==========================================" diff --git a/monitor-log.csv b/monitor-log.csv new file mode 100644 index 0000000..b63067b --- /dev/null +++ b/monitor-log.csv @@ -0,0 +1,35 @@ +timestamp,status,status_code,response_time_ms,error +2026-07-27T10:17:38.379006+00:00,UP,200,14.79, +2026-07-27T10:18:08.381609+00:00,UP,200,2.08, +2026-07-27T10:18:38.383866+00:00,UP,200,1.82, +2026-07-27T10:19:08.386140+00:00,UP,200,1.77, +2026-07-27T10:19:38.388582+00:00,UP,200,2.01, +2026-07-27T10:20:08.390836+00:00,UP,200,1.85, +2026-07-27T10:20:38.394181+00:00,UP,200,2.97, +2026-07-27T10:21:08.396674+00:00,UP,200,1.98, +2026-07-27T10:21:38.398784+00:00,UP,200,1.7, +2026-07-27T10:22:08.401664+00:00,UP,200,1.84, +2026-07-27T10:22:38.403790+00:00,UP,200,1.71, +2026-07-27T10:23:08.406096+00:00,UP,200,1.78, +2026-07-27T10:23:38.407503+00:00,DOWN,,0.43, +2026-07-27T10:24:08.408384+00:00,DOWN,,0.54, +2026-07-27T10:24:38.409097+00:00,DOWN,,0.38, +2026-07-27T10:25:08.409873+00:00,DOWN,,0.4, +2026-07-27T10:25:38.412335+00:00,UP,200,2.12, +2026-07-27T10:26:08.414479+00:00,UP,200,1.78, +2026-07-27T10:26:38.416810+00:00,UP,200,1.92, +2026-07-27T10:27:08.418967+00:00,UP,200,1.74, +2026-07-27T10:27:38.421130+00:00,UP,200,1.68, +2026-07-27T10:28:08.423401+00:00,UP,200,1.76, +2026-07-27T10:28:38.425814+00:00,UP,200,1.99, +2026-07-27T10:29:08.428129+00:00,UP,200,1.76, +2026-07-27T10:29:38.430424+00:00,UP,200,1.85, +2026-07-27T10:30:08.432756+00:00,UP,200,1.87, +2026-07-27T10:30:38.435351+00:00,UP,200,1.77, +2026-07-27T10:31:08.437531+00:00,UP,200,1.78, +2026-07-27T10:31:38.439587+00:00,UP,200,1.69, +2026-07-27T10:32:08.441908+00:00,UP,200,1.94, +2026-07-27T10:32:38.443987+00:00,UP,200,1.68, +2026-07-27T10:33:08.446193+00:00,UP,200,1.75, +2026-07-27T10:33:38.448817+00:00,UP,200,1.85, +2026-07-27T10:34:08.451690+00:00,UP,200,1.73, diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b25aa02 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +flask==3.0.3 +gunicorn==22.0.0 +