Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🔨 Profile api server #4754

Merged
merged 22 commits into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ba5adbb
add pyinstrument profiler
bisgaard-itis Sep 13, 2023
4c56ec8
merge master into profile-api-server
bisgaard-itis Sep 13, 2023
ebec735
add functionality for profiling api server
bisgaard-itis Sep 13, 2023
73eef43
document
bisgaard-itis Sep 13, 2023
1a1127a
merge master into profile-api-server
bisgaard-itis Sep 13, 2023
461f248
update doc
bisgaard-itis Sep 13, 2023
7bfea2c
minor changes according to PR feedback
bisgaard-itis Sep 14, 2023
8ff0639
minor fix
bisgaard-itis Sep 14, 2023
8c314b6
refactor to avoid code smell
bisgaard-itis Sep 14, 2023
a95ec01
Merge branch 'master' into profile-api-server
bisgaard-itis Sep 14, 2023
c1fa76a
several small cleanups
bisgaard-itis Sep 14, 2023
7c3ba56
Merge branch 'profile-api-server' of github.com:bisgaard-itis/osparc-…
bisgaard-itis Sep 14, 2023
c86fc76
improve function name
bisgaard-itis Sep 14, 2023
834b002
move pyinstrument to testing requirements
bisgaard-itis Sep 14, 2023
43672eb
Merge branch 'master' into profile-api-server
bisgaard-itis Sep 14, 2023
b2ebd84
change location where settings are read
bisgaard-itis Sep 14, 2023
1909543
Merge branch 'profile-api-server' of github.com:bisgaard-itis/osparc-…
bisgaard-itis Sep 14, 2023
3a80a34
minor fixes
bisgaard-itis Sep 14, 2023
177716a
improve assert
bisgaard-itis Sep 14, 2023
6da0eba
try to make sonar cloud happy
bisgaard-itis Sep 14, 2023
ed10118
have two copies of settings to make sonar clound happy
bisgaard-itis Sep 14, 2023
96553f6
factor out middleware to better control imports
bisgaard-itis Sep 14, 2023
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
5 changes: 5 additions & 0 deletions services/api-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ will start the api-server in development-mode together with a postgres db initia
- http://127.0.0.1:8000/docs: redoc documentation
- http://127.0.0.1:8000/dev/docs: swagger type of documentation

### Profiling requests to the api server
When in development mode (the environment variable `API_SERVER_DEV_FEATURES_ENABLED` is =1 in the running container) one can profile calls to the API server directly from the client side. On the server, the profiling is done using [Pyinstrument](https://github.com/joerick/pyinstrument). If we have our request in the form of a curl command, one simply adds the custom header `x-profile-api-server:true` to the command to the request, in which case the profile is received under the `profile` key of the response body. This makes it easy to visualise the profiling report directly in bash:
```bash
<curl_command> -H 'x-profile-api-server: true' | jq -r .profile
bisgaard-itis marked this conversation as resolved.
Show resolved Hide resolved
```

## Clients

Expand Down
1 change: 1 addition & 0 deletions services/api-server/requirements/_tools.in
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
change_case # for tools/get_api
jinja2 # for tools/get_api
watchdog[watchmedo]
pyinstrument
4 changes: 4 additions & 0 deletions services/api-server/requirements/_tools.txt
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ platformdirs==3.10.0
# virtualenv
pre-commit==3.3.3
# via -r requirements/../../../requirements/devenv.txt
pyinstrument==4.5.0
# via
# -c requirements/_base.txt
# -r requirements/_tools.in
pylint==2.17.5
# via -r requirements/../../../requirements/devenv.txt
pyproject-hooks==1.0.0
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import json
import logging
import os

from fastapi import FastAPI
from fastapi.exceptions import RequestValidationError
Expand All @@ -23,9 +25,61 @@
from .openapi import override_openapi_method, use_route_names_as_operation_ids
from .settings import ApplicationSettings

if os.environ.get("API_SERVER_DEV_FEATURES_ENABLED"):
bisgaard-itis marked this conversation as resolved.
Show resolved Hide resolved
from pyinstrument import Profiler
from starlette.requests import Request


_logger = logging.getLogger(__name__)


class ApiServerProfilerMiddleware:
"""Following
https://www.starlette.io/middleware/#cleanup-and-error-handling
https://www.starlette.io/middleware/#reusing-starlette-components
https://fastapi.tiangolo.com/advanced/middleware/#advanced-middleware
"""

def __init__(self, app: FastAPI):
self._app: FastAPI = app

async def __call__(self, scope, receive, send):
if scope["type"] != "http":
await self._app(scope, receive, send)
return

profiler = Profiler(async_mode="enabled")
request: Request = Request(scope)
headers = dict(request.headers)
if "x-profile-api-server" in headers:
del headers["x-profile-api-server"]
bisgaard-itis marked this conversation as resolved.
Show resolved Hide resolved
scope["headers"] = [
(k.encode("utf8"), v.encode("utf8")) for k, v in headers.items()
]
profiler.start()

async def send_wrapper(message):
if profiler.is_running:
profiler.stop()
if profiler.last_session:
body: bytes = json.dumps(
{"profile": profiler.output_text(unicode=True, color=True)}
).encode("utf8")
if message["type"] == "http.response.start":
for ii, header in enumerate(message["headers"]):
key, _ = header
if key.decode("utf8") == "content-length":
message["headers"][ii] = (
key,
str(len(body)).encode("utf8"),
)
elif message["type"] == "http.response.body":
message = {"type": "http.response.body", "body": body}
await send(message)

await self._app(scope, receive, send_wrapper)


def _label_info_with_state(settings: ApplicationSettings, title: str, version: str):
labels = []
if settings.API_SERVER_DEV_FEATURES_ENABLED:
Expand Down Expand Up @@ -113,6 +167,8 @@ def init_app(settings: ApplicationSettings | None = None) -> FastAPI:
else None,
),
)
if settings.API_SERVER_DEV_FEATURES_ENABLED:
app.add_middleware(ApiServerProfilerMiddleware)

# routing

Expand Down