diff --git a/docs/api/fastapi.rst b/docs/api/fastapi.rst new file mode 100644 index 0000000..9290b6d --- /dev/null +++ b/docs/api/fastapi.rst @@ -0,0 +1,11 @@ +FastAPI +======= + +This documents the various FastAPI specific functionality but doesn't cover +internals of the extension. + +Extension +--------- + +.. automodule:: dockerflow.fastapi + :members: router diff --git a/docs/fastapi.rst b/docs/fastapi.rst new file mode 100644 index 0000000..9f15112 --- /dev/null +++ b/docs/fastapi.rst @@ -0,0 +1,325 @@ +FastAPI +======= + +The package ``dockerflow.fastapi`` package implements various tools to support +`FastAPI`_ based projects that want to follow the Dockerflow specs: + +- A Python logging formatter following the `mozlog`_ format. + +- A FastAPI extension implements: + + - Emitting of `request.summary`_ log records based on request specific data. + + - Views for health monitoring: + + - ``/__version__`` - Serves a ``version.json`` file + + - ``/__heartbeat__`` - Runs the configured Dockerflow checks + + - ``/__lbheartbeat__`` - Retuns a HTTP 200 response + + - Hooks to add custom Dockerflow checks. + +.. _`FastAPI`: https://fastapi.tiangolo.com +.. _`mozlog`: https://github.com/mozilla-services/Dockerflow/blob/main/docs/mozlog.md +.. _`request.summary`: https://github.com/mozilla-services/Dockerflow/blob/main/docs/mozlog.md#application-request-summary-type-requestsummary + +.. seealso:: + + For more information see the :doc:`API documentation ` for + the ``dockerflow.fastapi`` module. + +Setup +----- + +To install ``python-dockerflow``'s FastAPI support please follow these steps: + +#. In your code where your FastAPI application lives set up the dockerflow FastAPI + extension:: + + from fastapi import FastAPI + from dockerflow.fastapi import router + from dockerflow.fastapi.middleware import MozlogRequestSummaryLogger + + app = FastAPI() + app.include_router(router) + app.add_middleware(MozlogRequestSummaryLogger) + +#. Make sure the app root path is set correctly as this will be used + to locate the ``version.json`` file that is generated by + your CI or another process during deployment. + + .. seealso:: :ref:`fastapi-versions` for more information + +#. Configure logging to use the ``JsonLogFormatter`` logging formatter for the + ``request.summary`` logger (you may have to extend your existing logging + configuration), see :ref:`fastapi-logging` for more information. + +.. _fastapi-config: + +Configuration +------------- + +.. epigraph:: + + Accept its configuration through environment variables. + +There are several options to handle configuration values through +environment variables when configuring FastAPI. + +``pydantic-settings`` +~~~~~~~~~~~~~~~~~~~~~ + +The simplest is to use `pydantic-settings`_ that will load settings from +environment variables or secrets files, and turn them into model instance. + +.. code-block:: python + + from pydantic_settings import BaseSettings + + class Settings(BaseSettings): + port: int = 8000 + + settings = Settings() + +.. _pydantic-settings: https://docs.pydantic.dev/latest/concepts/pydantic_settings/ + +.. _fastapi-serving: + +``PORT`` +-------- + +.. epigraph:: + + Listen on environment variable ``$PORT`` for HTTP requests. + +Depending on which ASGI server you are using to run your Python application +there are different ways to accept the :envvar:`PORT` as the port to launch +your application with. + +It's recommended to use port ``8000`` by default. + +Uvicorn +~~~~~~~ + +.. code-block:: python + + import uvicorn + + from myapp import Settings + + settings = Settings() + + if __name__ == "__main__": + server = uvicorn.Server( + uvicorn.Config( + "myapp:app", + host=settings.host, + port=settings.port, + reload=settings.app_reload, + log_config=None, + ) + ) + server.run() + +.. _fastapi-versions: + +Versions +-------- + +.. epigraph:: + + Must have a JSON version object at /app/version.json. + +Dockerflow requires writing a `version object`_ to the file +``/app/version.json`` as seen from the docker container to be served under +the URL path ``/__version__``. + +.. note:: + + The default ``/app`` location can be customized using the ``APP_DIR`` + environment variable. + +To facilitate this python-dockerflow comes with a FastAPI view to read the +file under path the parent directory of the app root. See the +:class:`FastAPI API docs <~fastapi.FastAPI>` for more information about the +app root path. + +.. _version object: https://github.com/mozilla-services/Dockerflow/blob/main/docs/version_object.md + +.. _fastapi-health: + +Health monitoring +----------------- + +Health monitoring happens via three different views following the Dockerflow_ +spec: + +.. http:get:: /__version__ + + The view that serves the :ref:`version information `. + + **Example request**: + + .. sourcecode:: http + + GET /__version__ HTTP/1.1 + Host: example.com + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept-Encoding + Content-Type: application/json + + { + "commit": "52ce614fbf99540a1bf6228e36be6cef63b4d73b", + "version": "2017.11.0", + "source": "https://github.com/mozilla/telemetry-analysis-service", + "build": "https://circleci.com/gh/mozilla/telemetry-analysis-service/2223" + } + + :statuscode 200: no error + :statuscode 404: a version.json wasn't found + +.. http:get:: /__heartbeat__ + + The heartbeat view will go through the list of registered Dockerflow + checks, run each check and add their results to a JSON response. + + The view will return HTTP responses with either an status code of 200 if + all checks ran successfully or 500 if there was one or more warnings or + errors returned by the checks. + + Here's an example of a check that handles various levels of exceptions + from an external storage system with different check message:: + + from dockerflow import checks + + @checks.register + def storage_reachable(): + result = [] + try: + acme.storage.ping() + except SlowConnectionException as exc: + result.append(checks.Warning(exc.msg, id='acme.health.0002')) + except StorageException as exc: + result.append(checks.Error(exc.msg, id='acme.health.0001')) + return result + + **Example request**: + + .. sourcecode:: http + + GET /__heartbeat__ HTTP/1.1 + Host: example.com + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 500 Internal Server Error + Vary: Accept-Encoding + Content-Type: application/json + + { + "status": "warning", + "checks": { + "check_debug": "ok", + "check_sts_preload": "warning" + }, + "details": { + "check_sts_preload": { + "status": "warning", + "level": 30, + "messages": { + "security.W021": "You have not set the SECURE_HSTS_PRELOAD setting to True. Without this, your site cannot be submitted to the browser preload list." + } + } + } + } + + :statuscode 200: no error + :statuscode 500: there was an error + +.. http:get:: /__lbheartbeat__ + + The view that simply returns a successful HTTP response so that a load + balancer in front of the application can check that the web application + has started up. + + **Example request**: + + .. sourcecode:: http + + GET /__lbheartbeat__ HTTP/1.1 + Host: example.com + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept-Encoding + Content-Type: application/json + + :statuscode 200: no error + +.. _Dockerflow: https://github.com/mozilla-services/Dockerflow + +.. _fastapi-logging: + +Logging +------- + +Dockerflow provides a :class:`~dockerflow.logging.JsonLogFormatter` Python +logging formatter class. + +To use it, put something like this **BEFORE** your FastAPI app is initialized +for at least the ``request.summary`` logger: + +.. code-block:: python + + from logging.conf import dictConfig + + dictConfig({ + 'version': 1, + 'formatters': { + 'json': { + '()': 'dockerflow.logging.JsonLogFormatter', + 'logger_name': 'myproject' + } + }, + 'handlers': { + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'json' + }, + }, + 'loggers': { + 'request.summary': { + 'handlers': ['console'], + 'level': 'DEBUG', + }, + } + }) + +.. _fastapi-static: + +Static content +-------------- + +We recommend using default `FastAPI features `_ for static files: + +.. code-block:: python + + from fastapi.staticfiles import StaticFiles + + SRC_DIR = Path(__file__).parent + + app = FastAPI() + + app.mount("/static", StaticFiles(directory=SRC_DIR / "static"), name="static") diff --git a/docs/index.rst b/docs/index.rst index 6e2d320..933073b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -24,17 +24,17 @@ Features environment Accept its configuration through environment variables. - See: :ref:`Django `, :ref:`Flask `, :ref:`Sanic ` + See: :ref:`Django `, :ref:`FastAPI `, :ref:`Flask `, :ref:`Sanic ` port Listen on environment variable ``$PORT`` for HTTP requests. - See: :ref:`Django `, :ref:`Flask `, :ref:`Sanic ` + See: :ref:`Django `, :ref:`FastAPI `, :ref:`Flask `, :ref:`Sanic ` version Must have a JSON version object at ``/app/version.json``. - See: :ref:`Django `, :ref:`Flask `, :ref:`Sanic ` + See: :ref:`Django `, :ref:`FastAPI `, :ref:`Flask `, :ref:`Sanic ` health @@ -44,18 +44,19 @@ Features * Respond to ``/__lbheartbeat__`` with an HTTP 200. This is for load balancer checks and should not check backing services. - See: :ref:`Django `, :ref:`Flask `, :ref:`Sanic ` + See: :ref:`Django `, :ref:`FastAPI `, :ref:`Flask `, :ref:`Sanic ` logging Send text logs to ``stdout`` or ``stderr``. See: :ref:`Generic `, :ref:`Django `, + :ref:`FastAPI `, :ref:`Flask `, :ref:`Sanic ` static content Serve its own static content. See: - :ref:`Django `, :ref:`Flask `, :ref:`Flask ` + :ref:`Django `, logging:ref:`FastAPI `, :ref:`Flask ` Contents -------- @@ -69,6 +70,7 @@ Contents changelog logging django + fastapi flask sanic api/index diff --git a/docs/requirements.txt b/docs/requirements.txt index 6b83b8c..cf151c7 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,6 @@ -r ../tests/requirements/default.txt -r ../tests/requirements/docs.txt -r ../tests/requirements/django.txt +-r ../tests/requirements/fastapi.txt -r ../tests/requirements/flask.txt -r ../tests/requirements/sanic.txt diff --git a/src/dockerflow/fastapi/__init__.py b/src/dockerflow/fastapi/__init__.py index 8181e18..19596b0 100644 --- a/src/dockerflow/fastapi/__init__.py +++ b/src/dockerflow/fastapi/__init__.py @@ -11,3 +11,4 @@ APIRoute("/__version__", endpoint=version, methods=["GET"]), ], ) +"""This router adds the Dockerflow views."""