The package dockerflow.sanic package implements various tools to support
Sanic based projects that want to follow the Dockerflow specs:
- A Python logging formatter following the mozlog format.
- A Sanic extension implements:
- Emitting of request.summary log records based on request specific data.
- Views for health monitoring:
/__version__- Serves aversion.jsonfile/__heartbeat__- Runs the configured Dockerflow checks/__lbheartbeat__- Retuns a HTTP 200 response
- Signals for passed and failed heartbeats.
- Built-in Dockerflow checks for SQLAlchemy and Redis connections and validating Alembic migrations.
- Hooks to add custom Dockerflow checks.
.. seealso::
For more information see the :doc:`API documentation <api/sanic>` for
the ``dockerflow.sanic`` module.
To install python-dockerflow's Sanic support please follow these steps:
In your code where your Sanic application lives set up the dockerflow Sanic extension:
from sanic import Sanic from dockerflow.sanic import Dockerflow app = Sanic(__name__) dockerflow = Dockerflow(app)
Make sure the app root path is set correctly as this will be used to locate the
version.jsonfile that is generated by CircleCI or another process during deployment... seealso:: :ref:`sanic-versions` for more information
Configure logging to use the
JsonLogFormatterlogging formatter for therequest.summarylogger (you may have to extend your existing logging configuration), see :ref:`sanic-logging` for more information.
Accept its configuration through environment variables.
There are several options to handle configuration values through environment variables when configuring Sanic.
The simplest is to use Sanic's own ability to access environment variables for settings and other variables.
Any variables defined with the
SANIC_prefix will be applied to the sanic config. For example, settingSANIC_REQUEST_TIMEOUTwill be loaded by the application automatically and fed into theREQUEST_TIMEOUTconfig variable.
The downside of that is that it nicely works only for string
based variables, since that's what os.environ returns.
A good replacement is python-decouple as it's agnostic to the framework in use and offers casting the returned value to the type wanted, e.g.:
from decouple import config
MY_SETTING = config('SANIC_MY_SETTING', default='default value')
DEBUG = config('SANIC_DEBUG', default=False, cast=bool)
As you can see the DEBUG configuration value would be populated from
the SANIC_DEBUG environment variable but also be cast as a boolean
(while considering the string values '1', 'yes', 'true' and
'on' as truthy values, and similar for falsey values).
If you need to solve more complex configuration scenarios there are tools like sanic-envconfig which allows loading settings for different environments (e.g. dev, stage, prod) via environment variables. It provides a small Python base class to allow setting up the configuration values:
E.g. in a config.py file next to your application:
from sanic_envconfig import EnvConfig
class Dev(EnvConfig):
DEBUG: bool = True
DB_URL: str = None
WORKERS: int = 1
PORT: int = 5000
class Prod(Dev):
DEBUG: bool = False
Then in your application code:
import os
from sanic import Sanic
app = Sanic(__name__)
app.config.from_object(os.environ.get('SANIC_CONFIG', 'config.Dev'))
In that example the configuration class that is given in the
SANIC_CONFIG environment variable would be used to update
the default Sanic configuration values while allowing to override
the values via environment variables.
It's recommended to use the sanic-envconfig feature to define a prefix for the environment variable it uses to check, e.g.:
from sanic_envconfig import EnvConfig
class Dev(EnvConfig):
_ENV_PREFIX = 'ACME_'
DEBUG = True
To override the config value of DEBUG the environment variable would be
called ACME_DEBUG.
Listen on environment variable $PORT for HTTP requests.
Depending on which WSGI 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.
Gunicorn automatically will bind to the hostname:port combination of
0.0.0.0:$PORT if it find the :envvar:`PORT` environment variable.
That means running gunicorn is as simple as using this, for example:
gunicorn myproject:app --worker-class sanic.worker.GunicornWorker
.. seealso::
The `full gunicorn documentation <http://docs.gunicorn.org/>`_
for more details.
Sanic is also ASGI-compliant. This means you can use your preferred ASGI webserver to run Sanic. The three main implementations of ASGI are Daphne, Uvicorn, and Hypercorn.
.. seealso::
The `Sanic deployment documentation`_ has more details.
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__.
To facilitate this python-dockerflow comes with a Sanic view to read the
file under the current worked directory (.).
If you'd like to override the location from which the view is reading the
version.json file from, simply override the optional version_path
parameter to the :class:`~dockerflow.sanic.app.Dockerflow` class, e.g.:
from sanic import Sanic from dockerflow.sanic import Dockerflow app = Sanic(__name__) dockerflow = Dockerflow(app, version_path='/app')
Alternatively if you'd like to completely override the way the version
information is read use the
:meth:`~dockerflow.sanic.app.Dockerflow.version_callback` decorator to
decorate a callback that gets the version_path value passed. E.g.:
import json
from sanic import Sanic
from dockerflow.sanic import Dockerflow
app = Sanic(__name__)
dockerflow = Dockerflow(app)
@dockerflow.version_callback
def my_version(root):
return json.loads(os.path.join(root, 'acme_version.json'))
Health monitoring happens via three different views following the Dockerflow spec:
.. http:get:: /__version__
The view that serves the :ref:`version information <sanic-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.
**Built-in Dockerflow checks:**
There are a few built-in checks that are automatically added to the list
of checks if the appropriate Sanic extension objects are passed to
the :class:`~dockerflow.sanic.app.Dockerflow` class during instantiation.
For detailed examples please see the API documentation for the built-in
:ref:`Sanic Dockerflow checks <sanic-checks>`.
**Custom Dockerflow checks:**
To write your own custom Dockerflow checks simply write a function
that returns a list of one or many check message instances representing
the severity of the check result. The :mod:`dockerflow.sanic.checks`
module contains a series of predefined check messages for the
severity levels: :class:`~dockerflow.sanic.checks.Debug`,
:class:`~dockerflow.sanic.checks.Info`,
:class:`~dockerflow.sanic.checks.Warning`,
:class:`~dockerflow.sanic.checks.Error`,
:class:`~dockerflow.sanic.checks.Critical`.
Here's an example of a check that handles various levels of exceptions
from an external storage system with different check message::
from sanic import Sanic
from dockerflow.sanic import checks, Dockerflow
app = Sanic(__name__)
dockerflow = Dockerflow(app)
@dockerflow.check
async 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
also works without async::
@dockerflow.check
def storage_reachable():
result = []
# ...
Notice the use of the :meth:`~dockerflow.sanic.app.Dockerflow.check`
decorator to mark the check to be used.
**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 a warning or 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 provides a :class:`~dockerflow.logging.JsonLogFormatter` Python logging formatter class.
To use it, pass something like this to your Sanic app when it is initialized
for at least the request.summary logger:
from sanic import Sanic
log_config = {
'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',
},
}
})
sanic = Sanic(__name__, log_config=log)
By default the log_info parameter has the value of
sanic.log.LOGGING_CONFIG_DEFAULTS.
Alternatively you can also pass the same logging config dictionary to the
logging.conf.dictConfig utility BEFORE your Sanic app is initialized:
from logging.conf import dictConfig
from sanic import Sanic
log_config = {
# ...
}
dictConfig(log_config)
sanic = Sanic(__name__)
Please refer to the Sanic documentation about serving static files for more information.