mirror of
https://github.com/grillazz/fastapi-sqlalchemy-asyncpg.git
synced 2026-03-06 10:00:39 +03:00
feat: add profiling middleware and update README for performance profiling
This commit is contained in:
33
README.md
33
README.md
@@ -34,6 +34,7 @@
|
||||
<li><a href="#uv-knowledge-and-inspirations">UV knowledge and inspirations</a></li>
|
||||
<li><a href="#large-language-model">Integration with local LLM</a></li>
|
||||
<li><a href="#ha-sample-with-nginx-as-load-balancer">High Availability sample with nginx as load balancer</a></li>
|
||||
<li><a heref="#performance-profiling-with-pyinstrument">Performance Profiling with Pyinstrument</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#acknowledgments">Acknowledgments</a></li>
|
||||
@@ -193,6 +194,35 @@ make docker-up-ha
|
||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||
|
||||
|
||||
### Performance Profiling with Pyinstrument
|
||||
To help identify performance bottlenecks and analyze request handling, this project integrates `pyinstrument` for on-demand profiling.
|
||||
The `ProfilingMiddleware` allows you to profile any endpoint by simply adding a query parameter to your request.
|
||||
|
||||
When profiling is enabled for a request, `pyinstrument` will monitor the execution, and the server will respond with a detailed HTML report that you can download and view in your browser.
|
||||
This report provides a visual breakdown of where time is spent within your code.
|
||||
|
||||
To enable profiling for an endpoint, you need to:
|
||||
1. Add a `pyprofile` query parameter to the endpoint's signature. This makes the functionality discoverable through the API documentation.
|
||||
2. Make a request to the endpoint with the query parameter `?pyprofile=true`.
|
||||
|
||||
Here is an example from the `redis_check` health endpoint:
|
||||
```python
|
||||
from typing import Annotated
|
||||
from fastapi import Query
|
||||
|
||||
@router.get("/redis", status_code=status.HTTP_200_OK)
|
||||
async def redis_check(
|
||||
request: Request,
|
||||
pyprofile: Annotated[
|
||||
bool, Query(description="Enable profiler for this request")
|
||||
] = False,
|
||||
):
|
||||
# ... endpoint logic
|
||||
```
|
||||
|
||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||
|
||||
|
||||
### UV knowledge and inspirations
|
||||
- https://docs.astral.sh/uv/
|
||||
- https://hynek.me/articles/docker-uv/
|
||||
@@ -226,7 +256,8 @@ I've included a few of my favorites to kick things off!
|
||||
<details>
|
||||
<summary>2026 (1 change)</summary>
|
||||
<ul>
|
||||
<li>[JAN 11 2026] refactor test fixture infrastructure to improve test isolation :test_tube:</li>
|
||||
<li>[FEB 5 2026] add profiler middleware :crystal ball:</li>
|
||||
<li>[JAN 11 2026] refactor test fixture infrastructure to improve test isolation :test_tube:</li>
|
||||
</ul>
|
||||
</details>
|
||||
<details>
|
||||
|
||||
@@ -13,7 +13,12 @@ router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/redis", status_code=status.HTTP_200_OK)
|
||||
async def redis_check(request: Request):
|
||||
async def redis_check(
|
||||
request: Request,
|
||||
pyprofile: Annotated[
|
||||
bool, Query(description="Enable profiler for this request")
|
||||
] = False,
|
||||
):
|
||||
"""
|
||||
Endpoint to check Redis health and retrieve server information.
|
||||
|
||||
@@ -23,6 +28,7 @@ async def redis_check(request: Request):
|
||||
|
||||
Args:
|
||||
request (Request): The incoming HTTP request.
|
||||
pyprofile (bool, optional): If `True`, enables the profiler for this request. Defaults to `False`.
|
||||
|
||||
Returns:
|
||||
dict or None: Returns Redis server information as a dictionary if successful,
|
||||
|
||||
12
app/main.py
12
app/main.py
@@ -6,6 +6,8 @@ from fastapi import Depends, FastAPI, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from rotoger import get_logger
|
||||
from starlette.middleware import Middleware
|
||||
from starlette.middleware.gzip import GZipMiddleware
|
||||
|
||||
from app.api.health import router as health_router
|
||||
from app.api.ml import router as ml_router
|
||||
@@ -15,6 +17,7 @@ from app.api.stuff import router as stuff_router
|
||||
from app.api.user import router as user_router
|
||||
from app.config import settings as global_settings
|
||||
from app.exception_handlers import register_exception_handlers
|
||||
from app.middleware.profiler import ProfilingMiddleware
|
||||
from app.redis import get_redis
|
||||
from app.services.auth import AuthBearer
|
||||
|
||||
@@ -44,11 +47,18 @@ async def lifespan(app: FastAPI):
|
||||
await app.postgres_pool.close()
|
||||
|
||||
|
||||
middleware = [
|
||||
Middleware(GZipMiddleware),
|
||||
Middleware(ProfilingMiddleware),
|
||||
]
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
app = FastAPI(
|
||||
title="Stuff And Nonsense API",
|
||||
version="1.22.0",
|
||||
lifespan=lifespan,
|
||||
middleware=middleware,
|
||||
)
|
||||
app.include_router(stuff_router)
|
||||
app.include_router(nonsense_router)
|
||||
@@ -68,6 +78,8 @@ def create_app() -> FastAPI:
|
||||
# Register exception handlers
|
||||
register_exception_handlers(app)
|
||||
|
||||
|
||||
|
||||
@app.get("/index", response_class=HTMLResponse)
|
||||
def get_index(request: Request):
|
||||
return templates.TemplateResponse("index.html", {"request": request})
|
||||
|
||||
0
app/middleware/__init__.py
Normal file
0
app/middleware/__init__.py
Normal file
28
app/middleware/profiler.py
Normal file
28
app/middleware/profiler.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import Request
|
||||
from pyinstrument import Profiler
|
||||
from starlette.middleware.base import (
|
||||
BaseHTTPMiddleware,
|
||||
RequestResponseEndpoint,
|
||||
)
|
||||
from starlette.responses import HTMLResponse, Response
|
||||
|
||||
|
||||
class ProfilingMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(
|
||||
self, request: Request, call_next: RequestResponseEndpoint
|
||||
) -> Response:
|
||||
if request.query_params.get("pyprofile") == "true":
|
||||
profiler = Profiler(interval=0.001, async_mode="enabled")
|
||||
profiler.start()
|
||||
|
||||
await call_next(request)
|
||||
|
||||
profiler.stop()
|
||||
return HTMLResponse(
|
||||
profiler.output_html(),
|
||||
headers={"Content-Disposition": "attachment; filename=profile.html"},
|
||||
)
|
||||
|
||||
return await call_next(request)
|
||||
@@ -10,7 +10,7 @@ services:
|
||||
command: bash -c "
|
||||
uvicorn app.main:app
|
||||
--host 0.0.0.0 --port 8080
|
||||
--lifespan=on --use-colors --loop uvloop --http httptools
|
||||
--lifespan=on --use-colors --loop uvloop --http httptools --reload
|
||||
"
|
||||
volumes:
|
||||
- ./app:/panettone/app
|
||||
|
||||
@@ -30,6 +30,7 @@ dependencies = [
|
||||
"granian==2.6.0",
|
||||
"apscheduler[redis,sqlalchemy]>=4.0.0a6",
|
||||
"rotoger==0.2.1",
|
||||
"pyinstrument>=5.1.2",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
|
||||
26
uv.lock
generated
26
uv.lock
generated
@@ -395,6 +395,7 @@ dependencies = [
|
||||
{ name = "polyfactory" },
|
||||
{ name = "pydantic", extra = ["email"] },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "pyinstrument" },
|
||||
{ name = "pyjwt" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-cov" },
|
||||
@@ -433,6 +434,7 @@ requires-dist = [
|
||||
{ name = "polyfactory", specifier = "==3.1.0" },
|
||||
{ name = "pydantic", extras = ["email"], specifier = "==2.12.5" },
|
||||
{ name = "pydantic-settings", specifier = "==2.12.0" },
|
||||
{ name = "pyinstrument", specifier = ">=5.1.2" },
|
||||
{ name = "pyjwt", specifier = "==2.10.1" },
|
||||
{ name = "pytest", specifier = "==9.0.2" },
|
||||
{ name = "pytest-cov", specifier = "==7.0.0" },
|
||||
@@ -999,6 +1001,30 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyinstrument"
|
||||
version = "5.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/32/7f/d3c4ef7c43f3294bd5a475dfa6f295a9fee5243c292d5c8122044fa83bcb/pyinstrument-5.1.2.tar.gz", hash = "sha256:af149d672da9493fa37334a1cc68f7b80c3e6cb9fd99b9e426c447db5c650bf0", size = 266889, upload-time = "2026-01-04T18:38:58.464Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/66/0f/7d5154c92904bdf25be067a7fe4cad4ba48919f16ccbb51bb953d9ae1a20/pyinstrument-5.1.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0baed297beee2bb9897e737bbd89e3b9d45a2fbbea9f1ad4e809007d780a9b1e", size = 131388, upload-time = "2026-01-04T18:38:10.491Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/28/bf83231a3f951e11b4dfaf160e1eeba1ce29377eab30e3d2eb6ee22ff3ba/pyinstrument-5.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ebb910a32a45bde6c3fc30c578efc28a54517990e11e94b5e48a0d5479728568", size = 124456, upload-time = "2026-01-04T18:38:11.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/98/762cf10896d907268629e1db08a48f128984a53e8d92b99ea96f862597e5/pyinstrument-5.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bad403c157f9c6dba7f731a6fca5bfcd8ca2701a39bcc717dcc6e0b10055ffc4", size = 149594, upload-time = "2026-01-04T18:38:13.434Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/1b/48580e16e623d89af58b89c552c95a2ae65f70a1f4fab1d97879f34791db/pyinstrument-5.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f456cabdb95fd343c798a7f2a56688b028f981522e283c5f59bd59195b66df5", size = 148339, upload-time = "2026-01-04T18:38:14.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/7e/38157a8a6ec67789d8ee109fd09877ea3340df44e1a7add8f249e30a8ade/pyinstrument-5.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4e9c4dcc1f2c4a0cd6b576e3604abc37496a7868243c9a1443ad3b9db69d590f", size = 148485, upload-time = "2026-01-04T18:38:16.121Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/34/31ee72b19cfc48a82801024b5d653f07982154a11381a3ae65bbfdbf2c7b/pyinstrument-5.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:acf93b128328c6d80fdb85431068ac17508f0f7845e89505b0ea6130dead5ca6", size = 148106, upload-time = "2026-01-04T18:38:17.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/b4/7ab20243187262d66ab062778b1ccac4ca55090752f32a83f603f4e5e3a2/pyinstrument-5.1.2-cp314-cp314-win32.whl", hash = "sha256:9c7f0167903ecff8b1d744f7e37b2bd4918e05a69cca724cb112f5ed59d1e41b", size = 126593, upload-time = "2026-01-04T18:38:18.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/a0/db6a8ae3182546227f5a043b1be29b8d5f98bf973e20d922981ef206de85/pyinstrument-5.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:ce3f6b1f9a2b5d74819ecc07d631eadececf915f551474a75ad65ac580ec5a0e", size = 127358, upload-time = "2026-01-04T18:38:20.28Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/d2/719f439972b3f80e35fb5b1bcd888c3218d60dbc91957b99ffafd7ac9221/pyinstrument-5.1.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:af8651b239049accbeecd389d35823233f649446f76f47fd005316b05d08cef2", size = 132317, upload-time = "2026-01-04T18:38:21.669Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/1c/0ebfef69ae926665fae635424c5647411235c3689c9a9ad69fd68de6cae2/pyinstrument-5.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c6082f1c3e43e1d22834e91ba8975f0080186df4018a04b4dd29f9623c59df1d", size = 124917, upload-time = "2026-01-04T18:38:23.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/ee/5599f769f515a0f1c97443edc7394fe2b9829bf39f404c046499c1a62378/pyinstrument-5.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c031eb066ddc16425e1e2f56aad5c1ce1e27b2432a70329e5385b85e812decee", size = 157407, upload-time = "2026-01-04T18:38:24.774Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/40/32aa865252288caef301237488ee309bd6701125888bf453d23ab764e357/pyinstrument-5.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f447ec391cad30667ba412dce41607aaa20d4a2496a7ab867e0c199f0fe3ae3d", size = 155068, upload-time = "2026-01-04T18:38:26.112Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/68/0b56a1540fe1c357dfcda82d4f5b52c87fada5962cbf18703ea39ccbbe69/pyinstrument-5.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:50299bddfc1fe0039898f895b10ef12f9db08acffb4d85326fad589cda24d2ee", size = 155186, upload-time = "2026-01-04T18:38:27.914Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/48/7ef84abfc3e41148cf993095214f104e75ecff585e94c6e8be001e672573/pyinstrument-5.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a193ff08825ece115ececa136832acb14c491c77ab1e6b6a361905df8753d5c6", size = 153979, upload-time = "2026-01-04T18:38:29.236Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/cf/a28ad117d58b33c1d74bcdfbbcf1603b67346883800ac7d510cff8d3bcee/pyinstrument-5.1.2-cp314-cp314t-win32.whl", hash = "sha256:de887ba19e1057bd2d86e6584f17788516a890ae6fe1b7eed9927873f416b4d8", size = 127267, upload-time = "2026-01-04T18:38:30.619Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/97/03635143a12a5d941f545548b00f8ac39d35565321a2effb4154ed267338/pyinstrument-5.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:b6a71f5e7f53c86c9b476b30cf19509463a63581ef17ddbd8680fee37ae509db", size = 128164, upload-time = "2026-01-04T18:38:32.281Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyjwt"
|
||||
version = "2.10.1"
|
||||
|
||||
Reference in New Issue
Block a user