Skip to content
Draft
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
34 changes: 34 additions & 0 deletions sample-apps/fastapi-postgres-gunicorn/Makefile
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions sample-apps/fastapi-postgres-gunicorn/README.md
Original file line number Diff line number Diff line change
@@ -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.
105 changes: 105 additions & 0 deletions sample-apps/fastapi-postgres-gunicorn/app.py
Original file line number Diff line number Diff line change
@@ -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
66 changes: 66 additions & 0 deletions sample-apps/fastapi-postgres-gunicorn/manage.py
Original file line number Diff line number Diff line change
@@ -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)
25 changes: 25 additions & 0 deletions sample-apps/fastapi-postgres-gunicorn/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 }
1 change: 1 addition & 0 deletions sample-apps/fastapi-postgres-gunicorn/templates/index.html
Loading