diff --git a/sample-apps/fastapi-postgres-gunicorn/Makefile b/sample-apps/fastapi-postgres-gunicorn/Makefile new file mode 100644 index 00000000..251b61b0 --- /dev/null +++ b/sample-apps/fastapi-postgres-gunicorn/Makefile @@ -0,0 +1,34 @@ +.PHONY: install +install: + poetry install + +.PHONY: run +run: install + @echo "Running sample app fastapi-postgres-gunicorn with Zen on port 8104" + AIKIDO_DEBUG=true AIKIDO_BLOCK=true AIKIDO_TOKEN="AIK_secret_token" \ + AIKIDO_REALTIME_ENDPOINT="http://localhost:5000/" \ + AIKIDO_ENDPOINT="http://localhost:5000/" AIKIDO_DISABLE=0 \ + poetry run uvicorn app:app --host 0.0.0.0 --port 8104 --workers 4 + +.PHONY: runBenchmark +runBenchmark: install + @echo "Running sample app fastapi-postgres-gunicorn with Zen (benchmark mode) on port 8104" + AIKIDO_DEBUG=false AIKIDO_BLOCK=true AIKIDO_TOKEN="AIK_secret_token" \ + AIKIDO_REALTIME_ENDPOINT="http://localhost:5000/" \ + AIKIDO_ENDPOINT="http://localhost:5000/" AIKIDO_DISABLE=0 \ + DONT_ADD_MIDDLEWARE=1 \ + poetry run uvicorn app:app --host 0.0.0.0 --port 8104 --workers 4 + +.PHONY: runZenDisabled +runZenDisabled: install + @echo "Running sample app fastapi-postgres-gunicorn without Zen on port 8105" + AIKIDO_DISABLE=1 \ + poetry run uvicorn app:app --host 0.0.0.0 --port 8105 --workers 4 + +.PHONY: runGunicorn +runGunicorn: install + @echo "Running sample app fastapi-postgres-gunicorn with Gunicorn on port 8104" + AIKIDO_DEBUG=true AIKIDO_BLOCK=true AIKIDO_TOKEN="AIK_secret_token" \ + AIKIDO_REALTIME_ENDPOINT="http://localhost:5000/" \ + AIKIDO_ENDPOINT="http://localhost:5000/" AIKIDO_DISABLE=0 \ + python manage.py gunicornserver \ No newline at end of file diff --git a/sample-apps/fastapi-postgres-gunicorn/README.md b/sample-apps/fastapi-postgres-gunicorn/README.md new file mode 100644 index 00000000..b47c58e2 --- /dev/null +++ b/sample-apps/fastapi-postgres-gunicorn/README.md @@ -0,0 +1,23 @@ +# FastAPI w/ Postgres and Gunicorn Sample app +It runs **multi-threaded** and **async** with Gunicorn + +## Getting started +Run : +```bash +make run # Runs app with zen +make runZenDisabled # Runs app with zen disabled. +``` + +- You'll be able to access the FastAPI Server at : [localhost:8104](http://localhost:8104) +- To Create a reference test dog use `http://localhost:8104/create/` +- To Create a reference test dog (with executemany) use `http://localhost:8104/create_many/` + +- To test a sql injection enter the following dog name : `Malicious dog', TRUE); -- ` + +## Running with Gunicorn +To run with Gunicorn: +```bash +python manage.py gunicornserver +``` + +This will start the Gunicorn server with Uvicorn workers for optimal async performance. \ No newline at end of file diff --git a/sample-apps/fastapi-postgres-gunicorn/app.py b/sample-apps/fastapi-postgres-gunicorn/app.py new file mode 100644 index 00000000..5c23a09c --- /dev/null +++ b/sample-apps/fastapi-postgres-gunicorn/app.py @@ -0,0 +1,105 @@ +import aikido_zen # Aikido package import +import os + +dont_add_middleware = os.getenv("DONT_ADD_MIDDLEWARE") +import time +import asyncpg +from fastapi import FastAPI, Request, HTTPException +from fastapi.responses import HTMLResponse, JSONResponse +from fastapi.templating import Jinja2Templates +from fastapi.middleware.cors import CORSMiddleware + +templates = Jinja2Templates(directory="templates") + +app = FastAPI() + +# CORS middleware (optional, depending on your needs) +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Adjust this as needed + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +if dont_add_middleware is None or dont_add_middleware.lower() != "1": + # Use DONT_ADD_MIDDLEWARE so we don't add this middleware during e.g. benchmarks. + import aikido_zen + from aikido_zen.middleware import AikidoFastAPIMiddleware # Aikido package import + + class SetUserMiddleware: + def __init__(self, app): + self.app = app + + async def __call__(self, scope, receive, send): + aikido_zen.set_user({"id": "user123", "name": "John Doe"}) + return await self.app(scope, receive, send) + + app.add_middleware(SetUserMiddleware) + app.add_middleware(AikidoFastAPIMiddleware) + +async def get_db_connection(): + return await asyncpg.connect( + host="localhost", + database="db", + user="user", + password="password" + ) + +@app.get("/", response_class=HTMLResponse) +async def homepage(request: Request): + conn = await get_db_connection() + dogs = await conn.fetch("SELECT * FROM dogs") + await conn.close() + return templates.TemplateResponse('index.html', {"request": request, "title": 'Homepage', "dogs": dogs}) + +@app.get("/dogpage/{dog_id:int}", response_class=HTMLResponse) +async def get_dogpage(request: Request, dog_id: int): + conn = await get_db_connection() + dog = await conn.fetchrow("SELECT * FROM dogs WHERE id = $1", dog_id) + await conn.close() + if dog is None: + raise HTTPException(status_code=404, detail="Dog not found") + return templates.TemplateResponse('dogpage.html', {"request": request, "title": 'Dog', "dog": dog, "isAdmin": "Yes" if dog[2] else "No"}) + +@app.get("/create", response_class=HTMLResponse) +async def show_create_dog_form(request: Request): + return templates.TemplateResponse('create_dog.html', {"request": request}) + +@app.post("/create") +async def create_dog(request: Request): + data = await request.form() + dog_name = data.get('dog_name') + + if not dog_name: + return JSONResponse({"error": "dog_name is required"}, status_code=400) + + conn = await get_db_connection() + try: + await conn.execute("INSERT INTO dogs (dog_name, isAdmin) VALUES ($1, FALSE)", dog_name) + finally: + await conn.close() + + return JSONResponse({"message": f'Dog {dog_name} created successfully'}, status_code=201) + +@app.get("/just") +async def just(): + return JSONResponse({"message": "Empty Page"}) + +@app.get("/test_ratelimiting_1") +async def just(): + return JSONResponse({"message": "Empty Page"}) + +@app.get("/delayed_route") +async def delayed_route(): + time.sleep(1/1000) # Note: This will block the event loop; consider using asyncio.sleep instead + return JSONResponse({"message": "Empty Page"}) + +@app.get("/sync_route") +def sync_route(): + data = {"message": "This is a non-async route!"} + return JSONResponse(data) + +# For gunicorn compatibility +def create_app(): + return app \ No newline at end of file diff --git a/sample-apps/fastapi-postgres-gunicorn/manage.py b/sample-apps/fastapi-postgres-gunicorn/manage.py new file mode 100755 index 00000000..da43cfc0 --- /dev/null +++ b/sample-apps/fastapi-postgres-gunicorn/manage.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 + +import os +import sys +from gunicorn.app.base import BaseApplication +from app import create_app + +# Add the current directory to the Python path +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +class Config: + def __init__(self): + # Default configuration values + self.gunicorn_workers_count = 4 # (2 x $num_cores) + 1 + self.threads_per_worker = 1 + self.gunicorn_keepalive = 2 + self.gunicorn_max_requests = 1000 + self.gunicorn_max_requests_jitter = 50 + self.requests_read_timeout = 30 + self.gunicorn_graceful_timeout = 30 + self.log_level = "info" + self.app_dir = os.path.dirname(os.path.abspath(__file__)) + +def run_gunicorn_server(): + # Optimized Gunicorn with UvicornWorker for high-performance async concurrency + # Relies on the operating system to provide all of the load balancing + # Generally we recommend (2 x $num_cores) + 1 as the number of workers + + config = Config() + WORKERS = config.gunicorn_workers_count + WORKER_CONNECTIONS = config.threads_per_worker * 100 + + class GunicornApplication(BaseApplication): + def load_config(self): + self.cfg.set("bind", f"unix:{os.path.join(config.app_dir, 'da.sock')}") + self.cfg.set("accesslog", "-") + self.cfg.set("workers", WORKERS) + self.cfg.set("worker_class", "uvicorn.workers.UvicornWorker") + self.cfg.set("worker_connections", WORKER_CONNECTIONS) + self.cfg.set("loglevel", config.log_level) + + # Performance optimizations + self.cfg.set("keepalive", config.gunicorn_keepalive) + self.cfg.set("max_requests", config.gunicorn_max_requests) + self.cfg.set("max_requests_jitter", config.gunicorn_max_requests_jitter) + + # Timeout optimizations + self.cfg.set("timeout", config.requests_read_timeout) + self.cfg.set("graceful_timeout", config.gunicorn_graceful_timeout) + + def load(self): + return create_app() + + def init(self, parser, opts, args): + return + + if __name__ == "__main__": + wsgi_server = GunicornApplication() + wsgi_server.run() + +if __name__ == "__main__": + if len(sys.argv) > 1 and sys.argv[1] == "gunicornserver": + run_gunicorn_server() + else: + print("Usage: python manage.py gunicornserver") + sys.exit(1) \ No newline at end of file diff --git a/sample-apps/fastapi-postgres-gunicorn/pyproject.toml b/sample-apps/fastapi-postgres-gunicorn/pyproject.toml new file mode 100644 index 00000000..9b8bb089 --- /dev/null +++ b/sample-apps/fastapi-postgres-gunicorn/pyproject.toml @@ -0,0 +1,25 @@ +[project] +name = "fastapi-postgres-gunicorn" +version = "0.1.0" +description = "" +requires-python = ">=3.10,<4.0" +dependencies = [ + "aikido_zen", + "fastapi (>=0.115.0,<0.116.0)", + "jinja2 (>=3.1.5,<4.0.0)", + "asyncpg (>=0.30.0,<0.31.0)", + "cryptography (>=44.0.0,<45.0.0)", + "uvicorn (>=0.34.0,<0.35.0)", + "python-multipart (>=0.0.20,<0.0.21)", + "gunicorn (>=22.0.0,<23.0.0)", +] + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry] +package-mode = false + +[tool.poetry.dependencies] +aikido_zen = { path = "../../", develop = true } \ No newline at end of file diff --git a/sample-apps/fastapi-postgres-gunicorn/templates/create_dog.html b/sample-apps/fastapi-postgres-gunicorn/templates/create_dog.html new file mode 120000 index 00000000..21988c9e --- /dev/null +++ b/sample-apps/fastapi-postgres-gunicorn/templates/create_dog.html @@ -0,0 +1 @@ +../../shared-templates/postgres/create_dog.html \ No newline at end of file diff --git a/sample-apps/fastapi-postgres-gunicorn/templates/dogpage.html b/sample-apps/fastapi-postgres-gunicorn/templates/dogpage.html new file mode 120000 index 00000000..369e4aad --- /dev/null +++ b/sample-apps/fastapi-postgres-gunicorn/templates/dogpage.html @@ -0,0 +1 @@ +../../shared-templates/postgres/dogpage.html \ No newline at end of file diff --git a/sample-apps/fastapi-postgres-gunicorn/templates/index.html b/sample-apps/fastapi-postgres-gunicorn/templates/index.html new file mode 120000 index 00000000..43aa4ca3 --- /dev/null +++ b/sample-apps/fastapi-postgres-gunicorn/templates/index.html @@ -0,0 +1 @@ +../../shared-templates/postgres/index.html \ No newline at end of file