pax_global_header00006660000000000000000000000064143053720120014507gustar00rootroot0000000000000052 comment=f42257ae117b6fa18a7b4a2cfa1fb5efd3b824f9 django-health-check-3.17.0/000077500000000000000000000000001430537201200153575ustar00rootroot00000000000000django-health-check-3.17.0/.bandit000066400000000000000000000000301430537201200166120ustar00rootroot00000000000000[bandit] exclude: tests django-health-check-3.17.0/.editorconfig000066400000000000000000000013051430537201200200330ustar00rootroot00000000000000# http://editorconfig.org root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true [*.py] indent_style = space indent_size = 4 # isort config atomic = true multi_line_output = 5 line_length = 80 combine_as_imports = true skip = wsgi.py,docs,env,.eggs known_first_party = health_check,tests known_third_party = django,celery,psutil default_section=THIRDPARTY not_skip = __init__.py [*.{rst,ini}] indent_style = space indent_size = 4 [*.{yml,html,xml,xsl,json}] indent_style = space indent_size = 2 [*.{css,less}] indent_style = space indent_size = 2 [*.{js,coffee}] indent_style = space indent_size = 4 [Makefile] indent_style = tab indent_size = 1 django-health-check-3.17.0/.github/000077500000000000000000000000001430537201200167175ustar00rootroot00000000000000django-health-check-3.17.0/.github/FUNDING.yml000066400000000000000000000000721430537201200205330ustar00rootroot00000000000000github: codingjoe custom: https://www.paypal.me/codingjoe django-health-check-3.17.0/.github/dependabot.yml000066400000000000000000000002631430537201200215500ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: pip directory: "/" schedule: interval: daily - package-ecosystem: github-actions directory: "/" schedule: interval: daily django-health-check-3.17.0/.github/workflows/000077500000000000000000000000001430537201200207545ustar00rootroot00000000000000django-health-check-3.17.0/.github/workflows/ci.yml000066400000000000000000000036061430537201200220770ustar00rootroot00000000000000name: CI on: push: branches: - main pull_request: jobs: lint: runs-on: ubuntu-latest strategy: matrix: lint-command: - bandit -r . -x ./tests - black --check --diff . - flake8 . - isort --check-only --diff . - pydocstyle . steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: "3.10" cache: 'pip' cache-dependency-path: 'linter-requirements.txt' - run: python -m pip install -r linter-requirements.txt - run: ${{ matrix.lint-command }} dist: runs-on: ubuntu-latest needs: [lint] steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: "3.x" - run: python -m pip install --upgrade pip build wheel twine - run: python -m build --sdist --wheel - run: python -m twine check dist/* docs: runs-on: ubuntu-latest needs: [lint] steps: - uses: actions/checkout@v3 - name: setup Python uses: actions/setup-python@v4 with: python-version: "3.10" - run: python -m pip install -e .[docs] - run: python -m sphinx -b html -W docs docs/_build PyTest: runs-on: ubuntu-latest needs: [lint] strategy: matrix: python-version: - "3.8" - "3.9" - "3.10" django-version: - "2.2" - "4.0" - "4.1" steps: - uses: actions/checkout@v3 - name: Setup Python version ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - run: python -m pip install .[test] - run: python -m pip install Django~="${{ matrix.django-version }}.0" - run: python -m pytest - uses: codecov/codecov-action@v3 django-health-check-3.17.0/.github/workflows/release.yml000066400000000000000000000007211430537201200231170ustar00rootroot00000000000000name: Release on: release: types: [published] jobs: PyPi: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: "3.x" - run: python -m pip install --upgrade pip build wheel twine - run: python -m build --sdist --wheel - run: python -m twine upload dist/* env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} django-health-check-3.17.0/.gitignore000066400000000000000000000022361430537201200173520ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # dotenv .env # virtualenv .venv venv/ ENV/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ # pytest .pytest_cache/ django-health-check-3.17.0/.readthedocs.yaml000066400000000000000000000005131430537201200206050ustar00rootroot00000000000000# .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details version: 2 build: os: ubuntu-20.04 tools: python: "3.10" sphinx: configuration: docs/conf.py python: install: - method: pip path: . extra_requirements: - docs django-health-check-3.17.0/LICENSE000066400000000000000000000020751430537201200163700ustar00rootroot00000000000000Copyright (C) 2011-2019 Kristian Øllegaard and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. django-health-check-3.17.0/MANIFEST.in000066400000000000000000000000471430537201200171160ustar00rootroot00000000000000include LICENSE prune tests prune docs django-health-check-3.17.0/README.rst000066400000000000000000000231431430537201200170510ustar00rootroot00000000000000=================== django-health-check =================== |version| |pyversion| |djversion| |coverage| |license| This project checks for various conditions and provides reports when anomalous behavior is detected. The following health checks are bundled with this project: - cache - database - storage - disk and memory utilization (via ``psutil``) - AWS S3 storage - Celery task queue - Celery ping - RabbitMQ - Migrations Writing your own custom health checks is also very quick and easy. We also like contributions, so don't be afraid to make a pull request. Use Cases --------- The primary intended use case is to monitor conditions via HTTP(S), with responses available in HTML and JSON formats. When you get back a response that includes one or more problems, you can then decide the appropriate course of action, which could include generating notifications and/or automating the replacement of a failing node with a new one. If you are monitoring health in a high-availability environment with a load balancer that returns responses from multiple nodes, please note that certain checks (e.g., disk and memory usage) will return responses specific to the node selected by the load balancer. Supported Versions ------------------ We officially only support the latest version of Python as well as the latest version of Django and the latest Django LTS version. Installation ------------ First install the ``django-health-check`` package: .. code:: pip install django-health-check Add the health checker to a URL you want to use: .. code:: python urlpatterns = [ # ... url(r'^ht/', include('health_check.urls')), ] Add the ``health_check`` applications to your ``INSTALLED_APPS``: .. code:: python INSTALLED_APPS = [ # ... 'health_check', # required 'health_check.db', # stock Django health checkers 'health_check.cache', 'health_check.storage', 'health_check.contrib.migrations', 'health_check.contrib.celery', # requires celery 'health_check.contrib.celery_ping', # requires celery 'health_check.contrib.psutil', # disk and memory utilization; requires psutil 'health_check.contrib.s3boto3_storage', # requires boto3 and S3BotoStorage backend 'health_check.contrib.rabbitmq', # requires RabbitMQ broker 'health_check.contrib.redis', # requires Redis broker ] Note : If using ``boto 2.x.x`` use ``health_check.contrib.s3boto_storage`` (Optional) If using the ``psutil`` app, you can configure disk and memory threshold settings; otherwise below defaults are assumed. If you want to disable one of these checks, set its value to ``None``. .. code:: python HEALTH_CHECK = { 'DISK_USAGE_MAX': 90, # percent 'MEMORY_MIN': 100, # in MB } If using the DB check, run migrations: .. code:: django-admin migrate To use the RabbitMQ healthcheck, please make sure that there is a variable named ``BROKER_URL`` on django.conf.settings with the required format to connect to your rabbit server. For example: .. code:: BROKER_URL = amqp://myuser:mypassword@localhost:5672/myvhost To use the Redis healthcheck, please make sure that there is a variable named ``REDIS_URL`` on django.conf.settings with the required format to connect to your redis server. For example: .. code:: REDIS_URL = redis://localhost:6370 Setting up monitoring --------------------- You can use tools like Pingdom_ or other uptime robots to monitor service status. The ``/ht/`` endpoint will respond a HTTP 200 if all checks passed and a HTTP 500 if any of the tests failed. .. code:: $ curl -v -X GET -H http://www.example.com/ht/ > GET /ht/ HTTP/1.1 > Host: www.example.com > Accept: */* > < HTTP/1.1 200 OK < Content-Type: text/html; charset=utf-8

System status

