|
| 1 | +import os |
| 2 | +import time |
| 3 | +import asyncpg |
| 4 | +from fastapi import FastAPI, HTTPException |
| 5 | +from pydantic import BaseModel |
| 6 | +from prometheus_client import Counter, Histogram, generate_latest |
| 7 | +from fastapi.responses import PlainTextResponse |
| 8 | + |
| 9 | +app = FastAPI(title="DevOps Task API", version="1.0.0") |
| 10 | + |
| 11 | +# --- Prometheus metrics --- |
| 12 | +REQUEST_COUNT = Counter("http_requests_total", "Total HTTP requests", ["method", "endpoint", "status"]) |
| 13 | +REQUEST_DURATION = Histogram("http_request_duration_seconds", "Request duration", ["endpoint"]) |
| 14 | +TASKS_CREATED = Counter("tasks_created_total", "Total tasks created") |
| 15 | +TASKS_COMPLETED = Counter("tasks_completed_total", "Total tasks completed") |
| 16 | + |
| 17 | +# --- DB connection --- |
| 18 | +DB_URL = ( |
| 19 | + f"postgresql://{os.getenv('DB_USER', 'devops')}:{os.getenv('DB_PASSWORD', 'devops2026')}" |
| 20 | + f"@{os.getenv('DB_HOST', 'postgres')}:5432/{os.getenv('DB_NAME', 'taskdb')}" |
| 21 | +) |
| 22 | +pool = None |
| 23 | + |
| 24 | +class TaskIn(BaseModel): |
| 25 | + title: str |
| 26 | + description: str = "" |
| 27 | + |
| 28 | +class TaskOut(BaseModel): |
| 29 | + id: int |
| 30 | + title: str |
| 31 | + description: str |
| 32 | + done: bool |
| 33 | + |
| 34 | +@app.on_event("startup") |
| 35 | +async def startup(): |
| 36 | + global pool |
| 37 | + pool = await asyncpg.create_pool(DB_URL, min_size=2, max_size=10) |
| 38 | + async with pool.acquire() as conn: |
| 39 | + await conn.execute(""" |
| 40 | + CREATE TABLE IF NOT EXISTS tasks ( |
| 41 | + id SERIAL PRIMARY KEY, |
| 42 | + title TEXT NOT NULL, |
| 43 | + description TEXT DEFAULT '', |
| 44 | + done BOOLEAN DEFAULT FALSE |
| 45 | + ) |
| 46 | + """) |
| 47 | + |
| 48 | +@app.on_event("shutdown") |
| 49 | +async def shutdown(): |
| 50 | + if pool: |
| 51 | + await pool.close() |
| 52 | + |
| 53 | +@app.get("/metrics", response_class=PlainTextResponse) |
| 54 | +async def metrics(): |
| 55 | + return generate_latest() |
| 56 | + |
| 57 | +@app.get("/health") |
| 58 | +async def health(): |
| 59 | + try: |
| 60 | + async with pool.acquire() as conn: |
| 61 | + await conn.fetchval("SELECT 1") |
| 62 | + return {"status": "healthy", "db": "connected"} |
| 63 | + except Exception as e: |
| 64 | + raise HTTPException(status_code=503, detail=str(e)) |
| 65 | + |
| 66 | +@app.get("/tasks", response_model=list[TaskOut]) |
| 67 | +async def list_tasks(): |
| 68 | + start = time.time() |
| 69 | + async with pool.acquire() as conn: |
| 70 | + rows = await conn.fetch("SELECT id, title, description, done FROM tasks ORDER BY id") |
| 71 | + REQUEST_COUNT.labels("GET", "/tasks", 200).inc() |
| 72 | + REQUEST_DURATION.labels("/tasks").observe(time.time() - start) |
| 73 | + return [dict(r) for r in rows] |
| 74 | + |
| 75 | +@app.post("/tasks", response_model=TaskOut, status_code=201) |
| 76 | +async def create_task(task: TaskIn): |
| 77 | + start = time.time() |
| 78 | + async with pool.acquire() as conn: |
| 79 | + row = await conn.fetchrow( |
| 80 | + "INSERT INTO tasks (title, description) VALUES ($1, $2) RETURNING id, title, description, done", |
| 81 | + task.title, task.description, |
| 82 | + ) |
| 83 | + TASKS_CREATED.inc() |
| 84 | + REQUEST_COUNT.labels("POST", "/tasks", 201).inc() |
| 85 | + REQUEST_DURATION.labels("/tasks").observe(time.time() - start) |
| 86 | + return dict(row) |
| 87 | + |
| 88 | +@app.patch("/tasks/{task_id}/complete", response_model=TaskOut) |
| 89 | +async def complete_task(task_id: int): |
| 90 | + start = time.time() |
| 91 | + async with pool.acquire() as conn: |
| 92 | + row = await conn.fetchrow( |
| 93 | + "UPDATE tasks SET done = TRUE WHERE id = $1 RETURNING id, title, description, done", |
| 94 | + task_id, |
| 95 | + ) |
| 96 | + if not row: |
| 97 | + raise HTTPException(status_code=404, detail="Task not found") |
| 98 | + TASKS_COMPLETED.inc() |
| 99 | + REQUEST_COUNT.labels("PATCH", "/tasks/complete", 200).inc() |
| 100 | + REQUEST_DURATION.labels("/tasks/complete").observe(time.time() - start) |
| 101 | + return dict(row) |
| 102 | + |
| 103 | +@app.delete("/tasks/{task_id}", status_code=204) |
| 104 | +async def delete_task(task_id: int): |
| 105 | + async with pool.acquire() as conn: |
| 106 | + result = await conn.execute("DELETE FROM tasks WHERE id = $1", task_id) |
| 107 | + if result == "DELETE 0": |
| 108 | + raise HTTPException(status_code=404, detail="Task not found") |
| 109 | + REQUEST_COUNT.labels("DELETE", "/tasks", 204).inc() |
0 commit comments