From a1973b97ff2e097512a4ed3b6e2bf4192afc0d04 Mon Sep 17 00:00:00 2001 From: rezayaghobi-dev Date: Sat, 3 Oct 2026 19:08:48 +0330 Subject: [PATCH] Add FastAPI service, Dockerfile and compose --- webapp/.dockerignore | 4 ++++ webapp/.gitignore | 4 ++++ webapp/Dockerfile | 18 +++++++++++++++ webapp/app/__init__.py | 0 webapp/app/main.py | 44 +++++++++++++++++++++++++++++++++++++ webapp/compose.yml | 14 ++++++++++++ webapp/pytest.ini | 2 ++ webapp/requirements-dev.txt | 3 +++ webapp/requirements.txt | 3 +++ webapp/tests/test_app.py | 27 +++++++++++++++++++++++ 10 files changed, 119 insertions(+) create mode 100644 webapp/.dockerignore create mode 100644 webapp/.gitignore create mode 100644 webapp/Dockerfile create mode 100644 webapp/app/__init__.py create mode 100644 webapp/app/main.py create mode 100644 webapp/compose.yml create mode 100644 webapp/pytest.ini create mode 100644 webapp/requirements-dev.txt create mode 100644 webapp/requirements.txt create mode 100644 webapp/tests/test_app.py diff --git a/webapp/.dockerignore b/webapp/.dockerignore new file mode 100644 index 0000000..17cd2fc --- /dev/null +++ b/webapp/.dockerignore @@ -0,0 +1,4 @@ +.venv/ +__pycache__/ +*.pyc +.pytest_cache/ diff --git a/webapp/.gitignore b/webapp/.gitignore new file mode 100644 index 0000000..17cd2fc --- /dev/null +++ b/webapp/.gitignore @@ -0,0 +1,4 @@ +.venv/ +__pycache__/ +*.pyc +.pytest_cache/ diff --git a/webapp/Dockerfile b/webapp/Dockerfile new file mode 100644 index 0000000..bd62698 --- /dev/null +++ b/webapp/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY app ./app + +RUN useradd --system --uid 1001 appuser +USER appuser + +EXPOSE 8000 +HEALTHCHECK --interval=15s --timeout=3s --retries=3 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/webapp/app/__init__.py b/webapp/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/webapp/app/main.py b/webapp/app/main.py new file mode 100644 index 0000000..94b4b5d --- /dev/null +++ b/webapp/app/main.py @@ -0,0 +1,44 @@ +import time + +from fastapi import FastAPI, HTTPException, Request, Response +from prometheus_client import CONTENT_TYPE_LATEST, Counter, Histogram, generate_latest + +app = FastAPI(title="DevOps Test Service") + +REQUESTS = Counter( + "http_requests_total", "Total HTTP requests", ["method", "path", "status"] +) +LATENCY = Histogram( + "http_request_duration_seconds", "Request latency in seconds", ["path"] +) + + +@app.middleware("http") +async def track_metrics(request: Request, call_next): + start = time.perf_counter() + response = await call_next(request) + path = request.url.path + REQUESTS.labels(request.method, path, response.status_code).inc() + LATENCY.labels(path).observe(time.perf_counter() - start) + return response + + +@app.get("/") +def index(): + return {"message": "Hello from the DevOps test service"} + + +@app.get("/health") +def health(): + return {"status": "ok"} + + +@app.get("/error") +def error(): + # Used to generate 5xx responses and trigger the high-error-rate alert + raise HTTPException(status_code=500, detail="simulated failure") + + +@app.get("/metrics") +def metrics(): + return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST) diff --git a/webapp/compose.yml b/webapp/compose.yml new file mode 100644 index 0000000..b24d780 --- /dev/null +++ b/webapp/compose.yml @@ -0,0 +1,14 @@ +services: + webapp: + build: . + image: webapp:latest + container_name: webapp + restart: unless-stopped + ports: + - "8000:8000" + networks: + - monitoring-net + +networks: + monitoring-net: + external: true \ No newline at end of file diff --git a/webapp/pytest.ini b/webapp/pytest.ini new file mode 100644 index 0000000..a635c5c --- /dev/null +++ b/webapp/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +pythonpath = . diff --git a/webapp/requirements-dev.txt b/webapp/requirements-dev.txt new file mode 100644 index 0000000..7af6bb0 --- /dev/null +++ b/webapp/requirements-dev.txt @@ -0,0 +1,3 @@ +-r requirements.txt +pytest +httpx diff --git a/webapp/requirements.txt b/webapp/requirements.txt new file mode 100644 index 0000000..3c3c95c --- /dev/null +++ b/webapp/requirements.txt @@ -0,0 +1,3 @@ +fastapi +uvicorn[standard] +prometheus-client diff --git a/webapp/tests/test_app.py b/webapp/tests/test_app.py new file mode 100644 index 0000000..c6b7051 --- /dev/null +++ b/webapp/tests/test_app.py @@ -0,0 +1,27 @@ +from fastapi.testclient import TestClient + +from app.main import app + +client = TestClient(app) + + +def test_index(): + r = client.get("/") + assert r.status_code == 200 + + +def test_health(): + r = client.get("/health") + assert r.status_code == 200 + assert r.json() == {"status": "ok"} + + +def test_error_endpoint_returns_500(): + assert client.get("/error").status_code == 500 + + +def test_metrics_exposed(): + client.get("/health") + r = client.get("/metrics") + assert r.status_code == 200 + assert "http_requests_total" in r.text \ No newline at end of file