CacheBackend working
DatabaseBackend working
S3BotoStorageHealthCheck working
Getting machine readable JSON reports ------------------------------------- If you want machine readable status reports you can request the ``/ht/`` endpoint with the ``Accept`` HTTP header set to ``application/json`` or pass ``format=json`` as a query parameter. The backend will return a JSON response: .. code:: $ curl -v -X GET -H "Accept: application/json" http://www.example.com/ht/ > GET /ht/ HTTP/1.1 > Host: www.example.com > Accept: application/json > < HTTP/1.1 200 OK < Content-Type: application/json { "CacheBackend": "working", "DatabaseBackend": "working", "S3BotoStorageHealthCheck": "working" } $ curl -v -X GET http://www.example.com/ht/?format=json > GET /ht/?format=json HTTP/1.1 > Host: www.example.com > < HTTP/1.1 200 OK < Content-Type: application/json { "CacheBackend": "working", "DatabaseBackend": "working", "S3BotoStorageHealthCheck": "working" } Writing a custom health check ----------------------------- Writing a health check is quick and easy: .. code:: python from health_check.backends import BaseHealthCheckBackend class MyHealthCheckBackend(BaseHealthCheckBackend): #: The status endpoints will respond with a 200 status code #: even if the check errors. critical_service = False def check_status(self): # The test code goes here. # You can use `self.add_error` or # raise a `HealthCheckException`, # similar to Django's form validation. pass def identifier(self): return self.__class__.__name__ # Display name on the endpoint. After writing a custom checker, register it in your app configuration: .. code:: python from django.apps import AppConfig from health_check.plugins import plugin_dir class MyAppConfig(AppConfig): name = 'my_app' def ready(self): from .backends import MyHealthCheckBackend plugin_dir.register(MyHealthCheckBackend) Make sure the application you write the checker into is registered in your ``INSTALLED_APPS``. Customizing output ------------------ You can customize HTML or JSON rendering by inheriting from ``MainView`` in ``health_check.views`` and customizing the ``template_name``, ``get``, ``render_to_response`` and ``render_to_response_json`` properties: .. code:: python # views.py from health_check.views import MainView class HealthCheckCustomView(MainView): template_name = 'myapp/health_check_dashboard.html' # customize the used templates def get(self, request, *args, **kwargs): plugins = [] status = 200 # needs to be filled status you need # ... if 'application/json' in request.META.get('HTTP_ACCEPT', ''): return self.render_to_response_json(plugins, status) return self.render_to_response(plugins, status) def render_to_response(self, plugins, status): # customize HTML output return HttpResponse('COOL' if status == 200 else 'SWEATY', status=status) def render_to_response_json(self, plugins, status): # customize JSON output return JsonResponse( {str(p.identifier()): 'COOL' if status == 200 else 'SWEATY' for p in plugins}, status=status ) # urls.py import views urlpatterns = [ # ... url(r'^ht/$', views.HealthCheckCustomView.as_view(), name='health_check_custom'), ] Django command -------------- You can run the Django command `health_check` to perform your health checks via the command line, or periodically with a cron, as follow: .. code:: django-admin health_check This should yield the following output: .. code:: DatabaseHealthCheck ... working CustomHealthCheck ... unavailable: Something went wrong! Similar to the http version, a critical error will cause the command to quit with the exit code `1`. Other resources --------------- - django-watchman_ is a package that does some of the same things in a slightly different way. - See this weblog_ about configuring Django and health checking with AWS Elastic Load Balancer. .. |version| image:: https://img.shields.io/pypi/v/django-health-check.svg :target: https://pypi.python.org/pypi/django-health-check/ .. |pyversion| image:: https://img.shields.io/pypi/pyversions/django-health-check.svg :target: https://pypi.python.org/pypi/django-health-check/ .. |djversion| image:: https://img.shields.io/pypi/djversions/django-health-check.svg :target: https://pypi.python.org/pypi/django-health-check/ .. |coverage| image:: https://codecov.io/gh/KristianOellegaard/django-health-check/branch/master/graph/badge.svg :target: https://codecov.io/gh/KristianOellegaard/django-health-check .. |license| image:: https://img.shields.io/badge/license-MIT-blue.svg :target: LICENSE .. _Pingdom: https://www.pingdom.com/ .. _django-watchman: https://github.com/mwarkentin/django-watchman .. _weblog: https://www.vincit.fi/en/blog/deploying-django-to-elastic-beanstalk-with-https-redirects-and-functional-health-checks/ django-health-check-3.17.0/docs/000077500000000000000000000000001430537201200163075ustar00rootroot00000000000000django-health-check-3.17.0/docs/changelog.rst000066400000000000000000000003141430537201200207660ustar00rootroot00000000000000ChangeLog ========= This package is released on GitHub. Please refer to the GitHub release page to review the changes in each version. https://github.com/KristianOellegaard/django-health-check/releases django-health-check-3.17.0/docs/conf.py000066400000000000000000000003401430537201200176030ustar00rootroot00000000000000try: import sphinx_rtd_theme except ImportError: sphinx_rtd_theme = None master_doc = "index" if sphinx_rtd_theme: html_theme = "sphinx_rtd_theme" html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] django-health-check-3.17.0/docs/contrib.rst000066400000000000000000000041151430537201200205020ustar00rootroot00000000000000contrib ======= ``psutil`` ---------- Full disks and out-of-memory conditions are common causes of service outages. These situations can be averted by checking disk and memory utilization via the ``psutil`` package: .. code:: pip install psutil Once that dependency has been installed, make sure that the corresponding Django app has been added to ``INSTALLED_APPS``: .. code:: python INSTALLED_APPS = [ # ... 'health_check', # required 'health_check.contrib.psutil', # disk and memory utilization; requires psutil # ... ] The following default settings will be used to check for disk and memory utilization. If you would prefer different thresholds, you can add the dictionary below to your Django settings file and adjust the values accordingly. If you want to disable any of these checks, set its value to ``None``. .. code:: python HEALTH_CHECK = { 'DISK_USAGE_MAX': 90, # percent 'MEMORY_MIN' = 100, # in MB } ``celery`` ---------- If you are using Celery you may choose between two different Celery checks. `health_check.contrib.celery` sends a task to the queue and it expects that task to be executed in `HEALTHCHECK_CELERY_TIMEOUT` seconds which by default is three seconds. You may override that in your Django settings module. This check is suitable for use cases which require that tasks can be processed frequently all the time. `health_check.contrib.celery_ping` is a different check. It checks that each predefined Celery task queue has a consumer (i.e. worker) that responds `{"ok": "pong"}` in `HEALTHCHECK_CELERY_PING_TIMEOUT` seconds. The default for this is one second. You may override that in your Django settings module. This check is suitable for use cases which don't require that tasks are executed almost instantly but require that they are going to be executed in sometime the future i.e. that the worker process is alive and processing tasks all the time. You may also use both of them. To use these checks add them to `INSTALLED_APPS` in your Django settings module. django-health-check-3.17.0/docs/index.rst000066400000000000000000000005351430537201200201530ustar00rootroot00000000000000django-health-check ------------------- This project checks for various conditions and provides reports when anomalous behavior is detected. Many of these checks involve connecting to back-end services and ensuring basic operations are successful. .. toctree:: :maxdepth: 2 :caption: Contents: readme contrib settings changelog django-health-check-3.17.0/docs/readme.rst000066400000000000000000000000331430537201200202720ustar00rootroot00000000000000.. include:: ../README.rst django-health-check-3.17.0/docs/settings.rst000066400000000000000000000053751430537201200207130ustar00rootroot00000000000000Settings ======== Settings can be configured via the ``HEALTH_CHECK`` dictionary. .. data:: WARNINGS_AS_ERRORS Treats :class:`ServiceWarning` as errors, meaning they will cause the views to respond with a 500 status code. Default is ``True``. If set to ``False`` warnings will be displayed in the template on in the JSON response but the status code will remain a 200. Security -------- Django health check can be used as a possible DOS attack vector as it can put your system under a lot of stress. As a default the view is also not cached by CDNs. Therefore we recommend to use a secure token to protect you application servers from an attacker. 1. Setup HTTPS. Seriously... 2. Add a secure token to your URL. Create a secure token: .. code:: shell python -c "import secrets; print(secrets.token_urlsafe())" Add it to your URL: .. code:: python urlpatterns = [ # ... url(r'^ht/super_secret_token/'), include('health_check.urls')), ] You can still use any uptime bot that is URL based while enjoying token protection. .. warning:: Do NOT use Django's `SECRET_KEY` setting. This should never be exposed, to any third party. Not even your trusted uptime bot. ``psutil`` ---------- The following default settings will be used to check for disk and memory utilization. If you would prefer different thresholds, you can add the dictionary below to your Django settings file and adjust the values accordingly. If you want to disable any of these checks, set its value to ``None``. .. code:: python HEALTH_CHECK = { 'DISK_USAGE_MAX': 90, # percent 'MEMORY_MIN' = 100, # in MB } With the above default settings, warnings will be reported when disk utilization exceeds 90% or available memory drops below 100 MB. .. data:: DISK_USAGE_MAX Specify the desired disk utilization threshold, in percent. When disk usage exceeds the specified value, a warning will be reported. .. data:: MEMORY_MIN Specify the desired memory utilization threshold, in megabytes. When available memory falls below the specified value, a warning will be reported. Celery Health Check ---------------------- Using `django.settings` you may exert more fine-grained control over the behavior of the celery health check .. list-table:: Additional Settings :widths: 25 10 10 55 :header-rows: 1 * - Name - Type - Default - Description * - `HEALTHCHECK_CELERY_QUEUE_TIMEOUT` - Number - 3 - Specifies the maximum amount of time a task may spend in the queue before being automatically revoked with a `TaskRevokedError`. * - `HEALTHCHECK_CELERY_RESULT_TIMEOUT` - Number - 3 - Specifies the maximum total time for a task to complete and return a result, including queue time. django-health-check-3.17.0/health_check/000077500000000000000000000000001430537201200177615ustar00rootroot00000000000000django-health-check-3.17.0/health_check/__init__.py000066400000000000000000000000001430537201200220600ustar00rootroot00000000000000django-health-check-3.17.0/health_check/backends.py000066400000000000000000000032521430537201200221070ustar00rootroot00000000000000import logging from timeit import default_timer as timer from django.utils.translation import gettext_lazy as _ # noqa: N812 from health_check.exceptions import HealthCheckException logger = logging.getLogger("health-check") class BaseHealthCheckBackend: critical_service = True """ Define if service is critical to the operation of the site. If set to ``True`` service failures return 500 response code on the health check endpoint. """ def __init__(self): self.errors = [] def check_status(self): raise NotImplementedError def run_check(self): start = timer() self.errors = [] try: self.check_status() except HealthCheckException as e: self.add_error(e, e) except BaseException: logger.exception("Unexpected Error!") raise finally: self.time_taken = timer() - start def add_error(self, error, cause=None): if isinstance(error, HealthCheckException): pass elif isinstance(error, str): msg = error error = HealthCheckException(msg) else: msg = _("unknown error") error = HealthCheckException(msg) if isinstance(cause, BaseException): logger.exception(str(error)) else: logger.error(str(error)) self.errors.append(error) def pretty_status(self): if self.errors: return "\n".join(str(e) for e in self.errors) return _("working") @property def status(self): return int(not self.errors) def identifier(self): return self.__class__.__name__ django-health-check-3.17.0/health_check/cache/000077500000000000000000000000001430537201200210245ustar00rootroot00000000000000django-health-check-3.17.0/health_check/cache/__init__.py000066400000000000000000000001601430537201200231320ustar00rootroot00000000000000import django if django.VERSION < (3, 2): default_app_config = "health_check.cache.apps.HealthCheckConfig" django-health-check-3.17.0/health_check/cache/apps.py000066400000000000000000000005371430537201200223460ustar00rootroot00000000000000from django.apps import AppConfig from django.conf import settings from health_check.plugins import plugin_dir class HealthCheckConfig(AppConfig): name = "health_check.cache" def ready(self): from .backends import CacheBackend for backend in settings.CACHES: plugin_dir.register(CacheBackend, backend=backend) django-health-check-3.17.0/health_check/cache/backends.py000066400000000000000000000020271430537201200231510ustar00rootroot00000000000000from django.core.cache import CacheKeyWarning, caches from health_check.backends import BaseHealthCheckBackend from health_check.exceptions import ServiceReturnedUnexpectedResult, ServiceUnavailable class CacheBackend(BaseHealthCheckBackend): def __init__(self, backend="default"): super().__init__() self.backend = backend def identifier(self): return f"Cache backend: {self.backend}" def check_status(self): cache = caches[self.backend] try: cache.set("djangohealtcheck_test", "itworks") if not cache.get("djangohealtcheck_test") == "itworks": raise ServiceUnavailable("Cache key does not match") except CacheKeyWarning as e: self.add_error(ServiceReturnedUnexpectedResult("Cache key warning"), e) except ValueError as e: self.add_error(ServiceReturnedUnexpectedResult("ValueError"), e) except ConnectionError as e: self.add_error(ServiceReturnedUnexpectedResult("Connection Error"), e) django-health-check-3.17.0/health_check/conf.py000066400000000000000000000003441430537201200212610ustar00rootroot00000000000000from django.conf import settings HEALTH_CHECK = getattr(settings, "HEALTH_CHECK", {}) HEALTH_CHECK.setdefault("DISK_USAGE_MAX", 90) HEALTH_CHECK.setdefault("MEMORY_MIN", 100) HEALTH_CHECK.setdefault("WARNINGS_AS_ERRORS", True) django-health-check-3.17.0/health_check/contrib/000077500000000000000000000000001430537201200214215ustar00rootroot00000000000000django-health-check-3.17.0/health_check/contrib/__init__.py000066400000000000000000000000001430537201200235200ustar00rootroot00000000000000django-health-check-3.17.0/health_check/contrib/celery/000077500000000000000000000000001430537201200227045ustar00rootroot00000000000000django-health-check-3.17.0/health_check/contrib/celery/__init__.py000066400000000000000000000001711430537201200250140ustar00rootroot00000000000000import django if django.VERSION < (3, 2): default_app_config = "health_check.contrib.celery.apps.HealthCheckConfig" django-health-check-3.17.0/health_check/contrib/celery/apps.py000066400000000000000000000016711430537201200242260ustar00rootroot00000000000000import warnings from celery import current_app from django.apps import AppConfig from django.conf import settings from health_check.plugins import plugin_dir class HealthCheckConfig(AppConfig): name = "health_check.contrib.celery" def ready(self): from .backends import CeleryHealthCheck if hasattr(settings, "HEALTHCHECK_CELERY_TIMEOUT"): warnings.warn( "HEALTHCHECK_CELERY_TIMEOUT is depricated and may be removed in the " "future. Please use HEALTHCHECK_CELERY_RESULT_TIMEOUT and " "HEALTHCHECK_CELERY_QUEUE_TIMEOUT instead.", DeprecationWarning, ) for queue in current_app.amqp.queues: celery_class_name = "CeleryHealthCheck" + queue.title() celery_class = type( celery_class_name, (CeleryHealthCheck,), {"queue": queue} ) plugin_dir.register(celery_class) django-health-check-3.17.0/health_check/contrib/celery/backends.py000066400000000000000000000035331430537201200250340ustar00rootroot00000000000000from celery.exceptions import TaskRevokedError, TimeoutError from django.conf import settings from health_check.backends import BaseHealthCheckBackend from health_check.exceptions import ServiceReturnedUnexpectedResult, ServiceUnavailable from .tasks import add class CeleryHealthCheck(BaseHealthCheckBackend): def check_status(self): timeout = getattr(settings, "HEALTHCHECK_CELERY_TIMEOUT", 3) result_timeout = getattr(settings, "HEALTHCHECK_CELERY_RESULT_TIMEOUT", timeout) queue_timeout = getattr(settings, "HEALTHCHECK_CELERY_QUEUE_TIMEOUT", timeout) try: result = add.apply_async( args=[4, 4], expires=queue_timeout, queue=self.queue ) result.get(timeout=result_timeout) if result.result != 8: self.add_error( ServiceReturnedUnexpectedResult("Celery returned wrong result") ) except IOError as e: self.add_error(ServiceUnavailable("IOError"), e) except NotImplementedError as e: self.add_error( ServiceUnavailable( "NotImplementedError: Make sure CELERY_RESULT_BACKEND is set" ), e, ) except TaskRevokedError as e: self.add_error( ServiceUnavailable( "TaskRevokedError: The task was revoked, likely because it spent " "too long in the queue" ), e, ) except TimeoutError as e: self.add_error( ServiceUnavailable( "TimeoutError: The task took too long to return a result" ), e, ) except BaseException as e: self.add_error(ServiceUnavailable("Unknown error"), e) django-health-check-3.17.0/health_check/contrib/celery/tasks.py000066400000000000000000000001431430537201200244010ustar00rootroot00000000000000from celery import shared_task @shared_task(ignore_result=False) def add(x, y): return x + y django-health-check-3.17.0/health_check/contrib/celery_ping/000077500000000000000000000000001430537201200237215ustar00rootroot00000000000000django-health-check-3.17.0/health_check/contrib/celery_ping/__init__.py000066400000000000000000000001761430537201200260360ustar00rootroot00000000000000import django if django.VERSION < (3, 2): default_app_config = "health_check.contrib.celery_ping.apps.HealthCheckConfig" django-health-check-3.17.0/health_check/contrib/celery_ping/apps.py000066400000000000000000000004411430537201200252350ustar00rootroot00000000000000from django.apps import AppConfig from health_check.plugins import plugin_dir class HealthCheckConfig(AppConfig): name = "health_check.contrib.celery_ping" def ready(self): from .backends import CeleryPingHealthCheck plugin_dir.register(CeleryPingHealthCheck) django-health-check-3.17.0/health_check/contrib/celery_ping/backends.py000066400000000000000000000043701430537201200260510ustar00rootroot00000000000000from celery.app import default_app as app from django.conf import settings from health_check.backends import BaseHealthCheckBackend from health_check.exceptions import ServiceUnavailable class CeleryPingHealthCheck(BaseHealthCheckBackend): CORRECT_PING_RESPONSE = {"ok": "pong"} def check_status(self): timeout = getattr(settings, "HEALTHCHECK_CELERY_PING_TIMEOUT", 1) try: ping_result = app.control.ping(timeout=timeout) except IOError as e: self.add_error(ServiceUnavailable("IOError"), e) except NotImplementedError as exc: self.add_error( ServiceUnavailable( "NotImplementedError: Make sure CELERY_RESULT_BACKEND is set" ), exc, ) except BaseException as exc: self.add_error(ServiceUnavailable("Unknown error"), exc) else: if not ping_result: self.add_error( ServiceUnavailable("Celery workers unavailable"), ) else: self._check_ping_result(ping_result) def _check_ping_result(self, ping_result): active_workers = [] for result in ping_result: worker, response = list(result.items())[0] if response != self.CORRECT_PING_RESPONSE: self.add_error( ServiceUnavailable( f"Celery worker {worker} response was incorrect" ), ) continue active_workers.append(worker) if not self.errors: self._check_active_queues(active_workers) def _check_active_queues(self, active_workers): defined_queues = app.conf.CELERY_QUEUES if not defined_queues: return defined_queues = set([queue.name for queue in defined_queues]) active_queues = set() for queues in app.control.inspect(active_workers).active_queues().values(): active_queues.update([queue.get("name") for queue in queues]) for queue in defined_queues.difference(active_queues): self.add_error( ServiceUnavailable(f"No worker for Celery task queue {queue}"), ) django-health-check-3.17.0/health_check/contrib/migrations/000077500000000000000000000000001430537201200235755ustar00rootroot00000000000000django-health-check-3.17.0/health_check/contrib/migrations/__init__.py000066400000000000000000000001751430537201200257110ustar00rootroot00000000000000import django if django.VERSION < (3, 2): default_app_config = "health_check.contrib.migrations.apps.HealthCheckConfig" django-health-check-3.17.0/health_check/contrib/migrations/apps.py000066400000000000000000000004401430537201200251100ustar00rootroot00000000000000from django.apps import AppConfig from health_check.plugins import plugin_dir class HealthCheckConfig(AppConfig): name = "health_check.contrib.migrations" def ready(self): from .backends import MigrationsHealthCheck plugin_dir.register(MigrationsHealthCheck) django-health-check-3.17.0/health_check/contrib/migrations/backends.py000066400000000000000000000020441430537201200257210ustar00rootroot00000000000000import logging from django.conf import settings from django.db import DEFAULT_DB_ALIAS, DatabaseError, connections from django.db.migrations.executor import MigrationExecutor from health_check.backends import BaseHealthCheckBackend from health_check.exceptions import ServiceUnavailable logger = logging.getLogger(__name__) class MigrationsHealthCheck(BaseHealthCheckBackend): def get_migration_plan(self, executor): return executor.migration_plan(executor.loader.graph.leaf_nodes()) def check_status(self): db_alias = getattr(settings, "HEALTHCHECK_MIGRATIONS_DB", DEFAULT_DB_ALIAS) try: executor = MigrationExecutor(connections[db_alias]) plan = self.get_migration_plan(executor) if plan: self.add_error(ServiceUnavailable("There are migrations to apply")) except DatabaseError as e: self.add_error(ServiceUnavailable("Database is not ready"), e) except Exception as e: self.add_error(ServiceUnavailable("Unexpected error"), e) django-health-check-3.17.0/health_check/contrib/psutil/000077500000000000000000000000001430537201200227415ustar00rootroot00000000000000django-health-check-3.17.0/health_check/contrib/psutil/__init__.py000066400000000000000000000001711430537201200250510ustar00rootroot00000000000000import django if django.VERSION < (3, 2): default_app_config = "health_check.contrib.psutil.apps.HealthCheckConfig" django-health-check-3.17.0/health_check/contrib/psutil/apps.py000066400000000000000000000015611430537201200242610ustar00rootroot00000000000000from django.apps import AppConfig from django.conf import settings from health_check.plugins import plugin_dir class HealthCheckConfig(AppConfig): name = "health_check.contrib.psutil" def ready(self): from .backends import DiskUsage, MemoryUsage # Ensure checks haven't been explicitly disabled before registering if ( hasattr(settings, "HEALTH_CHECK") and ("DISK_USAGE_MAX" in settings.HEALTH_CHECK) and (settings.HEALTH_CHECK["DISK_USAGE_MAX"] is None) ): pass else: plugin_dir.register(DiskUsage) if ( hasattr(settings, "HEALTH_CHECK") and ("DISK_USAGE_MAX" in settings.HEALTH_CHECK) and (settings.HEALTH_CHECK["MEMORY_MIN"] is None) ): pass else: plugin_dir.register(MemoryUsage) django-health-check-3.17.0/health_check/contrib/psutil/backends.py000066400000000000000000000031121430537201200250620ustar00rootroot00000000000000import locale import socket import psutil from health_check.backends import BaseHealthCheckBackend from health_check.conf import HEALTH_CHECK from health_check.exceptions import ServiceReturnedUnexpectedResult, ServiceWarning host = socket.gethostname() DISK_USAGE_MAX = HEALTH_CHECK["DISK_USAGE_MAX"] MEMORY_MIN = HEALTH_CHECK["MEMORY_MIN"] class DiskUsage(BaseHealthCheckBackend): def check_status(self): try: du = psutil.disk_usage("/") if DISK_USAGE_MAX and du.percent >= DISK_USAGE_MAX: raise ServiceWarning( "{host} {percent}% disk usage exceeds {disk_usage}%".format( host=host, percent=du.percent, disk_usage=DISK_USAGE_MAX ) ) except ValueError as e: self.add_error(ServiceReturnedUnexpectedResult("ValueError"), e) class MemoryUsage(BaseHealthCheckBackend): def check_status(self): try: memory = psutil.virtual_memory() if MEMORY_MIN and memory.available < (MEMORY_MIN * 1024 * 1024): locale.setlocale(locale.LC_ALL, "") avail = "{:n}".format(int(memory.available / 1024 / 1024)) threshold = "{:n}".format(MEMORY_MIN) raise ServiceWarning( "{host} {avail} MB available RAM below {threshold} MB".format( host=host, avail=avail, threshold=threshold ) ) except ValueError as e: self.add_error(ServiceReturnedUnexpectedResult("ValueError"), e) django-health-check-3.17.0/health_check/contrib/rabbitmq/000077500000000000000000000000001430537201200232225ustar00rootroot00000000000000django-health-check-3.17.0/health_check/contrib/rabbitmq/__init__.py000066400000000000000000000001731430537201200253340ustar00rootroot00000000000000import django if django.VERSION < (3, 2): default_app_config = "health_check.contrib.rabbitmq.apps.HealthCheckConfig" django-health-check-3.17.0/health_check/contrib/rabbitmq/apps.py000066400000000000000000000004321430537201200245360ustar00rootroot00000000000000from django.apps import AppConfig from health_check.plugins import plugin_dir class HealthCheckConfig(AppConfig): name = "health_check.contrib.rabbitmq" def ready(self): from .backends import RabbitMQHealthCheck plugin_dir.register(RabbitMQHealthCheck) django-health-check-3.17.0/health_check/contrib/rabbitmq/backends.py000066400000000000000000000032541430537201200253520ustar00rootroot00000000000000import logging from amqp.exceptions import AccessRefused from django.conf import settings from kombu import Connection from health_check.backends import BaseHealthCheckBackend from health_check.exceptions import ServiceUnavailable logger = logging.getLogger(__name__) class RabbitMQHealthCheck(BaseHealthCheckBackend): """Health check for RabbitMQ.""" def check_status(self): """Check RabbitMQ service by opening and closing a broker channel.""" logger.debug("Checking for a broker_url on django settings...") broker_url = getattr(settings, "BROKER_URL", None) logger.debug("Got %s as the broker_url. Connecting to rabbit...", broker_url) logger.debug("Attempting to connect to rabbit...") try: # conn is used as a context to release opened resources later with Connection(broker_url) as conn: conn.connect() # exceptions may be raised upon calling connect except ConnectionRefusedError as e: self.add_error( ServiceUnavailable( "Unable to connect to RabbitMQ: Connection was refused." ), e, ) except AccessRefused as e: self.add_error( ServiceUnavailable( "Unable to connect to RabbitMQ: Authentication error." ), e, ) except IOError as e: self.add_error(ServiceUnavailable("IOError"), e) except BaseException as e: self.add_error(ServiceUnavailable("Unknown error"), e) else: logger.debug("Connection established. RabbitMQ is healthy.") django-health-check-3.17.0/health_check/contrib/redis/000077500000000000000000000000001430537201200225275ustar00rootroot00000000000000django-health-check-3.17.0/health_check/contrib/redis/__init__.py000066400000000000000000000001701430537201200246360ustar00rootroot00000000000000import django if django.VERSION < (3, 2): default_app_config = "health_check.contrib.redis.apps.HealthCheckConfig" django-health-check-3.17.0/health_check/contrib/redis/apps.py000066400000000000000000000004211430537201200240410ustar00rootroot00000000000000from django.apps import AppConfig from health_check.plugins import plugin_dir class HealthCheckConfig(AppConfig): name = "health_check.contrib.redis" def ready(self): from .backends import RedisHealthCheck plugin_dir.register(RedisHealthCheck) django-health-check-3.17.0/health_check/contrib/redis/backends.py000066400000000000000000000031301430537201200246500ustar00rootroot00000000000000import logging from django.conf import settings from redis import exceptions, from_url from health_check.backends import BaseHealthCheckBackend from health_check.exceptions import ServiceUnavailable logger = logging.getLogger(__name__) class RedisHealthCheck(BaseHealthCheckBackend): """Health check for Redis.""" redis_url = getattr(settings, "REDIS_URL", "redis://localhost/1") def check_status(self): """Check Redis service by pinging the redis instance with a redis connection.""" logger.debug("Got %s as the redis_url. Connecting to redis...", self.redis_url) logger.debug("Attempting to connect to redis...") try: # conn is used as a context to release opened resources later with from_url(self.redis_url) as conn: conn.ping() # exceptions may be raised upon ping except ConnectionRefusedError as e: self.add_error( ServiceUnavailable( "Unable to connect to Redis: Connection was refused." ), e, ) except exceptions.TimeoutError as e: self.add_error( ServiceUnavailable("Unable to connect to Redis: Timeout."), e ) except exceptions.ConnectionError as e: self.add_error( ServiceUnavailable("Unable to connect to Redis: Connection Error"), e ) except BaseException as e: self.add_error(ServiceUnavailable("Unknown error"), e) else: logger.debug("Connection established. Redis is healthy.") django-health-check-3.17.0/health_check/contrib/s3boto3_storage/000077500000000000000000000000001430537201200244415ustar00rootroot00000000000000django-health-check-3.17.0/health_check/contrib/s3boto3_storage/__init__.py000066400000000000000000000002021430537201200265440ustar00rootroot00000000000000import django if django.VERSION < (3, 2): default_app_config = "health_check.contrib.s3boto3_storage.apps.HealthCheckConfig" django-health-check-3.17.0/health_check/contrib/s3boto3_storage/apps.py000066400000000000000000000004551430537201200257620ustar00rootroot00000000000000from django.apps import AppConfig from health_check.plugins import plugin_dir class HealthCheckConfig(AppConfig): name = "health_check.contrib.s3boto3_storage" def ready(self): from .backends import S3Boto3StorageHealthCheck plugin_dir.register(S3Boto3StorageHealthCheck) django-health-check-3.17.0/health_check/contrib/s3boto3_storage/backends.py000066400000000000000000000014371430537201200265720ustar00rootroot00000000000000import logging from health_check.storage.backends import StorageHealthCheck class S3Boto3StorageHealthCheck(StorageHealthCheck): """ Tests the status of a `S3BotoStorage` file storage backend. S3BotoStorage is included in the `django-storages` package and recommended by for example Amazon and Heroku for Django static and media file storage on cloud platforms. ``django-storages`` can be found at https://git.io/v1lGx ``S3Boto3Storage`` can be found at https://github.com/jschneier/django-storages/blob/master/storages/backends/s3boto3.py """ logger = logging.getLogger(__name__) storage = "storages.backends.s3boto3.S3Boto3Storage" def check_delete(self, file_name): storage = self.get_storage() storage.delete(file_name) django-health-check-3.17.0/health_check/contrib/s3boto_storage/000077500000000000000000000000001430537201200243565ustar00rootroot00000000000000django-health-check-3.17.0/health_check/contrib/s3boto_storage/__init__.py000066400000000000000000000002011430537201200264600ustar00rootroot00000000000000import django if django.VERSION < (3, 2): default_app_config = "health_check.contrib.s3boto_storage.apps.HealthCheckConfig" django-health-check-3.17.0/health_check/contrib/s3boto_storage/apps.py000066400000000000000000000004521430537201200256740ustar00rootroot00000000000000from django.apps import AppConfig from health_check.plugins import plugin_dir class HealthCheckConfig(AppConfig): name = "health_check.contrib.s3boto_storage" def ready(self): from .backends import S3BotoStorageHealthCheck plugin_dir.register(S3BotoStorageHealthCheck) django-health-check-3.17.0/health_check/contrib/s3boto_storage/backends.py000066400000000000000000000013221430537201200265000ustar00rootroot00000000000000import logging from health_check.storage.backends import StorageHealthCheck class S3BotoStorageHealthCheck(StorageHealthCheck): """ Tests the status of a `S3BotoStorage` file storage backend. S3BotoStorage is included in the `django-storages` package and recommended by for example Amazon and Heroku for Django static and media file storage on cloud platforms. ``django-storages`` can be found at https://git.io/v1lGx ``S3BotoStorage`` can be found at https://git.io/v1lGF """ logger = logging.getLogger(__name__) storage = "storages.backends.s3boto.S3BotoStorage" def check_delete(self, file_name): storage = self.get_storage() storage.delete(file_name) django-health-check-3.17.0/health_check/db/000077500000000000000000000000001430537201200203465ustar00rootroot00000000000000django-health-check-3.17.0/health_check/db/__init__.py000066400000000000000000000001551430537201200224600ustar00rootroot00000000000000import django if django.VERSION < (3, 2): default_app_config = "health_check.db.apps.HealthCheckConfig" django-health-check-3.17.0/health_check/db/apps.py000066400000000000000000000004721430537201200216660ustar00rootroot00000000000000from django.apps import AppConfig from health_check.plugins import plugin_dir class HealthCheckConfig(AppConfig): default_auto_field = "django.db.models.AutoField" name = "health_check.db" def ready(self): from .backends import DatabaseBackend plugin_dir.register(DatabaseBackend) django-health-check-3.17.0/health_check/db/backends.py000066400000000000000000000012031430537201200224660ustar00rootroot00000000000000from django.db import DatabaseError, IntegrityError from health_check.backends import BaseHealthCheckBackend from health_check.exceptions import ServiceReturnedUnexpectedResult, ServiceUnavailable from .models import TestModel class DatabaseBackend(BaseHealthCheckBackend): def check_status(self): try: obj = TestModel.objects.create(title="test") obj.title = "newtest" obj.save() obj.delete() except IntegrityError: raise ServiceReturnedUnexpectedResult("Integrity Error") except DatabaseError: raise ServiceUnavailable("Database error") django-health-check-3.17.0/health_check/db/migrations/000077500000000000000000000000001430537201200225225ustar00rootroot00000000000000django-health-check-3.17.0/health_check/db/migrations/0001_initial.py000066400000000000000000000015431430537201200251700ustar00rootroot00000000000000# Generated by Django 1.10.1 on 2016-09-26 18:46 from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): initial = True replaces = [ ("health_check_db", "0001_initial"), ] dependencies = [] operations = [ migrations.CreateModel( name="TestModel", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("title", models.CharField(max_length=128)), ], options={ "db_table": "health_check_db_testmodel", }, ), ] django-health-check-3.17.0/health_check/db/migrations/__init__.py000066400000000000000000000000001430537201200246210ustar00rootroot00000000000000django-health-check-3.17.0/health_check/db/models.py000066400000000000000000000002531430537201200222030ustar00rootroot00000000000000from django.db import models class TestModel(models.Model): title = models.CharField(max_length=128) class Meta: db_table = "health_check_db_testmodel" django-health-check-3.17.0/health_check/exceptions.py000066400000000000000000000013551430537201200225200ustar00rootroot00000000000000from django.utils.translation import gettext_lazy as _ # noqa: N812 class HealthCheckException(Exception): message_type = _("unknown error") def __init__(self, message): self.message = message def __str__(self): return "%s: %s" % (self.message_type, self.message) class ServiceWarning(HealthCheckException): """ Warning of service misbehavior. If the ``HEALTH_CHECK['WARNINGS_AS_ERRORS']`` is set to ``False``, these exceptions will not case a 500 status response. """ message_type = _("warning") class ServiceUnavailable(HealthCheckException): message_type = _("unavailable") class ServiceReturnedUnexpectedResult(HealthCheckException): message_type = _("unexpected result") django-health-check-3.17.0/health_check/management/000077500000000000000000000000001430537201200220755ustar00rootroot00000000000000django-health-check-3.17.0/health_check/management/__init__.py000066400000000000000000000000001430537201200241740ustar00rootroot00000000000000django-health-check-3.17.0/health_check/management/commands/000077500000000000000000000000001430537201200236765ustar00rootroot00000000000000django-health-check-3.17.0/health_check/management/commands/__init__.py000066400000000000000000000000001430537201200257750ustar00rootroot00000000000000django-health-check-3.17.0/health_check/management/commands/health_check.py000066400000000000000000000012311430537201200266470ustar00rootroot00000000000000import sys from django.core.management.base import BaseCommand from health_check.mixins import CheckMixin class Command(CheckMixin, BaseCommand): help = "Run health checks and exit 0 if everything went well." def handle(self, *args, **options): # perform all checks errors = self.errors for plugin in self.plugins: style_func = self.style.SUCCESS if not plugin.errors else self.style.ERROR self.stdout.write( "{:<24} ... {}\n".format( plugin.identifier(), style_func(plugin.pretty_status()) ) ) if errors: sys.exit(1) django-health-check-3.17.0/health_check/mixins.py000066400000000000000000000030701430537201200216420ustar00rootroot00000000000000import copy from concurrent.futures import ThreadPoolExecutor from health_check.conf import HEALTH_CHECK from health_check.exceptions import ServiceWarning from health_check.plugins import plugin_dir class CheckMixin: _errors = None _plugins = None @property def errors(self): if not self._errors: self._errors = self.run_check() return self._errors @property def plugins(self): if not self._plugins: self._plugins = sorted( ( plugin_class(**copy.deepcopy(options)) for plugin_class, options in plugin_dir._registry ), key=lambda plugin: plugin.identifier(), ) return self._plugins def run_check(self): errors = [] def _run(plugin): plugin.run_check() try: return plugin finally: from django.db import connections connections.close_all() with ThreadPoolExecutor(max_workers=len(self.plugins) or 1) as executor: for plugin in executor.map(_run, self.plugins): if plugin.critical_service: if not HEALTH_CHECK["WARNINGS_AS_ERRORS"]: errors.extend( e for e in plugin.errors if not isinstance(e, ServiceWarning) ) else: errors.extend(plugin.errors) return errors django-health-check-3.17.0/health_check/plugins.py000066400000000000000000000011571430537201200220200ustar00rootroot00000000000000class AlreadyRegistered(Exception): pass class NotRegistered(Exception): pass class HealthCheckPluginDirectory: """Django health check registry.""" def __init__(self): self._registry = [] # plugin_class class -> plugin options def reset(self): """Reset registry state, e.g. for testing purposes.""" self._registry = [] def register(self, plugin, **options): """Add the given plugin from the registry.""" # Instantiate the admin class to save in the registry self._registry.append((plugin, options)) plugin_dir = HealthCheckPluginDirectory() django-health-check-3.17.0/health_check/storage/000077500000000000000000000000001430537201200214255ustar00rootroot00000000000000django-health-check-3.17.0/health_check/storage/__init__.py000066400000000000000000000001621430537201200235350ustar00rootroot00000000000000import django if django.VERSION < (3, 2): default_app_config = "health_check.storage.apps.HealthCheckConfig" django-health-check-3.17.0/health_check/storage/apps.py000066400000000000000000000004451430537201200227450ustar00rootroot00000000000000from django.apps import AppConfig from health_check.plugins import plugin_dir class HealthCheckConfig(AppConfig): name = "health_check.storage" def ready(self): from .backends import DefaultFileStorageHealthCheck plugin_dir.register(DefaultFileStorageHealthCheck) django-health-check-3.17.0/health_check/storage/backends.py000066400000000000000000000045531430537201200235600ustar00rootroot00000000000000import uuid from django.conf import settings from django.core.files.base import ContentFile from django.core.files.storage import get_storage_class from health_check.backends import BaseHealthCheckBackend from health_check.exceptions import ServiceUnavailable class StorageHealthCheck(BaseHealthCheckBackend): """ Tests the status of a `StorageBackend`. Can be extended to test any storage backend by subclassing: class MyStorageHealthCheck(StorageHealthCheck): storage = 'some.other.StorageBackend' plugin_dir.register(MyStorageHealthCheck) storage must be either a string pointing to a storage class (e.g 'django.core.files.storage.FileSystemStorage') or a Storage instance. """ storage = None def get_storage(self): if isinstance(self.storage, str): return get_storage_class(self.storage)() else: return self.storage def get_file_name(self): return "health_check_storage_test/test-%s.txt" % uuid.uuid4() def get_file_content(self): return b"this is the healthtest file content" def check_save(self, file_name, file_content): storage = self.get_storage() # save the file file_name = storage.save(file_name, ContentFile(content=file_content)) # read the file and compare if not storage.exists(file_name): raise ServiceUnavailable("File does not exist") with storage.open(file_name) as f: if not f.read() == file_content: raise ServiceUnavailable("File content does not match") return file_name def check_delete(self, file_name): storage = self.get_storage() # delete the file and make sure it is gone storage.delete(file_name) if storage.exists(file_name): raise ServiceUnavailable("File was not deleted") def check_status(self): try: # write the file to the storage backend file_name = self.get_file_name() file_content = self.get_file_content() file_name = self.check_save(file_name, file_content) self.check_delete(file_name) return True except Exception as e: raise ServiceUnavailable("Unknown exception") from e class DefaultFileStorageHealthCheck(StorageHealthCheck): storage = settings.DEFAULT_FILE_STORAGE django-health-check-3.17.0/health_check/templates/000077500000000000000000000000001430537201200217575ustar00rootroot00000000000000django-health-check-3.17.0/health_check/templates/health_check/000077500000000000000000000000001430537201200243615ustar00rootroot00000000000000django-health-check-3.17.0/health_check/templates/health_check/index.html000066400000000000000000000032501430537201200263560ustar00rootroot00000000000000 {% block title %}System status{% endblock title %} {% block extra_head %}{% endblock extra_head %} {% block content %}

