Skip to content

Commit

Permalink
Add integration for FastAPI (#81)
Browse files Browse the repository at this point in the history
* Add tox config for FastAPI 100

* Add dockerflow router to package root

* Add request summary middleware

* Add version endpoint and tests

* Add initial heartbeat route, check mechanism, and tests

* Refactor package structure, names of things

- Define "router" in package init, register view functions defined in views
  module
- Add test for check with custom name
- use better name for check operations

* Leverage centralized checks (#85)

* Do not test on Python 3.7

* Add 'taskName' to excluded fields

* Test with python 3.12

* Improve test coverage of FastAPI

* Blackify

* FastAPI integration docs (#95)

* Add FastAPI documentation

* Update docs/fastapi.rst

Co-authored-by: grahamalama <graham.beckley@gmail.com>

* Mention APP_DIR for the version file

---------

Co-authored-by: grahamalama <graham.beckley@gmail.com>

* Update tox.ini

Co-authored-by: grahamalama <graham.beckley@gmail.com>

---------

Co-authored-by: Mathieu Leplatre <mathieu@mozilla.com>
  • Loading branch information
grahamalama and leplatrem committed Feb 21, 2024
1 parent 6c032ec commit 9f6a0ec
Show file tree
Hide file tree
Showing 12 changed files with 631 additions and 5 deletions.
11 changes: 11 additions & 0 deletions docs/api/fastapi.rst
Original file line number Diff line number Diff line change
@@ -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
325 changes: 325 additions & 0 deletions docs/fastapi.rst
Original file line number Diff line number Diff line change
@@ -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 <api/fastapi>` 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 <fastapi-versions>`.

**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 <https://fastapi.tiangolo.com/reference/staticfiles/>`_ 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")
12 changes: 7 additions & 5 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,17 @@ Features
environment

Accept its configuration through environment variables.
See: :ref:`Django <django-config>`, :ref:`Flask <flask-config>`, :ref:`Sanic <sanic-config>`
See: :ref:`Django <django-config>`, :ref:`FastAPI <fastapi-config>`, :ref:`Flask <flask-config>`, :ref:`Sanic <sanic-config>`

port

Listen on environment variable ``$PORT`` for HTTP requests.
See: :ref:`Django <django-serving>`, :ref:`Flask <flask-serving>`, :ref:`Sanic <sanic-serving>`
See: :ref:`Django <django-serving>`, :ref:`FastAPI <fastapi-serving>`, :ref:`Flask <flask-serving>`, :ref:`Sanic <sanic-serving>`

version

Must have a JSON version object at ``/app/version.json``.
See: :ref:`Django <django-versions>`, :ref:`Flask <flask-versions>`, :ref:`Sanic <sanic-versions>`
See: :ref:`Django <django-versions>`, :ref:`FastAPI <fastapi-versions>`, :ref:`Flask <flask-versions>`, :ref:`Sanic <sanic-versions>`

health

Expand All @@ -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 <django-health>`, :ref:`Flask <flask-health>`, :ref:`Sanic <sanic-health>`
See: :ref:`Django <django-health>`, :ref:`FastAPI <fastapi-health>`, :ref:`Flask <flask-health>`, :ref:`Sanic <sanic-health>`

logging

Send text logs to ``stdout`` or ``stderr``. See:
:ref:`Generic <logging>`, :ref:`Django <django-logging>`,
:ref:`FastAPI <fastapi-logging>`,
:ref:`Flask <flask-logging>`, :ref:`Sanic <sanic-logging>`

static content

Serve its own static content. See:
:ref:`Django <django-static>`, :ref:`Flask <flask-static>`, :ref:`Flask <sanic-static>`
:ref:`Django <django-static>`, logging:ref:`FastAPI <fastapi-static>`, :ref:`Flask <sanic-static>`

Contents
--------
Expand All @@ -69,6 +70,7 @@ Contents
changelog
logging
django
fastapi
flask
sanic
api/index
Expand Down
1 change: 1 addition & 0 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 9f6a0ec

Please sign in to comment.