Skip to content

Commit cc657b3

Browse files
jefftriplettbrowniebrokefrankwiles
authored
⚙️ Adds Django 4.2 support and fixes trove classifiers (#363)
* ⚙️ Adds Django 4.2 support and fixes trove classifiers * :shirts: Runs black on code to make CI happy * :shirts: Runs isort on code to make CI happy * 💚 Updates test to match exception copy * 📝 mirrors old README.rst to make docs work Not a great solution, but a workaround. * ⚙️ Adds pip cache support * 🔥 Removes 4.0 from grid because it's no longer supported * ⚙️ Updates cache-key This isn't really used, but I think having caching is a win * ⚙️ Adds missing file * 🔥 Removes cache line * Fix storages deprecation warnings (#372) * ⚙️ Adds Django 4.2 support and fixes trove classifiers * :shirts: Runs black on code to make CI happy * :shirts: Runs isort on code to make CI happy * 💚 Updates test to match exception copy * 📝 mirrors old README.rst to make docs work Not a great solution, but a workaround. * ⚙️ Adds pip cache support * 🔥 Removes 4.0 from grid because it's no longer supported * ⚙️ Updates cache-key This isn't really used, but I think having caching is a win * ⚙️ Adds missing file * 🔥 Removes cache line * Fix storages deprecation warnings --------- Co-authored-by: Jeff Triplett <jeff.triplett@gmail.com> * Fix isort ordering --------- Co-authored-by: Bruno Alla <browniebroke@users.noreply.github.com> Co-authored-by: Frank Wiles <frank@revsys.com>
1 parent 9f9810a commit cc657b3

File tree

9 files changed

+381
-20
lines changed

9 files changed

+381
-20
lines changed

.github/workflows/ci.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ jobs:
3535
- uses: actions/setup-python@v4
3636
with:
3737
python-version: "3.x"
38+
cache: 'pip'
39+
cache-dependency-path: '**/pyproject.toml'
3840
- run: python -m pip install --upgrade pip build wheel twine
3941
- run: python -m build --sdist --wheel
4042
- run: python -m twine check dist/*
@@ -49,6 +51,8 @@ jobs:
4951
uses: actions/setup-python@v4
5052
with:
5153
python-version: "3.10"
54+
cache: 'pip'
55+
cache-dependency-path: '**/pyproject.toml'
5256
- run: python -m pip install -e .[docs]
5357
- run: python -m sphinx -b html -W docs docs/_build
5458

@@ -65,14 +69,16 @@ jobs:
6569
django-version:
6670
- "2.2"
6771
- "3.2"
68-
- "4.0"
6972
- "4.1"
73+
- "4.2"
7074
steps:
7175
- uses: actions/checkout@v3
7276
- name: Setup Python version ${{ matrix.python-version }}
7377
uses: actions/setup-python@v4
7478
with:
7579
python-version: ${{ matrix.python-version }}
80+
cache: 'pip'
81+
cache-dependency-path: '**/pyproject.toml'
7682
- run: python -m pip install .[test]
7783
- run: python -m pip install Django~="${{ matrix.django-version }}.0"
7884
- run: python -m pytest

docs/readme.rst

Lines changed: 291 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,291 @@
1-
.. include:: ../README.rst
1+
django-health-check
2+
===================
3+
4+
|version| |pyversion| |djversion| |license|
5+
6+
This project checks for various conditions and provides reports when
7+
anomalous behavior is detected.
8+
9+
The following health checks are bundled with this project:
10+
11+
- cache
12+
- database
13+
- storage
14+
- disk and memory utilization (via ``psutil``)
15+
- AWS S3 storage
16+
- Celery task queue
17+
- Celery ping
18+
- RabbitMQ
19+
- Migrations
20+
21+
Writing your own custom health checks is also very quick and easy.
22+
23+
We also like contributions, so don’t be afraid to make a pull request.
24+
25+
Use Cases
26+
---------
27+
28+
The primary intended use case is to monitor conditions via HTTP(S), with
29+
responses available in HTML and JSON formats. When you get back a
30+
response that includes one or more problems, you can then decide the
31+
appropriate course of action, which could include generating
32+
notifications and/or automating the replacement of a failing node with a
33+
new one. If you are monitoring health in a high-availability environment
34+
with a load balancer that returns responses from multiple nodes, please
35+
note that certain checks (e.g., disk and memory usage) will return
36+
responses specific to the node selected by the load balancer.
37+
38+
Supported Versions
39+
------------------
40+
41+
We officially only support the latest version of Python as well as the
42+
latest version of Django and the latest Django LTS version.
43+
44+
Installation
45+
------------
46+
47+
First, install the ``django-health-check`` package:
48+
49+
.. code:: shell
50+
51+
$ pip install django-health-check
52+
53+
Add the health checker to a URL you want to use:
54+
55+
.. code:: python
56+
57+
urlpatterns = [
58+
# ...
59+
url(r'^ht/', include('health_check.urls')),
60+
]
61+
62+
Add the ``health_check`` applications to your ``INSTALLED_APPS``:
63+
64+
.. code:: python
65+
66+
INSTALLED_APPS = [
67+
# ...
68+
'health_check', # required
69+
'health_check.db', # stock Django health checkers
70+
'health_check.cache',
71+
'health_check.storage',
72+
'health_check.contrib.migrations',
73+
'health_check.contrib.celery', # requires celery
74+
'health_check.contrib.celery_ping', # requires celery
75+
'health_check.contrib.psutil', # disk and memory utilization; requires psutil
76+
'health_check.contrib.s3boto3_storage', # requires boto3 and S3BotoStorage backend
77+
'health_check.contrib.rabbitmq', # requires RabbitMQ broker
78+
'health_check.contrib.redis', # requires Redis broker
79+
]
80+
81+
**Note:** If using ``boto 2.x.x`` use
82+
``health_check.contrib.s3boto_storage``
83+
84+
(Optional) If using the ``psutil`` app, you can configure disk and
85+
memory threshold settings; otherwise below defaults are assumed. If you
86+
want to disable one of these checks, set its value to ``None``.
87+
88+
.. code:: python
89+
90+
HEALTH_CHECK = {
91+
'DISK_USAGE_MAX': 90, # percent
92+
'MEMORY_MIN': 100, # in MB
93+
}
94+
95+
If using the DB check, run migrations:
96+
97+
.. code:: shell
98+
99+
$ django-admin migrate
100+
101+
To use the RabbitMQ healthcheck, please make sure that there is a
102+
variable named ``BROKER_URL`` on django.conf.settings with the required
103+
format to connect to your rabbit server. For example:
104+
105+
.. code:: python
106+
107+
BROKER_URL = "amqp://myuser:mypassword@localhost:5672/myvhost"
108+
109+
To use the Redis healthcheck, please make sure that there is a variable
110+
named ``REDIS_URL`` on django.conf.settings with the required format to
111+
connect to your redis server. For example:
112+
113+
.. code:: python
114+
115+
REDIS_URL = "redis://localhost:6370"
116+
117+
The cache healthcheck tries to write and read a specific key within the
118+
cache backend. It can be customized by setting ``HEALTHCHECK_CACHE_KEY``
119+
to another value:
120+
121+
.. code:: python
122+
123+
HEALTHCHECK_CACHE_KEY = "custom_healthcheck_key"
124+
125+
Setting up monitoring
126+
---------------------
127+
128+
You can use tools like Pingdom, StatusCake or other uptime robots to
129+
monitor service status. The ``/ht/`` endpoint will respond with an HTTP
130+
200 if all checks passed and with an HTTP 500 if any of the tests
131+
failed. Getting machine-readable JSON reports
132+
133+
If you want machine-readable status reports you can request the ``/ht/``
134+
endpoint with the ``Accept`` HTTP header set to ``application/json`` or
135+
pass ``format=json`` as a query parameter.
136+
137+
The backend will return a JSON response:
138+
139+
.. code:: shell
140+
141+
$ curl -v -X GET -H "Accept: application/json" http://www.example.com/ht/
142+
143+
> GET /ht/ HTTP/1.1
144+
> Host: www.example.com
145+
> Accept: application/json
146+
>
147+
< HTTP/1.1 200 OK
148+
< Content-Type: application/json
149+
150+
{
151+
"CacheBackend": "working",
152+
"DatabaseBackend": "working",
153+
"S3BotoStorageHealthCheck": "working"
154+
}
155+
156+
$ curl -v -X GET http://www.example.com/ht/?format=json
157+
158+
> GET /ht/?format=json HTTP/1.1
159+
> Host: www.example.com
160+
>
161+
< HTTP/1.1 200 OK
162+
< Content-Type: application/json
163+
164+
{
165+
"CacheBackend": "working",
166+
"DatabaseBackend": "working",
167+
"S3BotoStorageHealthCheck": "working"
168+
}
169+
170+
Writing a custom health check
171+
-----------------------------
172+
173+
Writing a health check is quick and easy:
174+
175+
.. code:: python
176+
177+
from health_check.backends import BaseHealthCheckBackend
178+
179+
class MyHealthCheckBackend(BaseHealthCheckBackend):
180+
#: The status endpoints will respond with a 200 status code
181+
#: even if the check errors.
182+
critical_service = False
183+
184+
def check_status(self):
185+
# The test code goes here.
186+
# You can use `self.add_error` or
187+
# raise a `HealthCheckException`,
188+
# similar to Django's form validation.
189+
pass
190+
191+
def identifier(self):
192+
return self.__class__.__name__ # Display name on the endpoint.
193+
194+
After writing a custom checker, register it in your app configuration:
195+
196+
.. code:: python
197+
198+
from django.apps import AppConfig
199+
200+
from health_check.plugins import plugin_dir
201+
202+
class MyAppConfig(AppConfig):
203+
name = 'my_app'
204+
205+
def ready(self):
206+
from .backends import MyHealthCheckBackend
207+
plugin_dir.register(MyHealthCheckBackend)
208+
209+
Make sure the application you write the checker into is registered in
210+
your ``INSTALLED_APPS``.
211+
212+
Customizing output
213+
------------------
214+
215+
You can customize HTML or JSON rendering by inheriting from ``MainView``
216+
in ``health_check.views`` and customizing the ``template_name``,
217+
``get``, ``render_to_response`` and ``render_to_response_json``
218+
properties:
219+
220+
.. code:: python
221+
222+
# views.py
223+
from health_check.views import MainView
224+
225+
class HealthCheckCustomView(MainView):
226+
template_name = 'myapp/health_check_dashboard.html' # customize the used templates
227+
228+
def get(self, request, *args, **kwargs):
229+
plugins = []
230+
status = 200 # needs to be filled status you need
231+
# ...
232+
if 'application/json' in request.META.get('HTTP_ACCEPT', ''):
233+
return self.render_to_response_json(plugins, status)
234+
return self.render_to_response(plugins, status)
235+
236+
def render_to_response(self, plugins, status): # customize HTML output
237+
return HttpResponse('COOL' if status == 200 else 'SWEATY', status=status)
238+
239+
def render_to_response_json(self, plugins, status): # customize JSON output
240+
return JsonResponse(
241+
{str(p.identifier()): 'COOL' if status == 200 else 'SWEATY' for p in plugins},
242+
status=status
243+
)
244+
245+
# urls.py
246+
import views
247+
248+
urlpatterns = [
249+
# ...
250+
url(r'^ht/$', views.HealthCheckCustomView.as_view(), name='health_check_custom'),
251+
]
252+
253+
Django command
254+
--------------
255+
256+
You can run the Django command ``health_check`` to perform your health
257+
checks via the command line, or periodically with a cron, as follow:
258+
259+
.. code:: shell
260+
261+
django-admin health_check
262+
263+
This should yield the following output:
264+
265+
::
266+
267+
DatabaseHealthCheck ... working
268+
CustomHealthCheck ... unavailable: Something went wrong!
269+
270+
Similar to the http version, a critical error will cause the command to
271+
quit with the exit code ``1``.
272+
273+
Other resources
274+
---------------
275+
276+
- `django-watchman <https://github.com/mwarkentin/django-watchman>`__
277+
is a package that does some of the same things in a slightly
278+
different way.
279+
- See this
280+
`weblog <https://www.vincit.fi/en/blog/deploying-django-to-elastic-beanstalk-with-https-redirects-and-functional-health-checks/>`__
281+
about configuring Django and health checking with AWS Elastic Load
282+
Balancer.
283+
284+
.. |version| image:: https://img.shields.io/pypi/v/django-health-check.svg
285+
:target: https://pypi.python.org/pypi/django-health-check/
286+
.. |pyversion| image:: https://img.shields.io/pypi/pyversions/django-health-check.svg
287+
:target: https://pypi.python.org/pypi/django-health-check/
288+
.. |djversion| image:: https://img.shields.io/pypi/djversions/django-health-check.svg
289+
:target: https://pypi.python.org/pypi/django-health-check/
290+
.. |license| image:: https://img.shields.io/badge/license-MIT-blue.svg
291+
:target: https://pypi.python.org/pypi/django-health-check/

health_check/contrib/rabbitmq/backends.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ def check_status(self):
1919
"""Check RabbitMQ service by opening and closing a broker channel."""
2020
logger.debug("Checking for a broker_url on django settings...")
2121

22-
broker_url_setting_key = f"{self.namespace}_BROKER_URL" if self.namespace else "BROKER_URL"
22+
broker_url_setting_key = (
23+
f"{self.namespace}_BROKER_URL" if self.namespace else "BROKER_URL"
24+
)
2325
broker_url = getattr(settings, broker_url_setting_key, None)
2426

2527
logger.debug("Got %s as the broker_url. Connecting to rabbit...", broker_url)

health_check/db/migrations/0001_initial.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66

77
class Migration(migrations.Migration):
8-
98
initial = True
109

1110
replaces = [

health_check/storage/backends.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import uuid
22

3+
import django
34
from django.conf import settings
45
from django.core.files.base import ContentFile
5-
from django.core.files.storage import get_storage_class
6+
7+
if django.VERSION >= (4, 2):
8+
from django.core.files.storage import InvalidStorageError, storages
9+
else:
10+
from django.core.files.storage import get_storage_class
611

712
from health_check.backends import BaseHealthCheckBackend
813
from health_check.exceptions import ServiceUnavailable
@@ -22,13 +27,20 @@ class MyStorageHealthCheck(StorageHealthCheck):
2227
(e.g 'django.core.files.storage.FileSystemStorage') or a Storage instance.
2328
"""
2429

30+
storage_alias = None
2531
storage = None
2632

2733
def get_storage(self):
28-
if isinstance(self.storage, str):
29-
return get_storage_class(self.storage)()
34+
if django.VERSION >= (4, 2):
35+
try:
36+
return storages[self.storage_alias]
37+
except InvalidStorageError:
38+
return None
3039
else:
31-
return self.storage
40+
if isinstance(self.storage, str):
41+
return get_storage_class(self.storage)()
42+
else:
43+
return self.storage
3244

3345
def get_file_name(self):
3446
return "health_check_storage_test/test-%s.txt" % uuid.uuid4()
@@ -68,4 +80,5 @@ def check_status(self):
6880

6981

7082
class DefaultFileStorageHealthCheck(StorageHealthCheck):
83+
storage_alias = "default"
7184
storage = settings.DEFAULT_FILE_STORAGE

0 commit comments

Comments
 (0)