System status

{% for plugin in plugins %} {% endfor %}
Service Status Time Taken
{{ plugin.identifier }} {{ plugin.pretty_status | linebreaks }} {{ plugin.time_taken|floatformat:4 }} seconds
{% endblock content %} django-health-check-3.17.0/health_check/urls.py000066400000000000000000000002601430537201200213160ustar00rootroot00000000000000from django.urls import path from health_check.views import MainView app_name = "health_check" urlpatterns = [ path("", MainView.as_view(), name="health_check_home"), ] django-health-check-3.17.0/health_check/views.py000066400000000000000000000101001430537201200214600ustar00rootroot00000000000000import re from django.http import HttpResponse, JsonResponse from django.utils.decorators import method_decorator from django.views.decorators.cache import never_cache from django.views.generic import TemplateView from health_check.mixins import CheckMixin class MediaType: """ Sortable object representing HTTP's accept header. .. seealso:: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept """ pattern = re.compile( r""" ^ (?P (\w+|\*) # Media type, or wildcard / ([\w\d\-+.]+|\*) # subtype, or wildcard ) ( \s*;\s* # parameter separator with optional whitespace q= # q is expected to be the first parameter, by RFC2616 (?P 1([.]0{1,3})? # 1 with up to three digits of precision | 0([.]\d{1,3})? # 0.000 to 0.999 with optional precision ) )? ( \s*;\s* # parameter separator with optional whitespace [-!#$%&'*+.^_`|~0-9a-zA-Z]+ # any token from legal characters = [-!#$%&'*+.^_`|~0-9a-zA-Z]+ # any value from legal characters )* $ """, re.VERBOSE, ) def __init__(self, mime_type, weight=1.0): self.mime_type = mime_type self.weight = float(weight) @classmethod def from_string(cls, value): """Return single instance parsed from given accept header string.""" match = cls.pattern.search(value) if match is None: raise ValueError('"%s" is not a valid media type' % value) try: return cls(match.group("mime_type"), float(match.group("weight") or 1)) except ValueError: return cls(value) @classmethod def parse_header(cls, value="*/*"): """Parse HTTP accept header and return instances sorted by weight.""" yield from sorted( ( cls.from_string(token.strip()) for token in value.split(",") if token.strip() ), reverse=True, ) def __str__(self): return "%s; q=%s" % (self.mime_type, self.weight) def __repr__(self): return "%s: %s" % (type(self).__name__, self.__str__()) def __eq__(self, other): return self.weight == other.weight and self.mime_type == other.mime_type def __lt__(self, other): return self.weight.__lt__(other.weight) class MainView(CheckMixin, TemplateView): template_name = "health_check/index.html" @method_decorator(never_cache) def get(self, request, *args, **kwargs): status_code = 500 if self.errors else 200 format_override = request.GET.get("format") if format_override == "json": return self.render_to_response_json(self.plugins, status_code) accept_header = request.META.get("HTTP_ACCEPT", "*/*") for media in MediaType.parse_header(accept_header): if media.mime_type in ( "text/html", "application/xhtml+xml", "text/*", "*/*", ): context = self.get_context_data(**kwargs) return self.render_to_response(context, status=status_code) elif media.mime_type in ("application/json", "application/*"): return self.render_to_response_json(self.plugins, status_code) return HttpResponse( "Not Acceptable: Supported content types: text/html, application/json", status=406, content_type="text/plain", ) def get_context_data(self, **kwargs): return {**super().get_context_data(**kwargs), "plugins": self.plugins} def render_to_response_json(self, plugins, status): return JsonResponse( {str(p.identifier()): str(p.pretty_status()) for p in plugins}, status=status, ) django-health-check-3.17.0/linter-requirements.txt000066400000000000000000000001201430537201200221270ustar00rootroot00000000000000bandit==1.7.4 black==22.8.0 flake8==5.0.4 isort==5.10.1 pydocstyle[toml]==6.1.1 django-health-check-3.17.0/setup.cfg000066400000000000000000000043341430537201200172040ustar00rootroot00000000000000[metadata] name = django-health-check author = Kristian Ollegaard author_email = kristian@oellegaard.com description = Run checks on services like databases, queue servers, celery processes, etc. long_description = file: README.rst url = https://github.com/KristianOellegaard/django-health-check license = MIT License license_file = LICENSE classifier = Development Status :: 5 - Production/Stable Framework :: Django Framework :: Django :: 2.2 Framework :: Django :: 3.1 Framework :: Django :: 3.2 Intended Audience :: Developers License :: OSI Approved :: MIT License Operating System :: OS Independent Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Topic :: Software Development :: Quality Assurance Framework :: Django Framework :: Django :: 2.2 Framework :: Django :: 4.0 Framework :: Django :: 4.1 Topic :: System :: Logging Topic :: System :: Monitoring Topic :: Utilities keywords = django postgresql [options] python_requires = >=3.8 include_package_data = True packages = health_check install_requires = django>=2.2 setup_requires = setuptools_scm [options.extras_require] test = pytest pytest-cov pytest-django celery redis docs = sphinx [bdist_wheel] universal = 1 [bdist_rpm] requires = python-django-appconf >= 2.0 python-django-appconf >= 0.6 [aliases] test = pytest [tool:pytest] norecursedirs=venv env .eggs DJANGO_SETTINGS_MODULE=tests.testapp.settings addopts = --tb=short -rxs --cov=health_check [flake8] max-line-length = 88 select = C,E,F,W,B,B950 ignore = E203, E501, W503, E731 [isort] atomic = true line_length = 88 multi_line_output = 3 include_trailing_comma = True force_grid_wrap = 0 use_parentheses = True known_first_party = health_check, tests default_section = THIRDPARTY combine_as_imports = true [pydocstyle] add-ignore = D1 match-dir = (?!tests|env|.eggs|\.).* [coverage:run] branch = True omit = */migrations/* */tests/* */test_*.py .eggs/* [coverage:report] ignore_errors = True show_missing = True skip_covered = True sort = Cover django-health-check-3.17.0/setup.py000077500000000000000000000001541430537201200170740ustar00rootroot00000000000000#!/usr/bin/env python from setuptools import setup setup(name="django-health-check", use_scm_version=True) django-health-check-3.17.0/tests/000077500000000000000000000000001430537201200165215ustar00rootroot00000000000000django-health-check-3.17.0/tests/__init__.py000066400000000000000000000000001430537201200206200ustar00rootroot00000000000000django-health-check-3.17.0/tests/test_autodiscover.py000066400000000000000000000021001430537201200226320ustar00rootroot00000000000000from celery import current_app from django.conf import settings from health_check.contrib.celery.backends import CeleryHealthCheck from health_check.contrib.celery_ping.backends import CeleryPingHealthCheck from health_check.plugins import plugin_dir class TestAutoDiscover: def test_autodiscover(self): health_check_plugins = list( filter( lambda x: x.startswith("health_check.") and "celery" not in x, settings.INSTALLED_APPS, ) ) non_celery_plugins = [ x for x in plugin_dir._registry if not issubclass(x[0], (CeleryHealthCheck, CeleryPingHealthCheck)) ] # The number of installed apps excluding celery should equal to all plugins except celery assert len(non_celery_plugins) == len(health_check_plugins) def test_discover_celery_queues(self): celery_plugins = [ x for x in plugin_dir._registry if issubclass(x[0], CeleryHealthCheck) ] assert len(celery_plugins) == len(current_app.amqp.queues) django-health-check-3.17.0/tests/test_backends.py000066400000000000000000000056751430537201200217210ustar00rootroot00000000000000import logging from io import StringIO import pytest from health_check.backends import BaseHealthCheckBackend from health_check.exceptions import HealthCheckException class TestBaseHealthCheckBackend: def test_run_check(self): with pytest.raises(NotImplementedError): BaseHealthCheckBackend().run_check() def test_identifier(self): assert BaseHealthCheckBackend().identifier() == "BaseHealthCheckBackend" class MyHeathCheck(BaseHealthCheckBackend): pass assert MyHeathCheck().identifier() == "MyHeathCheck" class MyHeathCheck(BaseHealthCheckBackend): foo = "bar" def identifier(self): return self.foo assert MyHeathCheck().identifier() == "bar" def test_status(self): ht = BaseHealthCheckBackend() assert ht.status == 1 ht.errors = [1] assert ht.status == 0 def test_pretty_status(self): ht = BaseHealthCheckBackend() assert ht.pretty_status() == "working" ht.errors = ["foo"] assert ht.pretty_status() == "foo" ht.errors.append("bar") assert ht.pretty_status() == "foo\nbar" ht.errors.append(123) assert ht.pretty_status() == "foo\nbar\n123" def test_add_error(self): ht = BaseHealthCheckBackend() e = HealthCheckException("foo") ht.add_error(e) assert ht.errors[0] is e ht = BaseHealthCheckBackend() ht.add_error("bar") assert isinstance(ht.errors[0], HealthCheckException) assert str(ht.errors[0]) == "unknown error: bar" ht = BaseHealthCheckBackend() ht.add_error(type) assert isinstance(ht.errors[0], HealthCheckException) assert str(ht.errors[0]) == "unknown error: unknown error" def test_add_error_cause(self): ht = BaseHealthCheckBackend() logger = logging.getLogger("health-check") with StringIO() as stream: stream_handler = logging.StreamHandler(stream) logger.addHandler(stream_handler) try: raise Exception("bar") except Exception as e: ht.add_error("foo", e) stream.seek(0) log = stream.read() assert "foo" in log assert "bar" in log assert "Traceback" in log assert "Exception: bar" in log logger.removeHandler(stream_handler) with StringIO() as stream: stream_handler = logging.StreamHandler(stream) logger.addHandler(stream_handler) try: raise Exception("bar") except Exception: ht.add_error("foo") stream.seek(0) log = stream.read() assert "foo" in log assert "bar" not in log assert "Traceback" not in log assert "Exception: bar" not in log logger.removeHandler(stream_handler) django-health-check-3.17.0/tests/test_cache.py000066400000000000000000000072441430537201200212040ustar00rootroot00000000000000from unittest.mock import patch from django.core.cache.backends.base import BaseCache, CacheKeyWarning from django.test import TestCase from health_check.cache.backends import CacheBackend # A Mock version of the cache to use for testing class MockCache(BaseCache): """ A Mock Cache used for testing. set_works - set to False to make the mocked set method fail, but not raise set_raises - The Exception to be raised when set() is called, if any """ key = None value = None set_works = None set_raises = None def __init__(self, set_works=True, set_raises=None): super(MockCache, self).__init__(params={}) self.set_works = set_works self.set_raises = set_raises def set(self, key, value, *args, **kwargs): if self.set_raises is not None: raise self.set_raises elif self.set_works: self.key = key self.value = value else: self.key = key self.value = None def get(self, key, *args, **kwargs): if key == self.key: return self.value else: return None class HealthCheckCacheTests(TestCase): """ Tests health check behavior with a mocked cache backend. Ensures check_status returns/raises the expected result when the cache works, fails, or raises exceptions. """ @patch("health_check.cache.backends.caches", dict(default=MockCache())) def test_check_status_working(self): cache_backend = CacheBackend() cache_backend.run_check() self.assertFalse(cache_backend.errors) @patch( "health_check.cache.backends.caches", dict(default=MockCache(), broken=MockCache(set_works=False)), ) def test_multiple_backends_check_default(self): # default backend works while other is broken cache_backend = CacheBackend("default") cache_backend.run_check() self.assertFalse(cache_backend.errors) @patch( "health_check.cache.backends.caches", dict(default=MockCache(), broken=MockCache(set_works=False)), ) def test_multiple_backends_check_broken(self): cache_backend = CacheBackend("broken") cache_backend.run_check() self.assertTrue(cache_backend.errors) self.assertIn( "unavailable: Cache key does not match", cache_backend.pretty_status() ) # check_status should raise ServiceUnavailable when values at cache key do not match @patch( "health_check.cache.backends.caches", dict(default=MockCache(set_works=False)) ) def test_set_fails(self): cache_backend = CacheBackend() cache_backend.run_check() self.assertTrue(cache_backend.errors) self.assertIn( "unavailable: Cache key does not match", cache_backend.pretty_status() ) # check_status should catch generic exceptions raised by set and convert to ServiceUnavailable @patch( "health_check.cache.backends.caches", dict(default=MockCache(set_raises=Exception)), ) def test_set_raises_generic(self): cache_backend = CacheBackend() with self.assertRaises(Exception): cache_backend.run_check() # check_status should catch CacheKeyWarning and convert to ServiceReturnedUnexpectedResult @patch( "health_check.cache.backends.caches", dict(default=MockCache(set_raises=CacheKeyWarning)), ) def test_set_raises_cache_key_warning(self): cache_backend = CacheBackend() cache_backend.check_status() cache_backend.run_check() self.assertIn( "unexpected result: Cache key warning", cache_backend.pretty_status() ) django-health-check-3.17.0/tests/test_celery_ping.py000066400000000000000000000111031430537201200224260ustar00rootroot00000000000000from unittest.mock import patch import pytest from django.apps import apps from django.conf import settings from health_check.contrib.celery_ping.apps import HealthCheckConfig from health_check.contrib.celery_ping.backends import CeleryPingHealthCheck class TestCeleryPingHealthCheck: CELERY_APP_CONTROL_PING = ( "health_check.contrib.celery_ping.backends.app.control.ping" ) CELERY_APP_CONTROL_INSPECT_ACTIVE_QUEUES = ( "health_check.contrib.celery_ping.backends.app.control.inspect.active_queues" ) @pytest.fixture def health_check(self): return CeleryPingHealthCheck() def test_check_status_doesnt_add_errors_when_ping_successful(self, health_check): celery_worker = "celery@4cc150a7b49b" with patch( self.CELERY_APP_CONTROL_PING, return_value=[ {celery_worker: CeleryPingHealthCheck.CORRECT_PING_RESPONSE}, {f"{celery_worker}-2": CeleryPingHealthCheck.CORRECT_PING_RESPONSE}, ], ), patch( self.CELERY_APP_CONTROL_INSPECT_ACTIVE_QUEUES, return_value={ celery_worker: [ {"name": queue.name} for queue in settings.CELERY_QUEUES ] }, ): health_check.check_status() assert not health_check.errors def test_check_status_reports_errors_if_ping_responses_are_incorrect( self, health_check ): with patch( self.CELERY_APP_CONTROL_PING, return_value=[ {"celery1@4cc150a7b49b": CeleryPingHealthCheck.CORRECT_PING_RESPONSE}, {"celery2@4cc150a7b49b": {}}, {"celery3@4cc150a7b49b": {"error": "pong"}}, ], ): health_check.check_status() assert len(health_check.errors) == 2 def test_check_status_adds_errors_when_ping_successfull_but_not_all_defined_queues_have_consumers( self, health_check, ): celery_worker = "celery@4cc150a7b49b" queues = list(settings.CELERY_QUEUES) with patch( self.CELERY_APP_CONTROL_PING, return_value=[{celery_worker: CeleryPingHealthCheck.CORRECT_PING_RESPONSE}], ), patch( self.CELERY_APP_CONTROL_INSPECT_ACTIVE_QUEUES, return_value={celery_worker: [{"name": queues.pop().name}]}, ): health_check.check_status() assert len(health_check.errors) == len(queues) @pytest.mark.parametrize( "exception_to_raise", [ IOError, TimeoutError, ], ) def test_check_status_add_error_when_io_error_raised_from_ping( self, exception_to_raise, health_check ): with patch(self.CELERY_APP_CONTROL_PING, side_effect=exception_to_raise): health_check.check_status() assert len(health_check.errors) == 1 assert "ioerror" in health_check.errors[0].message.lower() @pytest.mark.parametrize( "exception_to_raise", [ValueError, SystemError, IndexError, MemoryError] ) def test_check_status_add_error_when_any_exception_raised_from_ping( self, exception_to_raise, health_check ): with patch(self.CELERY_APP_CONTROL_PING, side_effect=exception_to_raise): health_check.check_status() assert len(health_check.errors) == 1 assert health_check.errors[0].message.lower() == "unknown error" def test_check_status_when_raised_exception_notimplementederror(self, health_check): expected_error_message = ( "notimplementederror: make sure celery_result_backend is set" ) with patch(self.CELERY_APP_CONTROL_PING, side_effect=NotImplementedError): health_check.check_status() assert len(health_check.errors) == 1 assert health_check.errors[0].message.lower() == expected_error_message @pytest.mark.parametrize("ping_result", [None, list()]) def test_check_status_add_error_when_ping_result_failed( self, ping_result, health_check ): with patch(self.CELERY_APP_CONTROL_PING, return_value=ping_result): health_check.check_status() assert len(health_check.errors) == 1 assert "workers unavailable" in health_check.errors[0].message.lower() class TestCeleryPingHealthCheckApps: def test_apps(self): assert HealthCheckConfig.name == "health_check.contrib.celery_ping" celery_ping = apps.get_app_config("celery_ping") assert celery_ping.name == "health_check.contrib.celery_ping" django-health-check-3.17.0/tests/test_commands.py000066400000000000000000000016471430537201200217430ustar00rootroot00000000000000from io import StringIO import pytest from django.core.management import call_command from health_check.backends import BaseHealthCheckBackend from health_check.plugins import plugin_dir class FailPlugin(BaseHealthCheckBackend): def check_status(self): self.add_error("Oops") class OkPlugin(BaseHealthCheckBackend): def check_status(self): pass class TestCommand: @pytest.fixture(autouse=True) def setup(self): plugin_dir.reset() plugin_dir.register(FailPlugin) plugin_dir.register(OkPlugin) yield plugin_dir.reset() def test_command(self): stdout = StringIO() with pytest.raises(SystemExit): call_command("health_check", stdout=stdout) stdout.seek(0) assert stdout.read() == ( "FailPlugin ... unknown error: Oops\n" "OkPlugin ... working\n" ) django-health-check-3.17.0/tests/test_db.py000066400000000000000000000044661430537201200205310ustar00rootroot00000000000000from unittest.mock import patch from django.db import DatabaseError, IntegrityError from django.db.models import Model from django.test import TestCase from health_check.db.backends import DatabaseBackend class MockDBModel(Model): """ A Mock database used for testing. error_thrown - The Exception to be raised when save() is called, if any """ error_thrown = None def __init__(self, error_thrown=None, *args, **kwargs): super(MockDBModel, self).__init__(*args, **kwargs) self.error_thrown = error_thrown def save(self, *args, **kwargs): if self.error_thrown is not None: raise self.error_thrown else: return True def delete(self, *args, **kwargs): return True def raise_(ex): raise ex class HealthCheckDatabaseTests(TestCase): """ Tests health check behavior with a mocked database backend. Ensures check_status returns/raises the expected result when the database works or raises exceptions. """ @patch( "health_check.db.backends.TestModel.objects.create", lambda title=None: MockDBModel(), ) def test_check_status_works(self): db_backend = DatabaseBackend() db_backend.check_status() self.assertFalse(db_backend.errors) @patch( "health_check.db.backends.TestModel.objects.create", lambda title=None: raise_(IntegrityError), ) def test_raise_integrity_error(self): db_backend = DatabaseBackend() db_backend.run_check() self.assertTrue(db_backend.errors) self.assertIn("unexpected result: Integrity Error", db_backend.pretty_status()) @patch( "health_check.db.backends.TestModel.objects.create", lambda title=None: MockDBModel(error_thrown=DatabaseError), ) def test_raise_database_error(self): db_backend = DatabaseBackend() db_backend.run_check() self.assertTrue(db_backend.errors) self.assertIn("unavailable: Database error", db_backend.pretty_status()) @patch( "health_check.db.backends.TestModel.objects.create", lambda title=None: MockDBModel(error_thrown=Exception), ) def test_raise_exception(self): db_backend = DatabaseBackend() with self.assertRaises(Exception): db_backend.run_check() django-health-check-3.17.0/tests/test_migrations.py000066400000000000000000000017021430537201200223060ustar00rootroot00000000000000from unittest.mock import patch from django.db.migrations import Migration from django.test import TestCase from health_check.contrib.migrations.backends import MigrationsHealthCheck class MockMigration(Migration): ... class TestMigrationsHealthCheck(TestCase): def test_check_status_work(self): with patch( "health_check.contrib.migrations.backends.MigrationsHealthCheck.get_migration_plan", return_value=[], ): backend = MigrationsHealthCheck() backend.run_check() self.assertFalse(backend.errors) def test_check_status_raises_error_if_there_are_migrations(self): with patch( "health_check.contrib.migrations.backends.MigrationsHealthCheck.get_migration_plan", return_value=[(MockMigration, False)], ): backend = MigrationsHealthCheck() backend.run_check() self.assertTrue(backend.errors) django-health-check-3.17.0/tests/test_mixins.py000066400000000000000000000014741430537201200214470ustar00rootroot00000000000000import pytest from health_check.backends import BaseHealthCheckBackend from health_check.mixins import CheckMixin from health_check.plugins import plugin_dir class FailPlugin(BaseHealthCheckBackend): def check_status(self): self.add_error("Oops") class OkPlugin(BaseHealthCheckBackend): def check_status(self): pass class Checker(CheckMixin): pass class TestCheckMixin: @pytest.fixture(autouse=True) def setup(self): plugin_dir.reset() plugin_dir.register(FailPlugin) plugin_dir.register(OkPlugin) yield plugin_dir.reset() def test_plugins(self): assert len(Checker().plugins) == 2 def test_errors(self): assert len(Checker().errors) == 1 def test_run_check(self): assert len(Checker().run_check()) == 1 django-health-check-3.17.0/tests/test_plugins.py000066400000000000000000000010441430537201200216120ustar00rootroot00000000000000import pytest from health_check.backends import BaseHealthCheckBackend from health_check.plugins import plugin_dir class FakePlugin(BaseHealthCheckBackend): def check_status(self): pass class Plugin(BaseHealthCheckBackend): def check_status(self): pass class TestPlugin: @pytest.fixture(autouse=True) def setup(self): plugin_dir.reset() plugin_dir.register(FakePlugin) yield plugin_dir.reset() def test_register_plugin(self): assert len(plugin_dir._registry) == 1 django-health-check-3.17.0/tests/test_rabbitmq.py000066400000000000000000000057551430537201200217470ustar00rootroot00000000000000from unittest import mock from amqp.exceptions import AccessRefused from health_check.contrib.rabbitmq.backends import RabbitMQHealthCheck class TestRabbitMQHealthCheck: """Test RabbitMQ health check.""" @mock.patch("health_check.contrib.rabbitmq.backends.getattr") @mock.patch("health_check.contrib.rabbitmq.backends.Connection") def test_broker_refused_connection(self, mocked_connection, mocked_getattr): """Test when the connection to RabbitMQ is refused.""" mocked_getattr.return_value = "broker_url" conn_exception = ConnectionRefusedError("Refused connection") # mock returns mocked_conn = mock.MagicMock() mocked_connection.return_value.__enter__.return_value = mocked_conn mocked_conn.connect.side_effect = conn_exception # instantiates the class rabbitmq_healthchecker = RabbitMQHealthCheck() # invokes the method check_status() rabbitmq_healthchecker.check_status() assert len(rabbitmq_healthchecker.errors), 1 # mock assertions mocked_connection.assert_called_once_with("broker_url") @mock.patch("health_check.contrib.rabbitmq.backends.getattr") @mock.patch("health_check.contrib.rabbitmq.backends.Connection") def test_broker_auth_error(self, mocked_connection, mocked_getattr): """Test that the connection to RabbitMQ has an authentication error.""" mocked_getattr.return_value = "broker_url" conn_exception = AccessRefused("Refused connection") # mock returns mocked_conn = mock.MagicMock() mocked_connection.return_value.__enter__.return_value = mocked_conn mocked_conn.connect.side_effect = conn_exception # instantiates the class rabbitmq_healthchecker = RabbitMQHealthCheck() # invokes the method check_status() rabbitmq_healthchecker.check_status() assert len(rabbitmq_healthchecker.errors), 1 # mock assertions mocked_connection.assert_called_once_with("broker_url") @mock.patch("health_check.contrib.rabbitmq.backends.getattr") @mock.patch("health_check.contrib.rabbitmq.backends.Connection") def test_broker_connection_upon_none_url(self, mocked_connection, mocked_getattr): """Thest when the connection to RabbitMQ has no ``broker_url``.""" mocked_getattr.return_value = None # if the variable BROKER_URL is not set, AccessRefused exception is raised conn_exception = AccessRefused("Refused connection") # mock returns mocked_conn = mock.MagicMock() mocked_connection.return_value.__enter__.return_value = mocked_conn mocked_conn.connect.side_effect = conn_exception # instantiates the class rabbitmq_healthchecker = RabbitMQHealthCheck() # invokes the method check_status() rabbitmq_healthchecker.check_status() assert len(rabbitmq_healthchecker.errors), 1 # mock assertions mocked_connection.assert_called_once_with(None) django-health-check-3.17.0/tests/test_redis.py000066400000000000000000000066321430537201200212470ustar00rootroot00000000000000from unittest import mock from redis.exceptions import ConnectionError, TimeoutError from health_check.contrib.redis.backends import RedisHealthCheck class TestRedisHealthCheck: """Test Redis health check.""" @mock.patch("health_check.contrib.redis.backends.getattr") @mock.patch("health_check.contrib.redis.backends.from_url", autospec=True) def test_redis_refused_connection(self, mocked_connection, mocked_getattr): """Test when the connection to Redis is refused.""" mocked_getattr.return_value = "redis_url" # mock returns mocked_connection.return_value = mock.MagicMock() mocked_connection.return_value.__enter__.side_effect = ConnectionRefusedError( "Refused connection" ) # instantiates the class redis_healthchecker = RedisHealthCheck() # invokes the method check_status() redis_healthchecker.check_status() assert len(redis_healthchecker.errors), 1 # mock assertions mocked_connection.assert_called_once_with("redis://localhost/1") @mock.patch("health_check.contrib.redis.backends.getattr") @mock.patch("health_check.contrib.redis.backends.from_url") def test_redis_timeout_error(self, mocked_connection, mocked_getattr): """Test Redis TimeoutError.""" mocked_getattr.return_value = "redis_url" # mock returns mocked_connection.return_value = mock.MagicMock() mocked_connection.return_value.__enter__.side_effect = TimeoutError( "Timeout Error" ) # instantiates the class redis_healthchecker = RedisHealthCheck() # invokes the method check_status() redis_healthchecker.check_status() assert len(redis_healthchecker.errors), 1 # mock assertions mocked_connection.assert_called_once_with("redis://localhost/1") @mock.patch("health_check.contrib.redis.backends.getattr") @mock.patch("health_check.contrib.redis.backends.from_url") def test_redis_con_limit_exceeded(self, mocked_connection, mocked_getattr): """Test Connection Limit Exceeded error.""" mocked_getattr.return_value = "redis_url" # mock returns mocked_connection.return_value = mock.MagicMock() mocked_connection.return_value.__enter__.side_effect = ConnectionError( "Connection Error" ) # instantiates the class redis_healthchecker = RedisHealthCheck() # invokes the method check_status() redis_healthchecker.check_status() assert len(redis_healthchecker.errors), 1 # mock assertions mocked_connection.assert_called_once_with("redis://localhost/1") @mock.patch("health_check.contrib.redis.backends.getattr") @mock.patch("health_check.contrib.redis.backends.from_url") def test_redis_conn_ok(self, mocked_connection, mocked_getattr): """Test everything is OK.""" mocked_getattr.return_value = "redis_url" # mock returns mocked_connection.return_value = mock.MagicMock() mocked_connection.return_value.__enter__.side_effect = True # instantiates the class redis_healthchecker = RedisHealthCheck() # invokes the method check_status() redis_healthchecker.check_status() assert len(redis_healthchecker.errors), 0 # mock assertions mocked_connection.assert_called_once_with("redis://localhost/1") django-health-check-3.17.0/tests/test_storage.py000066400000000000000000000067621430537201200216110ustar00rootroot00000000000000from unittest import mock from django.core.files.storage import Storage from django.test import TestCase from health_check.exceptions import ServiceUnavailable from health_check.storage.backends import ( DefaultFileStorageHealthCheck, StorageHealthCheck, ) class MockStorage(Storage): """ A Mock Storage backend used for testing. saves - Determines whether save will mock a successful or unsuccessful save deletes - Determines whether save will mock a successful or unsuccessful deletion """ MOCK_FILE_COUNT = 0 saves = None deletes = None def __init__(self, saves=True, deletes=True): super(MockStorage, self).__init__() self.MOCK_FILE_COUNT = 0 self.saves = saves self.deletes = deletes def exists(self, file_name): return self.MOCK_FILE_COUNT != 0 def delete(self, name): if self.deletes: self.MOCK_FILE_COUNT -= 1 def save(self, name, content, max_length=None): if self.saves: self.MOCK_FILE_COUNT += 1 def get_file_name(*args, **kwargs): return "mockfile.txt" def get_file_content(*args, **kwargs): return b"mockcontent" @mock.patch( "health_check.storage.backends.StorageHealthCheck.get_file_name", get_file_name ) @mock.patch( "health_check.storage.backends.StorageHealthCheck.get_file_content", get_file_content, ) class HealthCheckStorageTests(TestCase): """ Tests health check behavior with a mocked storage backend. Ensures check_status returns/raises the expected result when the storage works or raises exceptions. """ def test_get_storage(self): """Test get_storage method returns None on the base class, but a Storage instance on default.""" base_storage = StorageHealthCheck() self.assertIsNone(base_storage.get_storage()) default_storage = DefaultFileStorageHealthCheck() self.assertIsInstance(default_storage.get_storage(), Storage) @mock.patch( "health_check.storage.backends.DefaultFileStorageHealthCheck.storage", MockStorage(), ) def test_check_status_working(self): """Test check_status returns True when storage is working properly.""" default_storage_health = DefaultFileStorageHealthCheck() default_storage = default_storage_health.get_storage() default_storage_open = "{}.{}.open".format( default_storage.__module__, default_storage.__class__.__name__ ) with mock.patch( default_storage_open, mock.mock_open(read_data=default_storage_health.get_file_content()), ): self.assertTrue(default_storage_health.check_status()) @mock.patch( "health_check.storage.backends.DefaultFileStorageHealthCheck.storage", MockStorage(saves=False), ) def test_file_does_not_exist(self): """Test check_status raises ServiceUnavailable when file is not saved.""" default_storage_health = DefaultFileStorageHealthCheck() with self.assertRaises(ServiceUnavailable): default_storage_health.check_status() @mock.patch( "health_check.storage.backends.DefaultFileStorageHealthCheck.storage", MockStorage(deletes=False), ) def test_file_not_deleted(self): """Test check_status raises ServiceUnavailable when file is not deleted.""" default_storage_health = DefaultFileStorageHealthCheck() with self.assertRaises(ServiceUnavailable): default_storage_health.check_status() django-health-check-3.17.0/tests/test_views.py000066400000000000000000000250471430537201200212770ustar00rootroot00000000000000import json import pytest from health_check.backends import BaseHealthCheckBackend from health_check.conf import HEALTH_CHECK from health_check.exceptions import ServiceWarning from health_check.plugins import plugin_dir from health_check.views import MediaType try: from django.urls import reverse except ImportError: from django.core.urlresolvers import reverse class TestMediaType: def test_lt(self): assert not MediaType("*/*") < MediaType("*/*") assert not MediaType("*/*") < MediaType("*/*", 0.9) assert MediaType("*/*", 0.9) < MediaType("*/*") def test_str(self): assert str(MediaType("*/*")) == "*/*; q=1.0" assert str(MediaType("image/*", 0.6)) == "image/*; q=0.6" def test_repr(self): assert repr(MediaType("*/*")) == "MediaType: */*; q=1.0" def test_eq(self): assert MediaType("*/*") == MediaType("*/*") assert MediaType("*/*", 0.9) != MediaType("*/*") valid_strings = [ ("*/*", MediaType("*/*")), ("*/*; q=0.9", MediaType("*/*", 0.9)), ("*/*; q=0", MediaType("*/*", 0.0)), ("*/*; q=0.0", MediaType("*/*", 0.0)), ("*/*; q=0.1", MediaType("*/*", 0.1)), ("*/*; q=0.12", MediaType("*/*", 0.12)), ("*/*; q=0.123", MediaType("*/*", 0.123)), ("*/*; q=1.000", MediaType("*/*", 1.0)), ("*/*; q=1", MediaType("*/*", 1.0)), ("*/*;q=0.9", MediaType("*/*", 0.9)), ("*/* ;q=0.9", MediaType("*/*", 0.9)), ("*/* ; q=0.9", MediaType("*/*", 0.9)), ("*/* ; q=0.9", MediaType("*/*", 0.9)), ("*/*;v=b3", MediaType("*/*")), ("*/*; q=0.5; v=b3", MediaType("*/*", 0.5)), ] @pytest.mark.parametrize("type, expected", valid_strings) def test_from_valid_strings(self, type, expected): assert MediaType.from_string(type) == expected invalid_strings = [ "*/*;0.9", 'text/html;z=""', "text/html; xxx", "text/html; =a", ] @pytest.mark.parametrize("type", invalid_strings) def test_from_invalid_strings(self, type): with pytest.raises(ValueError) as e: MediaType.from_string(type) expected_error = '"%s" is not a valid media type' % type assert expected_error in str(e.value) def test_parse_header(self): assert list(MediaType.parse_header()) == [ MediaType("*/*"), ] assert list( MediaType.parse_header( "text/html; q=0.1, application/xhtml+xml; q=0.1 ,application/json" ) ) == [ MediaType("application/json"), MediaType("text/html", 0.1), MediaType("application/xhtml+xml", 0.1), ] class TestMainView: url = reverse("health_check:health_check_home") def test_success(self, client): response = client.get(self.url) assert response.status_code == 200, response.content.decode("utf-8") assert response["content-type"] == "text/html; charset=utf-8" def test_error(self, client): class MyBackend(BaseHealthCheckBackend): def check_status(self): self.add_error("Super Fail!") plugin_dir.reset() plugin_dir.register(MyBackend) response = client.get(self.url) assert response.status_code == 500, response.content.decode("utf-8") assert response["content-type"] == "text/html; charset=utf-8" assert b"Super Fail!" in response.content def test_warning(self, client): class MyBackend(BaseHealthCheckBackend): def check_status(self): raise ServiceWarning("so so") plugin_dir.reset() plugin_dir.register(MyBackend) response = client.get(self.url) assert response.status_code == 500, response.content.decode("utf-8") assert b"so so" in response.content, response.content HEALTH_CHECK["WARNINGS_AS_ERRORS"] = False response = client.get(self.url) assert response.status_code == 200, response.content.decode("utf-8") assert response["content-type"] == "text/html; charset=utf-8" assert b"so so" in response.content, response.content def test_non_critical(self, client): class MyBackend(BaseHealthCheckBackend): critical_service = False def check_status(self): self.add_error("Super Fail!") plugin_dir.reset() plugin_dir.register(MyBackend) response = client.get(self.url) assert response.status_code == 200, response.content.decode("utf-8") assert response["content-type"] == "text/html; charset=utf-8" assert b"Super Fail!" in response.content def test_success_accept_json(self, client): class JSONSuccessBackend(BaseHealthCheckBackend): def run_check(self): pass plugin_dir.reset() plugin_dir.register(JSONSuccessBackend) response = client.get(self.url, HTTP_ACCEPT="application/json") assert response["content-type"] == "application/json" assert response.status_code == 200 def test_success_prefer_json(self, client): class JSONSuccessBackend(BaseHealthCheckBackend): def run_check(self): pass plugin_dir.reset() plugin_dir.register(JSONSuccessBackend) response = client.get( self.url, HTTP_ACCEPT="application/json; q=0.8, text/html; q=0.5" ) assert response["content-type"] == "application/json" assert response.status_code == 200 def test_success_accept_xhtml(self, client): class SuccessBackend(BaseHealthCheckBackend): def run_check(self): pass plugin_dir.reset() plugin_dir.register(SuccessBackend) response = client.get(self.url, HTTP_ACCEPT="application/xhtml+xml") assert response["content-type"] == "text/html; charset=utf-8" assert response.status_code == 200 def test_success_unsupported_accept(self, client): class SuccessBackend(BaseHealthCheckBackend): def run_check(self): pass plugin_dir.reset() plugin_dir.register(SuccessBackend) response = client.get(self.url, HTTP_ACCEPT="application/octet-stream") assert response["content-type"] == "text/plain" assert response.status_code == 406 assert ( response.content == b"Not Acceptable: Supported content types: text/html, application/json" ) def test_success_unsupported_and_supported_accept(self, client): class SuccessBackend(BaseHealthCheckBackend): def run_check(self): pass plugin_dir.reset() plugin_dir.register(SuccessBackend) response = client.get( self.url, HTTP_ACCEPT="application/octet-stream, application/json; q=0.9" ) assert response["content-type"] == "application/json" assert response.status_code == 200 def test_success_accept_order(self, client): class JSONSuccessBackend(BaseHealthCheckBackend): def run_check(self): pass plugin_dir.reset() plugin_dir.register(JSONSuccessBackend) response = client.get( self.url, HTTP_ACCEPT="text/html, application/xhtml+xml, application/json; q=0.9, */*; q=0.1", ) assert response["content-type"] == "text/html; charset=utf-8" assert response.status_code == 200 def test_success_accept_order__reverse(self, client): class JSONSuccessBackend(BaseHealthCheckBackend): def run_check(self): pass plugin_dir.reset() plugin_dir.register(JSONSuccessBackend) response = client.get( self.url, HTTP_ACCEPT="text/html; q=0.1, application/xhtml+xml; q=0.1, application/json", ) assert response["content-type"] == "application/json" assert response.status_code == 200 def test_format_override(self, client): class JSONSuccessBackend(BaseHealthCheckBackend): def run_check(self): pass plugin_dir.reset() plugin_dir.register(JSONSuccessBackend) response = client.get(self.url + "?format=json", HTTP_ACCEPT="text/html") assert response["content-type"] == "application/json" assert response.status_code == 200 def test_format_no_accept_header(self, client): class JSONSuccessBackend(BaseHealthCheckBackend): def run_check(self): pass plugin_dir.reset() plugin_dir.register(JSONSuccessBackend) response = client.get(self.url) assert response.status_code == 200, response.content.decode("utf-8") assert response["content-type"] == "text/html; charset=utf-8" def test_error_accept_json(self, client): class JSONErrorBackend(BaseHealthCheckBackend): def run_check(self): self.add_error("JSON Error") plugin_dir.reset() plugin_dir.register(JSONErrorBackend) response = client.get(self.url, HTTP_ACCEPT="application/json") assert response.status_code == 500, response.content.decode("utf-8") assert response["content-type"] == "application/json" assert ( "JSON Error" in json.loads(response.content.decode("utf-8"))[ JSONErrorBackend().identifier() ] ) def test_success_param_json(self, client): class JSONSuccessBackend(BaseHealthCheckBackend): def run_check(self): pass plugin_dir.reset() plugin_dir.register(JSONSuccessBackend) response = client.get(self.url, {"format": "json"}) assert response.status_code == 200, response.content.decode("utf-8") assert response["content-type"] == "application/json" assert json.loads(response.content.decode("utf-8")) == { JSONSuccessBackend().identifier(): JSONSuccessBackend().pretty_status() } def test_error_param_json(self, client): class JSONErrorBackend(BaseHealthCheckBackend): def run_check(self): self.add_error("JSON Error") plugin_dir.reset() plugin_dir.register(JSONErrorBackend) response = client.get(self.url, {"format": "json"}) assert response.status_code == 500, response.content.decode("utf-8") assert response["content-type"] == "application/json" assert ( "JSON Error" in json.loads(response.content.decode("utf-8"))[ JSONErrorBackend().identifier() ] ) django-health-check-3.17.0/tests/testapp/000077500000000000000000000000001430537201200202015ustar00rootroot00000000000000django-health-check-3.17.0/tests/testapp/__init__.py000066400000000000000000000000531430537201200223100ustar00rootroot00000000000000from .celery import app __all__ = ["app"] django-health-check-3.17.0/tests/testapp/celery.py000066400000000000000000000002121430537201200220310ustar00rootroot00000000000000from celery import Celery app = Celery("testapp", broker="memory://") app.config_from_object("django.conf:settings", namespace="CELERY") django-health-check-3.17.0/tests/testapp/manage.py000077500000000000000000000003621430537201200220070ustar00rootroot00000000000000#!/usr/bin/env python import os import sys if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") from django.core.management import execute_from_command_line execute_from_command_line(sys.argv) django-health-check-3.17.0/tests/testapp/settings.py000066400000000000000000000026371430537201200224230ustar00rootroot00000000000000import os.path import uuid from kombu import Queue BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DEBUG = True DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:", }, "other": { # 2nd database conneciton to ensure proper connection handling "ENGINE": "django.db.backends.sqlite3", "NAME": ":backup:", }, } INSTALLED_APPS = ( "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.staticfiles", "health_check", "health_check.cache", "health_check.db", "health_check.storage", "health_check.contrib.celery", "health_check.contrib.migrations", "health_check.contrib.celery_ping", "health_check.contrib.s3boto_storage", "tests", ) MIDDLEWARE_CLASSES = ( "django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", ) STATIC_URL = "/static/" MEDIA_ROOT = os.path.join(BASE_DIR, "media") SITE_ID = 1 ROOT_URLCONF = "tests.testapp.urls" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "APP_DIRS": True, "OPTIONS": { "debug": True, }, }, ] SECRET_KEY = uuid.uuid4().hex USE_TZ = True CELERY_QUEUES = [ Queue("default"), Queue("queue2"), ] django-health-check-3.17.0/tests/testapp/urls.py000066400000000000000000000001601430537201200215350ustar00rootroot00000000000000from django.urls import include, re_path urlpatterns = [ re_path(r"^ht/", include("health_check.urls")), ]