Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app_python/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ COPY app.py .
# Changing ownership to non-root user
RUN chown -R appuser:appuser /app

RUN mkdir -p /data && chown -R appuser:appuser /data

# Switch to non-root user
USER appuser

Expand Down
62 changes: 39 additions & 23 deletions app_python/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import logging
import sys
import json
import threading


class JSONFormatter(logging.Formatter):
Expand Down Expand Up @@ -64,7 +65,7 @@ def format(self, record: logging.LogRecord) -> str:
app = FastAPI()
START_TIME = datetime.now(timezone.utc)

# ── Prometheus Metrics ───────────────────────────────────────────────────────
# ── Prometheus Metrics ───────────────────────────────────────────────
http_requests_total = Counter(
"http_requests_total",
"Total HTTP requests",
Expand All @@ -91,6 +92,27 @@ def format(self, record: logging.LogRecord) -> str:

logger.info(f"Application starting - Host: {HOST}, Port: {PORT}")

# ── Visits Counter ───────────────────────────────────────────────────
VISITS_FILE = os.getenv("VISITS_FILE", "/data/visits")
_visits_lock = threading.Lock()


def get_visits() -> int:
try:
with open(VISITS_FILE, "r") as f:
return int(f.read().strip())
except (FileNotFoundError, ValueError):
return 0


def increment_visits() -> int:
with _visits_lock:
count = get_visits() + 1
os.makedirs(os.path.dirname(VISITS_FILE), exist_ok=True)
with open(VISITS_FILE, "w") as f:
f.write(str(count))
return count


def get_uptime():
delta = datetime.now(timezone.utc) - START_TIME
Expand Down Expand Up @@ -118,34 +140,25 @@ async def shutdown_event():
async def log_requests(request: Request, call_next):
start_time = datetime.now(timezone.utc)
client_ip = request.client.host if request.client else "unknown"

# Normalize endpoint (avoid high cardinality)
endpoint = request.url.path

http_requests_in_progress.inc()
logger.info(
f"Request started: {request.method} {endpoint} from {client_ip}"
)

try:
response = await call_next(request)
process_time = (
datetime.now(timezone.utc) - start_time
).total_seconds()

# Record metrics
http_requests_total.labels(
method=request.method,
endpoint=endpoint,
status_code=str(response.status_code),
).inc()

http_request_duration_seconds.labels(
method=request.method, endpoint=endpoint
).observe(process_time)

devops_info_endpoint_calls.labels(endpoint=endpoint).inc()

logger.info(
"Request completed",
extra={
Expand All @@ -156,10 +169,8 @@ async def log_requests(request: Request, call_next):
"duration_seconds": round(process_time, 3),
},
)

response.headers["X-Process-Time"] = str(process_time)
return response

except Exception as e:
process_time = (
datetime.now(timezone.utc) - start_time
Expand Down Expand Up @@ -191,6 +202,7 @@ def metrics():
def home(request: Request):
logger.debug("Home endpoint called")
uptime = get_uptime()
visits = increment_visits()
return {
"service": {
"name": "devops-info-service",
Expand Down Expand Up @@ -218,21 +230,25 @@ def home(request: Request):
"method": request.method,
"path": request.url.path,
},
"visits": visits,
"endpoints": [
{
"path": "/",
"method": "GET",
"description": "Service information",
},
{
"path": "/health",
"method": "GET",
"description": "Health check",
},
{"path": "/", "method": "GET", "description": "Service information"},
{"path": "/health", "method": "GET", "description": "Health check"},
{"path": "/visits", "method": "GET", "description": "Visit counter"},
],
}


@app.get("/visits")
def visits_endpoint():
logger.debug("Visits endpoint called")
count = get_visits()
return {
"visits": count,
"timestamp": datetime.now(timezone.utc).isoformat(),
}


@app.get("/health")
def health():
logger.debug("Health check endpoint called")
Expand Down Expand Up @@ -294,4 +310,4 @@ async def general_exception_handler(request: Request, exc: Exception):
import uvicorn

logger.info(f"Starting Uvicorn server on {HOST}:{PORT}")
uvicorn.run(app, host=HOST, port=PORT)
uvicorn.run(app, host=HOST, port=PORT)
9 changes: 8 additions & 1 deletion app_python/tests/test_app.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import os
import tempfile

_tmp = tempfile.NamedTemporaryFile(delete=False)
_tmp.close()
os.environ["VISITS_FILE"] = _tmp.name

from fastapi.testclient import TestClient

Check failure on line 8 in app_python/tests/test_app.py

View workflow job for this annotation

GitHub Actions / Test Python Application

Ruff (E402)

tests/test_app.py:8:1: E402 Module level import not at top of file
from app import app

Check failure on line 9 in app_python/tests/test_app.py

View workflow job for this annotation

GitHub Actions / Test Python Application

Ruff (E402)

tests/test_app.py:9:1: E402 Module level import not at top of file

client = TestClient(app)

Expand Down Expand Up @@ -92,4 +99,4 @@
data = response.json()

assert "uptime_seconds" in data
assert isinstance(data["uptime_seconds"], int)
assert isinstance(data["uptime_seconds"], int)
Loading
Loading