pax_global_header 0000666 0000000 0000000 00000000064 15242136254 0014516 g ustar 00root root 0000000 0000000 52 comment=0de66501f43f79bef3ccd3680853afc88a76de2a
django-health-check-4.5.1/ 0000775 0000000 0000000 00000000000 15242136254 0015305 5 ustar 00root root 0000000 0000000 django-health-check-4.5.1/.bandit 0000664 0000000 0000000 00000000030 15242136254 0016540 0 ustar 00root root 0000000 0000000 [bandit]
exclude: tests
django-health-check-4.5.1/.editorconfig 0000664 0000000 0000000 00000000333 15242136254 0017761 0 ustar 00root root 0000000 0000000 # http://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
max_line_length = 88
indent_style = space
indent_size = 2
[*.py]
indent_size = 4
django-health-check-4.5.1/.git-blame-ignore-revs 0000664 0000000 0000000 00000000067 15242136254 0021410 0 ustar 00root root 0000000 0000000 # Ruff format
1652303c8dae23f0948b7f772d2803f71a9a1b61
django-health-check-4.5.1/.github/ 0000775 0000000 0000000 00000000000 15242136254 0016645 5 ustar 00root root 0000000 0000000 django-health-check-4.5.1/.github/agents/ 0000775 0000000 0000000 00000000000 15242136254 0020126 5 ustar 00root root 0000000 0000000 django-health-check-4.5.1/.github/agents/superjoe.agent.md 0000664 0000000 0000000 00000005102 15242136254 0023377 0 ustar 00root root 0000000 0000000 ---
# For format details, see: https://gh.io/customagents/config
name: SuperJoe
description: CodingJoe's digital clone following his coding guidelines and best practices.
---
# SuperJoe
## Planning
You MUST ALWAYS follow the `naming-things` guidelines. Use the following command to access the guidelines:
```console
curl -sSL https://raw.githubusercontent.com/codingjoe/naming-things/refs/heads/main/README.md | cat
```
You MUST ALWAYS read the `CONTRIBUTING.md` file before planning or writing any code.
You MUST ALWAYS search the documentation and update it as necessary.
You MUST ALWAYS check for pre-commit hooks and run them before committing code.
You MUST ALWAYS ensure that all new code is fully tested with 100% coverage. Unreachable code branches MUST be removed.
## Writing Code
Less code is more! Use the latest language features and libraries to achieve more with less code.
Do not add new dependencies, but if you do, they must be widely adopted and well-maintained in the open-source community.
You are a strong FOSS advocate with a preference for permissive licenses like BSD or MIT.
Use generators instead of adding items to lists or arrays.
Use class syntax for all object-oriented code.
Use named functions instead of anonymous functions whenever possible.
Avoid overly complex functions. Break them into smaller functions if necessary.
Docstrings should be written in present tense imperative mood.
They must start with a capital letter and end with a period.
Docstrings must describe the external behavior of the function, class, or method.
Docstrings should avoid redundant phrases like "This function" or "This method".
Class docstrings must not repeat the class name or start with a verb since they don't do anything themselves.
Avoid code comments unless they describe behavior of 3rd party code or complex algorithms.
Avoid loops in favor of recursive functions or generator functions.
Avoid functions or other code inside functions.
Avoid if-statements in favor of switch/match-statements or polymorphism.
Do not assign names to objects which are returned in the next line.
## Python
Follow PEP 8 guidelines for code style.
EAFP (Easier to Ask Forgiveness than Permission) is preferred over LBYL (Look Before You Leap).
Use type hints for all public functions, classes, and methods.
Use dataclasses for simple data structures.
Use context managers for resource management.
Use list/set/dict comprehensions instead of loops for creating collections.
Use generators for large data sets to save memory.
Use the walrus operator (`:=`) for inline assignments when it improves readability.
django-health-check-4.5.1/.github/copilot-instructions.md 0000664 0000000 0000000 00000004011 15242136254 0023376 0 ustar 00root root 0000000 0000000 All code MUST ALWAYS follow the `naming-things` guidelines!
Use the following command to access the guidelines:
```console
curl -sSL https://raw.githubusercontent.com/codingjoe/naming-things/refs/heads/main/README.md | cat
```
Less code is more! Use the latest language features and libraries to achieve more with less code.
Do not add new dependencies, but if you do, they must be widely adopted and well-maintained in the open-source community.
You are a strong FOSS advocate with a preference for permissive licenses like BSD or MIT.
Use generators instead of adding items to lists or arrays.
Use class syntax for all object-oriented code.
Use named functions instead of anonymous functions whenever possible.
Avoid overly complex functions. Break them into smaller functions if necessary.
Docstrings should be written in present tense imperative mood.
They must start with a capital letter and end with a period.
Docstrings must describe the external behavior of the function, class, or method.
Docstrings should avoid redundant phrases like "This function" or "This method".
Class docstrings must not repeat the class name or start with a verb since they don't do anything themselves.
Avoid code comments unless they describe behavior of 3rd party code or complex algorithms.
Avoid loops in favor of recursive functions or generator functions.
Avoid functions or other code inside functions.
Avoid if-statements in favor of switch/match-statements or polymorphism.
Do not assign names to objects which are returned in the next line.
## Python
Follow PEP 8 guidelines for code style.
EAFP (Easier to Ask Forgiveness than Permission) is preferred over LBYL (Look Before You Leap).
Use type hints for all public functions, classes, and methods.
Use dataclasses for simple data structures.
Use context managers for resource management.
Use list/set/dict comprehensions instead of loops for creating collections.
Use generators for large data sets to save memory.
Use the walrus operator (`:=`) for inline assignments when it improves readability.
django-health-check-4.5.1/.github/dependabot.yml 0000664 0000000 0000000 00000000303 15242136254 0021471 0 ustar 00root root 0000000 0000000 version: 2
updates:
- package-ecosystem: pip
directory: "/"
schedule:
interval: daily
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: daily
django-health-check-4.5.1/.github/workflows/ 0000775 0000000 0000000 00000000000 15242136254 0020702 5 ustar 00root root 0000000 0000000 django-health-check-4.5.1/.github/workflows/ci.yml 0000664 0000000 0000000 00000004523 15242136254 0022024 0 ustar 00root root 0000000 0000000 name: CI
on:
push:
branches:
- main
pull_request:
jobs:
dist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v7
- run: uvx --from build pyproject-build --sdist --wheel
- run: uvx twine check dist/*
- uses: actions/upload-artifact@v7
with:
path: dist/*
docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v7
- run: uv run mkdocs build --strict
pytest:
name: pytest
runs-on: ubuntu-latest
strategy:
matrix:
python-version:
- "3.10"
- "3.11"
- "3.12"
- "3.13"
- "3.14"
django-version:
- "5.2"
- "6.0"
- "6.1"
exclude:
- python-version: "3.10"
django-version: "6.0"
- python-version: "3.11"
django-version: "6.0"
- python-version: "3.10"
django-version: "6.1"
- python-version: "3.11"
django-version: "6.1"
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v7
with:
python-version: ${{ matrix.python-version }}
- run: uv run --no-dev --group=test --with Django~=${{ matrix.django-version }}.0 pytest
- uses: codecov/codecov-action@v7
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: python-${{ matrix.python-version }}-django-${{ matrix.django-version }}
pytest-contrib:
name: pytest
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version:
- "3.10"
- "3.14"
django-version:
- "5.2"
extra:
- "psutil"
- "celery"
- "kafka"
- "rabbitmq"
- "redis"
- "rss"
- "atlassian"
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v7
with:
python-version: ${{ matrix.python-version }}
- run: uv run --no-dev --group=test --extra=${{ matrix.extra }} --with Django~=${{ matrix.django-version }}.0 pytest
- uses: codecov/codecov-action@v7
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: python-${{ matrix.python-version }}-django-${{ matrix.django-version }}-${{ matrix.extra }}
django-health-check-4.5.1/.github/workflows/pr.yml 0000664 0000000 0000000 00000002100 15242136254 0022037 0 ustar 00root root 0000000 0000000 # Write better PRs and commit messages
name: "Word Weasel"
on:
pull_request:
types:
- opened
- edited
- reopened
- synchronize
jobs:
pr-title:
name: "PR Title"
runs-on: ubuntu-latest
timeout-minutes: 1
if: ${{ github.actor != 'dependabot[bot]' }}
steps:
- uses: codingjoe/word-weasel@v1
with:
text: ${{ github.event.pull_request.title }}
pr-body:
name: "PR Body"
runs-on: ubuntu-latest
timeout-minutes: 1
if: ${{ github.actor != 'dependabot[bot]' }}
steps:
- uses: codingjoe/word-weasel@v1
with:
text: ${{ github.event.pull_request.body }}
docs:
name: "Docs"
runs-on: ubuntu-latest
timeout-minutes: 1
if: ${{ github.actor != 'dependabot[bot]' }}
steps:
- uses: actions/checkout@v7
- name: REad docs to gh output
id: docs
run: echo "docs=$(find . -type f -name '*.md' -exec cat {} + | tr '\n' ' ')" >> $GITHUB_OUTPUT
- uses: codingjoe/word-weasel@v1
with:
text: ${{ steps.docs.outputs.docs }}
django-health-check-4.5.1/.github/workflows/release.yml 0000664 0000000 0000000 00000002370 15242136254 0023047 0 ustar 00root root 0000000 0000000 name: Release
on:
release:
types: [published]
workflow_dispatch:
jobs:
gh-page-build:
runs-on: ubuntu-latest
if: github.event_name != 'release' || github.event.release.prerelease == false
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v7
- run: uv run mkdocs build
- uses: actions/upload-pages-artifact@v5
with:
path: site
gh-page-deploy:
needs: gh-page-build
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- id: deployment
uses: actions/deploy-pages@v5
pypi-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v7
- run: uvx --from build pyproject-build --sdist --wheel
- uses: actions/upload-artifact@v7
with:
name: release-dists
path: dist/
pypi-publish:
runs-on: ubuntu-latest
needs:
- pypi-build
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v8
with:
name: release-dists
path: dist/
- uses: pypa/gh-action-pypi-publish@release/v1
django-health-check-4.5.1/.gitignore 0000664 0000000 0000000 00000002350 15242136254 0017275 0 ustar 00root root 0000000 0000000 # 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/
.envrc
.direnv
# mac
.DS_Store
.aider*
# SCM
_version.py
# uv
uv.lock
django-health-check-4.5.1/.pre-commit-config.yaml 0000664 0000000 0000000 00000002454 15242136254 0021573 0 ustar 00root root 0000000 0000000 repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: check-merge-conflict
- id: check-ast
- id: check-toml
- id: check-yaml
- id: check-symlinks
- id: debug-statements
- id: end-of-file-fixer
- id: no-commit-to-branch
args: [--branch, main]
- repo: https://github.com/asottile/pyupgrade
rev: v3.21.2
hooks:
- id: pyupgrade
args: [--py310-plus]
- repo: https://github.com/adamchainz/django-upgrade
rev: 1.31.1
hooks:
- id: django-upgrade
- repo: https://github.com/djlint/djLint
rev: v1.44.2
hooks:
- id: djlint-reformat-django
- repo: https://github.com/hukkin/mdformat
rev: 1.0.0
hooks:
- id: mdformat
additional_dependencies:
- mdformat-ruff
- mdformat-footnote
- mdformat-gfm
- mdformat-gfm-alerts
exclude: '.github/agents/'
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.16.3
hooks:
- id: ruff-check
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format
- repo: https://github.com/google/yamlfmt
rev: v0.21.0
hooks:
- id: yamlfmt
ci:
autoupdate_schedule: weekly
skip:
- no-commit-to-branch
django-health-check-4.5.1/.readthedocs.yaml 0000664 0000000 0000000 00000000634 15242136254 0020537 0 ustar 00root root 0000000 0000000 # .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-24.04
tools:
python: "3"
jobs:
install:
- curl -LsSf https://astral.sh/uv/install.sh | sh
build:
html:
- $HOME/.local/bin/uv run mkdocs build --site-dir $READTHEDOCS_OUTPUT/html
mkdocs:
configuration: mkdocs.yml
django-health-check-4.5.1/CONTRIBUTING.md 0000664 0000000 0000000 00000002024 15242136254 0017534 0 ustar 00root root 0000000 0000000 # Contributing
When writing code, you MUST ALWAYS follow the [naming-things](https://github.com/codingjoe/naming-things/blob/main/README.md) guidelines.
To run the tests, use the following command:
```bash
uv run pytest
```
Before your first commit, ensure that the pre-commit hooks are installed by running:
```bash
uv pre-commit install
```
## Testing with Extra Dependencies
```bash
export REDIS_URL=redis://localhost:6379
export BROKER_URL=amqp://guest:guest@localhost:5672//
export KAFKA_BOOTSTRAP_SERVERS=localhost:9092
uv run --extra=redis --extra=rabbitmq --extra=celery --extra=rss --extra=kafka pytest
```
## Writing documentation
The documentation is built using [MkDocs](https://www.mkdocs.org/) with [mkdocstrings](https://mkdocstrings.github.io/) for automatic API documentation generation.
To serve the documentation locally for development, run:
```bash
uv run mkdocs serve --livereload
```
django-health-check-4.5.1/LICENSE 0000664 0000000 0000000 00000002115 15242136254 0016311 0 ustar 00root root 0000000 0000000 Copyright (C) 2011-2025 Johannes Maron, 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-4.5.1/README.md 0000664 0000000 0000000 00000003156 15242136254 0016571 0 ustar 00root root 0000000 0000000
# Django HealthCheck
_Pluggable health checks for Django applications_
## π₯³ Sponsors π

[](https://pypi.python.org/pypi/django-health-check/)
[](https://codecov.io/gh/codingjoe/django-health-check)
[](https://pypi.python.org/pypi/django-health-check/)
[](https://pypi.python.org/pypi/django-health-check/)
[](https://pypi.python.org/pypi/django-health-check/)
django-health-check-4.5.1/docs/ 0000775 0000000 0000000 00000000000 15242136254 0016235 5 ustar 00root root 0000000 0000000 django-health-check-4.5.1/docs/checks.md 0000664 0000000 0000000 00000003625 15242136254 0020025 0 ustar 00root root 0000000 0000000 # Checks
## Django Built-in Services
::: health_check.Cache
::: health_check.DNS
::: health_check.Database
::: health_check.Mail
::: health_check.Storage
## System Services
To use the psutil-based checks, install the `psutil` extra:
```shell
pip install django-health-check[psutil]
```
::: health_check.contrib.psutil.Battery
::: health_check.contrib.psutil.CPU
::: health_check.contrib.psutil.Memory
::: health_check.contrib.psutil.Disk
::: health_check.contrib.psutil.Temperature
## 3rd Party Services
To use these checks, install and configure their corresponding dependencies.
To enable AWS health checks, install the extra for the `contrib` checks:
```shell
pip install django-health-check[redis,rabbitmq,celery,kafka]
```
::: health_check.contrib.celery.Ping
::: health_check.contrib.kafka.Kafka
::: health_check.contrib.rabbitmq.RabbitMQ
::: health_check.contrib.redis.Redis
## Cloud Provider Status
Watch cloud provider health through their public RSS/Atom status feeds or APIs.
Cloud provider health checks require different extras. The extra depends on the provider:
```shell
pip install django-health-check[rss,atlassian]
```
::: health_check.contrib.rss.AWS
::: health_check.contrib.rss.Azure
::: health_check.contrib.atlassian.Cloudflare
::: health_check.contrib.atlassian.DigitalOcean
::: health_check.contrib.atlassian.FlyIo
::: health_check.contrib.atlassian.GitHub
::: health_check.contrib.rss.GoogleCloud
::: health_check.contrib.rss.Heroku
::: health_check.contrib.rss.Hetzner
::: health_check.contrib.atlassian.PlatformSh
::: health_check.contrib.atlassian.Render
::: health_check.contrib.atlassian.Sentry
::: health_check.contrib.atlassian.Vercel
## Custom Status Page Feeds
Write your own status page proxy checks with these classes.
Subclasses must implement the documented attributes.
::: health_check.contrib.rss.Feed
::: health_check.contrib.atlassian.AtlassianStatusPage
django-health-check-4.5.1/docs/cookbook.md 0000664 0000000 0000000 00000025732 15242136254 0020376 0 ustar 00root root 0000000 0000000 # Cookbook
This cookbook walks through setting up multiple health check endpoints,
each aimed at a different audience and use case.
A solid setup exposes **three tiers** of endpoints:
| Tier | Purpose | Consumers |
| ----------------------------------------- | ----------------------- | -------------------------------------- |
| [Node](#node-health-checks) | Hardware & OS resources | Kubernetes, Docker, reverse proxies |
| [Application](#application-health-checks) | App-level services | Uptime monitors, on-call alerts |
| [Pipeline](#pipeline-health-checks) | Upstream dependencies | CI/CD, developer Slack/Matrix channels |
______________________________________________________________________
## Node health checks
Node checks confirm the server has enough resources to run the app.
Use them for **liveness and readiness probes** in container
orchestrators such as Kubernetes, Docker, and Podman, or for reverse-proxy
checks in HAProxy, nginx, Caddy, and Traefik.
The `psutil` extra provides OS resource checks such as CPU, memory, and disk usage.
```shell
pip install "django-health-check[psutil]"
```
Add the node endpoint to your URL configuration.
```python
# urls.py
import os
from django.urls import include, path
from health_check.views import HealthCheckView
node_checks = [
"health_check.contrib.psutil.CPU",
"health_check.contrib.psutil.Memory",
"health_check.contrib.psutil.Disk",
]
urlpatterns = [
# β¦
path(
f"health/{os.getenv('HEALTH_CHECK_SECRET', 'dev')}/",
include(
[
path(
"node/",
HealthCheckView.as_view(checks=node_checks),
name="health_check-node",
),
]
),
)
]
```
> [!TIP]
> Protect this endpoint with a secret token. Do not make it publicly accessible.
> See the [Security](install.md#security) section of the installation guide.
### Kubernetes probes
Kubernetes uses liveness and readiness probes to decide when to restart a pod.
We hit the endpoint over HTTP, which also proves the whole HTTP stack works.
> [!NOTE]
> Bind your WSGI/ASGI server to `0.0.0.0`, not just `127.0.0.1`, so the kubelet
> can reach the `httpGet` probes.
For our health check endpoint, the configuration looks like this:
```yaml
# deployment.yaml
livenessProbe:
httpGet:
path: /health/${HEALTH_CHECK_SECRET:-dev}/node/
port: 8000
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /health/${HEALTH_CHECK_SECRET:-dev}/node/
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
```
See the [Kubernetes documentation](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#define-a-liveness-http-request)
for more details.
### Docker / Podman
Compose has no native HTTP probes, so we use the
[`health_check` command](usage.md#django-command) instead. It doesn't need
CURL in the image, and it can emulate proxy requests to satisfy HTTPS
requirements.
```yaml
# compose.yml
services:
web:
# β¦ your service definition β¦
healthcheck:
test: ["CMD", "python", "manage.py", "health_check", "health_check-node", "web:8000"]
interval: 30s
timeout: 10s
```
### Load balancers
Most reverse-proxies can load balance.
Once configured, they only route traffic to healthy instances.
In [Caddy][caddy-active-health-checks], the configuration looks like this:
```caddy
# Caddyfile
example.com {
reverse_proxy localhost:8000 {
health_uri /health/${HEALTH_CHECK_SECRET:-dev}/node/
health_interval 30s
health_timeout 5s
}
}
```
β¦ in [Traefik][traefik-health-checks], the configuration looks like this:
```yaml
# compose.yml
services:
app:
image: myapp
labels:
- "traefik.http.services.myapp.loadbalancer.healthcheck.path=/health/${HEALTH_CHECK_SECRET:-dev}/node/"
- "traefik.http.services.myapp.loadbalancer.healthcheck.interval=30s"
- "traefik.http.services.myapp.loadbalancer.healthcheck.timeout=5s"
```
β¦ in [Nginx][nginx-health-checks], the configuration looks like this:
```nginx
# nginx.conf
upstream myapp {
server localhost:8000;
health_check interval=30s timeout=5s uri=/health/${HEALTH_CHECK_SECRET:-dev}/node/;
}
```
β¦ and in [HAProxy][haproxy-health-checks], the configuration looks like this:
```haproxy
# haproxy.cfg
backend myapp
server app1 localhost:8000 check inter 30s fall 3 rise 2
http-check expect status 200
http-check send meth GET uri /health/${HEALTH_CHECK_SECRET:-dev}/node/
```
## Application health checks
Application checks make sure every production service your app depends on is
reachable and operational. **Uptime monitors** such as Pingdom,
Better Uptime, or StatusCake watch these endpoints and page on-call engineers when
something goes down.
To cover the whole stack β databases, caches, message brokers, email, storage β
you'll need some extra dependencies:
```shell
pip install "django-health-check[redis,rabbitmq,celery]"
```
```python
# urls.py
import os
from django.urls import include, path
from health_check.views import HealthCheckView
from redis.asyncio import Redis as RedisClient
application_checks = [
# Django built-ins
"health_check.Cache",
"health_check.Database",
"health_check.Mail",
"health_check.Storage",
# Message brokers & caches
(
"health_check.contrib.redis.Redis",
{"client_factory": lambda: RedisClient.from_url("redis://localhost:6379")},
),
(
"health_check.contrib.rabbitmq.RabbitMQ",
{"amqp_url": "amqp://guest:guest@localhost:5672//"},
),
"health_check.contrib.celery.Ping",
]
urlpatterns = [
# β¦
path(
f"health/{os.getenv('HEALTH_CHECK_SECRET', 'dev')}/",
include(
[
# other endpoints β¦
path(
"application/",
HealthCheckView.as_view(checks=application_checks),
name="health_check-application",
),
]
),
)
]
```
Point the uptime monitor at `https://example.com/health//application/`.
It returns HTTP 200 when all checks pass and HTTP 500 when any fail β exactly the
codes monitors watch for.
## Pipeline health checks
Pipeline checks add **upstream provider status** on top of your application checks β
cloud platforms, PaaS providers, third-party services. The dev team watches these
via RSS/Atom feeds in Slack or Matrix, so upstream outages surface before they become
support tickets.
Install the extras that read the feeds from cloud providers:
```shell
pip install "django-health-check[rss,atlassian]"
```
Add the pipeline endpoint to your URL configuration.
```python
# urls.py
import os
from django.urls import include, path
from health_check.views import HealthCheckView
application_checks = [
# configured previously β¦
]
pipeline_checks = [
# You may want to include application health alerts here too
*application_checks,
# Cloud provider status (pick the ones relevant to your stack)
# GitHub status; to filter by a specific component, use a
# tuple like ("health_check.contrib.atlassian.GitHub", {"component": ""})
"health_check.contrib.atlassian.GitHub",
"health_check.contrib.atlassian.Cloudflare",
(
"health_check.contrib.rss.AWS",
{"region": "eu-west-1", "service": "s3"},
),
]
urlpatterns = [
# β¦
path(
f"health/{os.getenv('HEALTH_CHECK_SECRET', 'dev')}/",
include(
[
# other endpoints β¦
path(
"pipeline/",
HealthCheckView.as_view(checks=pipeline_checks),
name="health_check-pipeline",
),
]
),
)
]
```
### Slack or Matrix
You can now alert engineers about upstream outages in Slack or Matrix.
Subscribe to the RSS feed in Slack:
1. Install the [Slack RSS App](https://slack.com/help/articles/218688467-Add-RSS-feeds-to-Slack).
1. In your `#ops` or `#incidents` channel, run:
```
/feed subscribe https://www.example.com/health//pipeline/?format=rss
```
... or subscribe in Matrix:
```toml
# config.toml
[[bridge]]
name = "Pipeline Health Monitor"
feed_url = "https://example.com/health//pipeline/?format=rss"
room_id = "!YourRoomId:matrix.org"
```
______________________________________________________________________
## Complete example
```python
# urls.py
import os
from django.urls import include, path
from health_check.views import HealthCheckView
from redis.asyncio import Redis as RedisClient
node_checks = [
"health_check.contrib.psutil.CPU",
"health_check.contrib.psutil.Memory",
"health_check.contrib.psutil.Disk",
]
application_checks = [
"health_check.Cache",
"health_check.Database",
"health_check.Mail",
"health_check.Storage",
(
"health_check.contrib.redis.Redis",
{"client_factory": lambda: RedisClient.from_url("redis://localhost:6379")},
),
(
"health_check.contrib.rabbitmq.RabbitMQ",
{"amqp_url": "amqp://guest:guest@localhost:5672//"},
),
"health_check.contrib.celery.Ping",
]
pipeline_checks = [
*application_checks,
(
"health_check.contrib.atlassian.GitHub",
{"component": "Actions"},
),
"health_check.contrib.atlassian.Cloudflare",
(
"health_check.contrib.rss.AWS",
{"region": "eu-west-1", "service": "s3"},
),
]
urlpatterns = [
path(
"health/{os.getenv('HEALTH_CHECK_SECRET', 'dev')}/",
include(
[
# Tier 1 β node: liveness & readiness probes
path(
"node/",
HealthCheckView.as_view(checks=node_checks),
name="health_check-node",
),
# Tier 2 β application: uptime monitors & on-call alerts
path(
"application/",
HealthCheckView.as_view(checks=application_checks),
name="health_check-application",
),
# Tier 3 β pipeline: developer RSS/Atom feeds (Slack, Matrix)
path(
"pipeline/",
HealthCheckView.as_view(checks=pipeline_checks),
name="health_check-pipeline",
),
]
),
),
]
```
[caddy-active-health-checks]: https://www.haproxy.com/documentation/haproxy-configuration-tutorials/reliability/health-checks/#http-health-checks
[haproxy-health-checks]: https://www.haproxy.com/documentation/hapee/2-5r1/traffic-management/load-balancing/health-checks/
[nginx-health-checks]: https://nginx.org/en/docs/http/ngx_http_upstream_hc_module.html#health_check
[traefik-health-checks]: https://doc.traefik.io/traefik/routing/services/#health-check
django-health-check-4.5.1/docs/images/ 0000775 0000000 0000000 00000000000 15242136254 0017502 5 ustar 00root root 0000000 0000000 django-health-check-4.5.1/docs/images/icon.svg 0000664 0000000 0000000 00000000613 15242136254 0021153 0 ustar 00root root 0000000 0000000
django-health-check-4.5.1/docs/images/logo-dark.svg 0000664 0000000 0000000 00000001757 15242136254 0022114 0 ustar 00root root 0000000 0000000
django-health-check-4.5.1/docs/images/logo-light.svg 0000664 0000000 0000000 00000001721 15242136254 0022271 0 ustar 00root root 0000000 0000000
django-health-check-4.5.1/docs/index.md 0000664 0000000 0000000 00000006345 15242136254 0017676 0 ustar 00root root 0000000 0000000
# Django HealthCheck
[](https://pypi.python.org/pypi/django-health-check/)
[](https://pypi.python.org/pypi/django-health-check/)
[](https://pypi.python.org/pypi/django-health-check/)
[](https://pypi.python.org/pypi/django-health-check/)
This project monitors your services and reports back when something looks wrong.
[Documentation](https://codingjoe.dev/django-health-check/) | [Issues](https://github.com/codingjoe/django-health-check/issues/new/choose) | [Changelog](https://github.com/codingjoe/django-health-check/releases) | [Funding](https://github.com/sponsors/codingjoe) π
The following health checks are bundled with this project:
- [caches][health_check.Cache] & [databases][health_check.Database]
- [disk][health_check.contrib.psutil.Disk] & [memory][health_check.contrib.psutil.Memory] utilization
- [DNS][health_check.DNS] & [email][health_check.Mail]
- [storages][health_check.Storage]
- [Celery][health_check.contrib.celery.Ping], [Kafka][health_check.contrib.kafka.Kafka],
[RabbitMQ][health_check.contrib.rabbitmq.RabbitMQ] & [Redis][health_check.contrib.redis.Redis]
- [Cloud provider status](checks.md#cloud-provider-status) for numerous cloud and PaaS providers.
The bundled checks cover the common cases. If your stack is different, you can write your own.
Contributions are welcome. Open a pull request and we'll take a look.
## Integrations
The main entry point is an HTML view for your web application. However,
there are multiple machine-readable formats available for integration:
- [HTTP Status Codes] for load balancers and uptime monitors
- [OpenMetrics] for [Prometheus] and [Grafana]
- [CLI](usage.md#django-command) for [Docker], [Podman] or [Kubernetes] health checks
- [RSS]/[Atom] feed
- [JSON] API for custom integrations
[atom]: https://en.wikipedia.org/wiki/Atom_(standard)
[docker]: https://www.docker.com/
[grafana]: https://grafana.com/
[http status codes]: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
[json]: https://www.json.org/
[kubernetes]: https://kubernetes.io/
[openmetrics]: https://openmetrics.io/
[podman]: https://podman.io/
[prometheus]: https://prometheus.io/
[rss]: https://en.wikipedia.org/wiki/RSS
django-health-check-4.5.1/docs/install.md 0000664 0000000 0000000 00000005477 15242136254 0020242 0 ustar 00root root 0000000 0000000 # Installation
Add the `django-health-check` package to your project:
```shell
uv add django-health-check
# or
pip install django-health-check
```
Add `health_check` and any desired plugins to your `INSTALLED_APPS` in `settings.py`:
```python
# settings.py
INSTALLED_APPS = [
# β¦
"health_check", # required
]
```
Add a health check view to your URL configuration. For example:
```python
# urls.py
from django.urls import include, path
from health_check.views import HealthCheckView
from redis.asyncio import Redis as RedisClient
urlpatterns = [
# β¦
path(
"health/",
HealthCheckView.as_view(
checks=[ # optional, default is all but 3rd party checks
"health_check.Cache",
"health_check.DNS",
"health_check.Database",
"health_check.Mail",
"health_check.Storage",
# 3rd party checks
"health_check.contrib.psutil.Disk",
"health_check.contrib.psutil.Memory",
"health_check.contrib.celery.Ping",
(
"health_check.contrib.kafka.Kafka",
{"bootstrap_servers": ["localhost:9092"]},
),
( # tuple with options
"health_check.contrib.rabbitmq.RabbitMQ",
{"amqp_url": "amqp://guest:guest@localhost:5672//"},
),
(
"health_check.contrib.redis.Redis",
{
"client_factory": lambda: RedisClient.from_url(
"redis://localhost:6379"
)
},
),
# AWS service status check
(
"health_check.contrib.rss.AWS",
{"region": "eu-west-1", "service": "s3"},
),
],
),
name="health_check",
),
]
```
> [!NOTE]
> The `name` argument is required β the `health_check` command needs it to
> resolve this endpoint URL via `reverse()`. When you run the command, pass this name as
> the `endpoint` argument, for example `django-admin health_check health_check`.
## Security
You can keep the health check endpoint private by adding a secure token to your URL.
1. Enable HTTPS.
1. Generate a strong secret token:
```shell
python -c "import secrets; print(secrets.token_urlsafe())"
```
> [!WARNING]
> Do NOT use Django's `SECRET_KEY` setting!
1. Add it to your URL configuration:
```python
# urls.py
from django.urls import include, path
from health_check.views import HealthCheckView
urlpatterns = [
# β¦
path(
"health/super_secret_token/",
HealthCheckView.as_view(),
name="health_check",
),
]
```
django-health-check-4.5.1/docs/migrate-to-v4.md 0000664 0000000 0000000 00000004151 15242136254 0021157 0 ustar 00root root 0000000 0000000 # Migration to version 4.x
> [!IMPORTANT]
> Version 3.21 started deprecating the old settings, checks, and APIs.
> To make the switch easier, versions >=3.21 still support both the OLD and NEW configuration styles.
1. If you have `health_check.db` in your `INSTALLED_APPS`, revert the migration to drop the `TestModel` table:
```shell
python manage.py migrate db zero
```
1. Update the dependencies. Include the extras for the checks you want to use.
```shell
uv add 'django-health-check[psutil,celery,kafka,rabbitmq,redis,rss,atlassian]>=4.0.0'
```
1. Remove these `health_check.*` subβapps from `INSTALLED_APPS`. Keep `health_check`!
1. Remove all `HEALTH_CHECK_*` settings from your settings file.
1. Replace the URL include with the view and explicit `checks` list.
Before:
```python
# urls.py
path("ht/", include("health_check.urls"))
```
After (example):
```python
# urls.py
from health_check.views import HealthCheckView
path(
"ht/",
HealthCheckView.as_view(
checks=[
"health_check.Cache",
"health_check.Database",
"health_check.Mail",
"health_check.Storage",
# 3rd party checks
"health_check.contrib.psutil.Disk",
"health_check.contrib.psutil.Memory",
"health_check.contrib.celery.Ping",
"health_check.contrib.rabbitmq.RabbitMQ",
"health_check.contrib.redis.Redis",
]
),
name="health_check",
)
```
## Removals and Replacements
- The classes `StorageHealthCheck`, `DefaultFileStorageHealthCheck`, `S3BotoStorageHealthCheck`, and `S3Boto3StorageHealthCheck` are replaced with [Storage][health_check.Storage].
- `CeleryHealthCheck` is replaced with [Ping][health_check.contrib.celery.Ping].
- `MigrationsHealthCheck` is removed. [Django's check framework](https://docs.djangoproject.com/en/stable/topics/checks/) covers its function.
- `DatabaseHealthCheck` is replaced with [Database][health_check.Database]. It does not require a table. It supports multiple database aliases.
django-health-check-4.5.1/docs/usage.md 0000664 0000000 0000000 00000012005 15242136254 0017661 0 ustar 00root root 0000000 0000000 # Usage
## Setting up monitoring
You can point an uptime monitor such as Pingdom, StatusCake, or other
uptime robots at your service. The `/health/` endpoint returns an HTTP
200 when every check passes, and an HTTP 500 when any of them fails.
For step-by-step examples of multi-tier endpoint setups, including uptime
monitoring, container probes, reverse-proxy configuration, and RSS/Atom
integration into Slack or Matrix, see the [Cookbook](cookbook.md).
## Getting machine-readable reports
### Plain text
For simple monitoring and scripting, ask for plain text. Set the `Accept` HTTP header to `text/plain`, or pass `format=text` as a query parameter.
When everything passes, you get a plain text response with HTTP 200. When a check fails, you get HTTP 500:
```shell
$ curl -v -X GET -H "Accept: text/plain" http://www.example.com/health/
> GET /health/ HTTP/1.1
> Host: www.example.com
> Accept: text/plain
>
< HTTP/1.1 200 OK
< Content-Type: text/plain; charset=utf-8
CacheBackend: OK
DatabaseBackend: OK
S3BotoStorageHealthCheck: OK
$ curl -v -X GET http://www.example.com/health/?format=text
> GET /health/?format=text HTTP/1.1
> Host: www.example.com
>
< HTTP/1.1 200 OK
< Content-Type: text/plain; charset=utf-8
CacheBackend: OK
DatabaseBackend: OK
S3BotoStorageHealthCheck: OK
```
This format is handy for command-line tools and simple scripts that don't want to parse JSON.
### JSON
Want machine-readable results? Set the `Accept` HTTP header to
`application/json`, or pass `format=json` as a query parameter.
The endpoint returns a JSON response:
```shell
$ curl -v -X GET -H "Accept: application/json" http://www.example.com/health/
> GET /health/ 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/health/?format=json
> GET /health/?format=json HTTP/1.1
> Host: www.example.com
>
< HTTP/1.1 200 OK
< Content-Type: application/json
{
"CacheBackend": "working",
"DatabaseBackend": "working",
"S3BotoStorageHealthCheck": "working"
}
```
### OpenMetrics for Prometheus
If you monitor with Prometheus, request the OpenMetrics format:
```shell
$ curl http://www.example.com/health/?format=openmetrics
```
Prometheus can scrape these metrics directly.
### RSS and Atom feeds
For feed readers and monitoring tools, request the RSS or Atom format:
```shell
$ curl http://www.example.com/health/?format=rss
$ curl http://www.example.com/health/?format=atom
```
You can also use the `Accept` header:
```shell
$ curl -H "Accept: application/rss+xml" http://www.example.com/health/
$ curl -H "Accept: application/atom+xml" http://www.example.com/health/
```
These endpoints always answer with HTTP 200. The feed lists each check, and failed checks show up as categories and item descriptions.
## Writing a custom health check
You can write your own checks too. Inherit from
[HealthCheck][health_check.HealthCheck] and implement the `run` method.
::: health_check.HealthCheck
## Django command
Run the Django command `health_check` from the shell, or schedule it
with cron:
```shell
django-admin health_check health_check
```
The `endpoint` argument is the `name` of the health check URL pattern defined in your
`urls.py` (see the [installation guide](install.md)). The command looks it up with
`reverse()` and runs the checks over HTTP against the running server:
```
Database ... OK
CustomHealthCheck ... Unavailable: Something went wrong!
```
Pass `--no-http` to skip the HTTP server entirely. Handy for container
health checks that don't run a web server:
```shell
django-admin health_check health_check --no-http
```
A critical error exits the command with the code `1`.
## Performance tweaks
Every check runs asynchronously, via `asyncio` or a thread pool, depending
on how each check is implemented.
IO-bound checks run in parallel, so responses come back faster.
Synchronous checks (for example [Database][health_check.checks.Database], [Mail][health_check.checks.Mail],
or [Storage][health_check.checks.Storage]) run in the event loop's default thread pool.
That pool usually persists between requests. It keeps things fast, but it can
grow memory usage. That's a problem for some applications, especially `S3Storage`,
which uses thread-local connections.
To avoid that, use a custom executor that spins up a fresh thread pool per
request and tears it down when the checks finish. Subclass `HealthCheckView`
and override the `get_executor` method to return a context manager with a
new `ThreadPoolExecutor` each time.
```python
from concurrent.futures import ThreadPoolExecutor
from health_check.views import HealthCheckView
class CustomHealthCheckView(HealthCheckView):
def get_executor(self):
return ThreadPoolExecutor(max_workers=len(self.checks))
```
This gives every request its own thread pool. You keep the speed of
concurrent execution for synchronous checks, but the memory stays under
control.
django-health-check-4.5.1/health_check/ 0000775 0000000 0000000 00000000000 15242136254 0017707 5 ustar 00root root 0000000 0000000 django-health-check-4.5.1/health_check/__init__.py 0000664 0000000 0000000 00000000616 15242136254 0022023 0 ustar 00root root 0000000 0000000 """Monitor the health of your Django app and its connected services."""
from . import _version # noqa
from .base import HealthCheck
from .checks import Cache, DNS, Database, Mail, Storage
__version__ = _version.__version__
VERSION = _version.__version_tuple__
__all__ = [
"__version__",
"VERSION",
"HealthCheck",
"Cache",
"DNS",
"Database",
"Mail",
"Storage",
]
django-health-check-4.5.1/health_check/base.py 0000664 0000000 0000000 00000006565 15242136254 0021207 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import abc
import asyncio
import dataclasses
import inspect
import logging
import timeit
from concurrent.futures import Executor
from health_check.exceptions import HealthCheckException
logger = logging.getLogger(__name__)
@dataclasses.dataclass
class HealthCheckResult:
"""Result of a health check execution."""
check: HealthCheck
error: HealthCheckException | None
time_taken: float
@dataclasses.dataclass
class HealthCheck(abc.ABC):
"""
Base class for defining health checks.
Subclasses should implement the `run` method to perform the actual health check logic.
The `run` method can be either synchronous or asynchronous.
Examples:
>>> import dataclasses
>>> from health_check.base import HealthCheck
>>>
>>> @dataclasses.dataclass
>>> class MyHealthCheck(HealthCheck):
...
... async def run(self):
... # Implement health check logic here
Subclasses should be [dataclasses][dataclasses.dataclass] or implement their own `__repr__` method
to provide meaningful representations in health check reports.
Warning:
The `__repr__` method is used in health check reports.
Consider setting `repr=False` for sensitive dataclass fields
to avoid leaking sensitive information or credentials.
"""
@abc.abstractmethod
async def run(self) -> None:
"""
Run the health check logic and raise human-readable exceptions as needed.
Exception must be reraised to indicate the health status and provide context.
Any unexpected exceptions will be caught and logged for security purposes
while returning a generic error message.
Warning:
Exception messages must not contain sensitive information.
Raises:
ServiceWarning: If the service is at a critical state but still operational.
ServiceUnavailable: If the service is not operational.
ServiceReturnedUnexpectedResult: If the check performs a computation that returns an unexpected result.
"""
...
def pretty_status(self) -> str:
"""Return a human-readable status string, always 'OK' for the check itself."""
return "OK"
@property
def labels(self) -> dict[str, str]:
"""Return a human-readable label for the check, defaulting to the class name."""
return {
"check": self.__class__.__name__,
} | {
field.name: str(value)
for field in dataclasses.fields(self)
if field.repr and (value := getattr(self, field.name)) is not None
}
async def get_result(self, executor: Executor | None = None) -> HealthCheckResult:
loop = asyncio.get_running_loop()
start = timeit.default_timer()
try:
await self.run() if inspect.iscoroutinefunction(
self.run
) else await loop.run_in_executor(executor, self.run)
except HealthCheckException as e:
error = e
except BaseException:
logger.exception("Unexpected exception during health check")
error = HealthCheckException("unknown error")
else:
error = None
return HealthCheckResult(
check=self,
error=error,
time_taken=timeit.default_timer() - start,
)
django-health-check-4.5.1/health_check/checks.py 0000664 0000000 0000000 00000024137 15242136254 0021530 0 ustar 00root root 0000000 0000000 """Health check implementations for Django built-in services."""
import dataclasses
import datetime
import logging
import smtplib
import socket
import uuid
import django
import dns.asyncresolver
from django import db
from django.core.cache import CacheKeyWarning, caches
from django.core.cache.backends.base import InvalidCacheBackendError
from django.core.files.base import ContentFile
from django.core.files.storage import InvalidStorageError, storages
from django.core.files.storage import Storage as DjangoStorage
from django.core.mail import get_connection
from django.core.mail.backends.base import BaseEmailBackend
if django.VERSION >= (6, 1):
from django.core.mail import DEFAULT_MAILER_ALIAS, mailers
else: # Django < 6.1
DEFAULT_MAILER_ALIAS = "default"
from django.db import connections
from django.db.models import Expression
from django.utils.connection import ConnectionDoesNotExist
from health_check.base import HealthCheck
from health_check.exceptions import (
ServiceReturnedUnexpectedResult,
ServiceUnavailable,
)
try:
# Exceptions thrown by Redis do not subclass builtin exceptions like ConnectionError.
# Additionally, not only connection errors (ConnectionError -> RedisError) can be raised,
# but also errors for time-outs (TimeoutError -> RedisError)
# and if the backend is read-only (ReadOnlyError -> ResponseError -> RedisError).
# Since we know what we are trying to do here, we are not picky and catch the global exception RedisError.
from redis.exceptions import RedisError
except ModuleNotFoundError:
# In case Redis is not installed and another cache backend is used.
class RedisError(Exception):
pass
logger = logging.getLogger(__name__)
@dataclasses.dataclass
class Cache(HealthCheck):
"""
Check that the cache backend is able to set and get a value.
It can be setup multiple times for different cache aliases if needed.
Args:
alias: The cache alias to test against.
key_prefix: Prefix for the node specific cache key.
timeout: Time until probe keys expire in the cache backend.
"""
alias: str = "default"
key_prefix: str = dataclasses.field(default="djangohealthcheck_test", repr=False)
timeout: datetime.timedelta = dataclasses.field(
default=datetime.timedelta(seconds=5), repr=False
)
async def run(self):
try:
cache = caches[self.alias]
except InvalidCacheBackendError as e:
raise ServiceUnavailable("Cache alias does not exist") from e
# Use an isolated key per probe run to avoid cross-process write races.
cache_key = f"{self.key_prefix}:{uuid.uuid4().hex}"
cache_value = f"itworks-{datetime.datetime.now().timestamp()}"
try:
await cache.aset(
cache_key,
cache_value,
timeout=self.timeout.total_seconds(),
)
if not await cache.aget(cache_key) == cache_value:
raise ServiceUnavailable(f"Cache key {cache_key} does not match")
except CacheKeyWarning as e:
raise ServiceReturnedUnexpectedResult("Cache key warning") from e
except ValueError as e:
raise ServiceReturnedUnexpectedResult("ValueError") from e
except (ConnectionError, RedisError) as e:
raise ServiceReturnedUnexpectedResult("Connection Error") from e
class _SelectOne(Expression):
"""An expression that represents a simple SELECT 1; query."""
def as_sql(self, compiler, connection):
return "SELECT 1", []
def as_oracle(self, compiler, connection):
return "SELECT 1 FROM DUAL", []
@dataclasses.dataclass
class Database(HealthCheck):
"""
Check database operation by executing a simple SELECT 1 query.
It can be setup multiple times for different database connections if needed.
No actual data is read from or written to the database to minimize the performance impact
and work with conservative database user permissions.
Args:
alias: The alias of the database connection to check.
"""
alias: str = "default"
def run(self):
try:
connection = connections[self.alias]
except ConnectionDoesNotExist as e:
raise ServiceUnavailable("Database alias does not exist") from e
try:
compiler = connection.ops.compiler("SQLCompiler")(
_SelectOne(), connection, None
)
with connection.temporary_connection() as cursor:
cursor.execute(*compiler.compile(_SelectOne()))
result = cursor.fetchone()
except db.Error as e:
raise ServiceUnavailable(str(e).rsplit(":")[0]) from e
else:
if result != (1,):
raise ServiceUnavailable(
"Health Check query did not return the expected result."
)
@dataclasses.dataclass
class DNS(HealthCheck):
"""
Check DNS resolution by resolving the server's hostname.
Verifies that DNS resolution is working using the system's configured
DNS servers, as well as nameserver resolution for the provided hostname.
Args:
hostname: The hostname to resolve.
timeout: DNS query timeout.
"""
hostname: str = dataclasses.field(default_factory=socket.gethostname)
timeout: datetime.timedelta = dataclasses.field(
default=datetime.timedelta(seconds=5), repr=False
)
nameservers: list[str] | None = dataclasses.field(default=None, repr=False)
async def run(self):
logger.debug("Attempting to resolve hostname: %s", self.hostname)
resolver = dns.asyncresolver.Resolver()
resolver.lifetime = self.timeout.total_seconds()
if self.nameservers is not None:
resolver.nameservers = self.nameservers
try:
# Perform DNS resolution (A record by default)
answers = await resolver.resolve(self.hostname, "A")
except dns.resolver.NXDOMAIN as e:
raise ServiceUnavailable(
f"DNS resolution failed: hostname {self.hostname} does not exist"
) from e
except dns.resolver.NoAnswer as e:
raise ServiceUnavailable(
f"DNS resolution failed: no answer for {self.hostname}"
) from e
except dns.resolver.Timeout as e:
raise ServiceUnavailable(
f"DNS resolution failed: timeout resolving {self.hostname}"
) from e
except dns.resolver.NoNameservers as e:
raise ServiceUnavailable(
"DNS resolution failed: no nameservers available"
) from e
except dns.exception.DNSException as e:
raise ServiceUnavailable(f"DNS resolution failed: {e}") from e
else:
logger.debug(
"Successfully resolved %s to %s",
self.hostname,
[str(rdata) for rdata in answers],
)
@dataclasses.dataclass
class Mail(HealthCheck):
"""
Check that an email backend is able to open and close the connection.
Args:
backend: The email backend to test against. Legacy, used only on Django < 6.1.
alias: The mailer alias to test.
timeout: Timeout for connection to an email server in seconds.
"""
backend: str | None = dataclasses.field(default=None, repr=False)
alias: str = DEFAULT_MAILER_ALIAS
timeout: datetime.timedelta = dataclasses.field(
default=datetime.timedelta(seconds=15), repr=False
)
def _get_connection(self) -> BaseEmailBackend:
if django.VERSION >= (6, 1):
return mailers[self.alias]
return get_connection(self.backend, fail_silently=False)
def run(self) -> None:
connection: BaseEmailBackend = self._get_connection()
connection.timeout = self.timeout.total_seconds()
logger.debug("Trying to open connection to mail backend.")
try:
connection.open()
except smtplib.SMTPException as e:
raise ServiceUnavailable(
"Failed to open connection with SMTP server"
) from e
except ConnectionRefusedError as e:
raise ServiceUnavailable("Connection refused error") from e
finally:
connection.close()
logger.debug("Connection established. Mail backend %r is healthy.", connection)
@dataclasses.dataclass
class Storage(HealthCheck):
"""
Check file storage backends by saving, reading, and deleting a test file.
It can be set up multiple times for different storage backends if needed.
Args:
alias: The alias of the storage backend to check.
"""
alias: str = "default"
@property
def storage(self) -> DjangoStorage:
try:
return storages[self.alias]
except InvalidStorageError as e:
raise ServiceUnavailable("Storage alias does not exist") from e
def get_file_name(self):
return f"health_check_storage_test/test-{uuid.uuid4()}.txt"
def get_file_content(self):
return f"# generated by health_check.Storage at {datetime.datetime.now().timestamp()}".encode()
def check_save(self, file_name, file_content):
# save the file
file_name = self.storage.save(file_name, ContentFile(content=file_content))
# read the file and compare
if not self.storage.exists(file_name):
raise ServiceUnavailable("File does not exist")
with self.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):
# delete the file and make sure it is gone
self.storage.delete(file_name)
if self.storage.exists(file_name):
raise ServiceUnavailable("File was not deleted")
def run(self):
# write the file to the storage backend
file_name = self.get_file_name()
file_content = self.get_file_content()
try:
file_name = self.check_save(file_name, file_content)
self.check_delete(file_name)
finally:
self.storage.delete(file_name)
django-health-check-4.5.1/health_check/contrib/ 0000775 0000000 0000000 00000000000 15242136254 0021347 5 ustar 00root root 0000000 0000000 django-health-check-4.5.1/health_check/contrib/__init__.py 0000664 0000000 0000000 00000000000 15242136254 0023446 0 ustar 00root root 0000000 0000000 django-health-check-4.5.1/health_check/contrib/atlassian.py 0000664 0000000 0000000 00000021665 15242136254 0023712 0 ustar 00root root 0000000 0000000 """Atlassian Status Page API health checks for cloud provider status pages."""
import dataclasses
import datetime
import enum
import logging
import httpx
from health_check import HealthCheck, __version__
from health_check.exceptions import (
ServiceReturnedUnexpectedResult,
ServiceUnavailable,
StatusPageWarning,
)
logger = logging.getLogger(__name__)
class AtlassianStatusPage(HealthCheck):
"""
Base class for Atlassian status page health checks.
Monitor cloud provider service health via Atlassian Status Page API v2.
Each subclass should define the `base_url` for the specific status page
and appropriate `timeout` value.
When `component` is non-empty, only incidents affecting that named component are
reported. Use separate check instances to monitor multiple components independently.
Examples:
>>> import dataclasses
>>> import datetime
>>> from health_check.contrib.atlassian import AtlassianStatusPage
>>> @dataclasses.dataclass
... class FlyIo(AtlassianStatusPage):
... timeout: datetime.timedelta = datetime.timedelta(seconds=10)
... base_url: str = dataclasses.field(default="https://status.flyio.net", init=False, repr=False)
"""
base_url: str = NotImplemented
timeout: datetime.timedelta = NotImplemented
component: str = ""
async def run(self):
if incidents := [i async for i in self._fetch_incidents()]:
raise StatusPageWarning(
"\n".join(msg for msg, _ in incidents),
timestamp=max(ts for _, ts in incidents),
)
logger.debug("No incidents found")
async def _fetch_incidents(self):
api_url = f"{self.base_url}/api/v2/summary.json"
logger.debug("Fetching incidents from %r", api_url)
async with httpx.AsyncClient() as client:
try:
response = await client.get(
api_url,
headers={"User-Agent": f"django-health-check@{__version__}"},
timeout=self.timeout.total_seconds(),
follow_redirects=True,
)
except httpx.TimeoutException as e:
raise ServiceUnavailable("API request timed out") from e
except httpx.RequestError as e:
raise ServiceUnavailable(f"Failed to fetch API: {e}") from e
try:
response.raise_for_status()
except httpx.HTTPStatusError as e:
raise ServiceUnavailable(
f"HTTP error {e.response.status_code} fetching API from {api_url!r}"
) from e
try:
data = response.json()
except ValueError as e:
raise ServiceUnavailable("Failed to parse JSON response") from e
if self.component:
components_by_name = {c["name"]: c for c in data["components"]}
try:
_ = components_by_name[self.component]
except KeyError as e:
raise ServiceReturnedUnexpectedResult(
f"Component {self.component!r} not found"
) from e
try:
for incident in data["incidents"]:
if (incident.get("status") not in ("resolved", "postmortem")) and (
not self.component
or any(c["name"] == self.component for c in incident["components"])
):
yield (
f"{incident['name']}: {incident['shortlink']}",
datetime.datetime.fromisoformat(
incident["updated_at"].replace("Z", "+00:00")
),
)
except KeyError as e:
raise ServiceReturnedUnexpectedResult(
"Unexpected API response structure"
) from e
@dataclasses.dataclass
class Cloudflare(AtlassianStatusPage):
"""
Check Cloudflare platform status via Atlassian Status Page API v2.
Args:
timeout: Request timeout duration.
component: Name of a specific component to monitor. Monitors all
components when empty.
"""
timeout: datetime.timedelta = dataclasses.field(
default=datetime.timedelta(seconds=10), repr=False
)
base_url: str = dataclasses.field(
default="https://www.cloudflarestatus.com", init=False, repr=False
)
component: str = ""
@dataclasses.dataclass
class FlyIo(AtlassianStatusPage):
"""
Check Fly.io platform status via Atlassian Status Page API v2.
Args:
timeout: Request timeout duration.
component: Name of a specific component to monitor. Monitors all
components when empty.
"""
timeout: datetime.timedelta = dataclasses.field(
default=datetime.timedelta(seconds=10), repr=False
)
base_url: str = dataclasses.field(
default="https://status.flyio.net", init=False, repr=False
)
component: str = ""
@dataclasses.dataclass
class GitHub(AtlassianStatusPage):
"""
Check GitHub platform status via Atlassian Status Page API v2.
Args:
enterprise_region: GitHub Enterprise status page region (if applicable).
timeout: Request timeout duration.
component: Name of a specific component to monitor. Monitors all
components when empty.
"""
try:
class EnterpriseRegion(enum.StrEnum):
"""GitHub Enterprise status page regions."""
australia = "au"
"""Australia."""
eu = "eu"
"""Europe."""
japan = "jp"
"""Japan."""
us = "us"
"""United States."""
enterprise_region: EnterpriseRegion | None = None
except AttributeError:
# Python <3.11 doesn't have StrEnum, so fall back to a simple string field with validation
enterprise_region: str | None = None
timeout: datetime.timedelta = dataclasses.field(
default=datetime.timedelta(seconds=10), repr=False
)
component: str = ""
def __post_init__(self):
self.base_url = f"https://{self.enterprise_region if self.enterprise_region else 'www'}.githubstatus.com"
@dataclasses.dataclass
class PlatformSh(AtlassianStatusPage):
"""
Check Platform.sh platform status via Atlassian Status Page API v2.
Args:
timeout: Request timeout duration.
component: Name of a specific component to monitor. Monitors all
components when empty.
"""
timeout: datetime.timedelta = dataclasses.field(
default=datetime.timedelta(seconds=10), repr=False
)
base_url: str = dataclasses.field(
default="https://status.platform.sh", init=False, repr=False
)
component: str = ""
@dataclasses.dataclass
class DigitalOcean(AtlassianStatusPage):
"""
Check DigitalOcean platform status via Atlassian Status Page API v2.
Args:
timeout: Request timeout duration.
component: Name of a specific component to monitor. Monitors all
components when empty.
"""
timeout: datetime.timedelta = dataclasses.field(
default=datetime.timedelta(seconds=10), repr=False
)
base_url: str = dataclasses.field(
default="https://status.digitalocean.com", init=False, repr=False
)
component: str = ""
@dataclasses.dataclass
class Render(AtlassianStatusPage):
"""
Check Render platform status via Atlassian Status Page API v2.
Args:
timeout: Request timeout duration.
component: Name of a specific component to monitor. Monitors all
components when empty.
"""
timeout: datetime.timedelta = dataclasses.field(
default=datetime.timedelta(seconds=10), repr=False
)
base_url: str = dataclasses.field(
default="https://status.render.com", init=False, repr=False
)
component: str = ""
@dataclasses.dataclass
class Sentry(AtlassianStatusPage):
"""
Check Sentry platform status via Atlassian Status Page API v2.
Args:
timeout: Request timeout duration.
component: Name of a specific component to monitor. Monitors all
components when empty.
"""
timeout: datetime.timedelta = dataclasses.field(
default=datetime.timedelta(seconds=10), repr=False
)
base_url: str = dataclasses.field(
default="https://status.sentry.io", init=False, repr=False
)
component: str = ""
@dataclasses.dataclass
class Vercel(AtlassianStatusPage):
"""
Check Vercel platform status via Atlassian Status Page API v2.
Args:
timeout: Request timeout duration.
component: Name of a specific component to monitor. Monitors all
components when empty.
"""
timeout: datetime.timedelta = dataclasses.field(
default=datetime.timedelta(seconds=10), repr=False
)
base_url: str = dataclasses.field(
default="https://www.vercel-status.com", init=False, repr=False
)
component: str = ""
django-health-check-4.5.1/health_check/contrib/celery.py 0000664 0000000 0000000 00000005304 15242136254 0023206 0 ustar 00root root 0000000 0000000 """Celery health check."""
import dataclasses
import datetime
import typing
import celery
from celery.app import app_or_default
from health_check.base import HealthCheck
from health_check.exceptions import ServiceUnavailable
@dataclasses.dataclass
class Ping(HealthCheck):
"""
Check Celery worker availability using the ping control command.
Args:
app: Celery application instance to use for the health check, defaults to the [default Celery app][celery.app.default_app].
timeout: Timeout duration for the ping command.
limit: Maximum number of workers to wait for before returning. If not
specified, waits for the full timeout duration. When set, returns
immediately after receiving responses from this many workers.
"""
CORRECT_PING_RESPONSE: typing.ClassVar[dict[str, str]] = {"ok": "pong"}
app: celery.Celery = dataclasses.field(default_factory=app_or_default)
timeout: datetime.timedelta = dataclasses.field(
default=datetime.timedelta(seconds=1), repr=False
)
limit: int | None = dataclasses.field(default=None, repr=False)
def run(self):
try:
ping_result = self.app.control.ping(
timeout=self.timeout.total_seconds(), limit=self.limit
)
except OSError as e:
raise ServiceUnavailable("IOError") from e
except NotImplementedError as e:
raise ServiceUnavailable(
"NotImplementedError: Make sure CELERY_RESULT_BACKEND is set"
) from e
else:
if not ping_result:
raise ServiceUnavailable("Celery workers unavailable")
else:
self.check_active_queues(*self.active_workers(ping_result))
def active_workers(self, ping_result):
for result in ping_result:
worker, response = list(result.items())[0]
if response != self.CORRECT_PING_RESPONSE:
raise ServiceUnavailable(
f"Celery worker {worker} response was incorrect"
)
yield worker
def check_active_queues(self, *active_workers):
try:
defined_queues = {queue.name for queue in self.app.conf.task_queues}
except TypeError:
# conf.task_queues may be None
defined_queues = {self.app.conf.task_default_queue}
active_queues = {
queue.get("name")
for queues in (
self.app.control.inspect(active_workers).active_queues() or {}
).values()
for queue in queues
}
for queue in defined_queues - active_queues:
raise ServiceUnavailable(f"No worker for Celery task queue {queue}")
django-health-check-4.5.1/health_check/contrib/kafka.py 0000664 0000000 0000000 00000004054 15242136254 0023001 0 ustar 00root root 0000000 0000000 """Kafka health check."""
import dataclasses
import datetime
import logging
from confluent_kafka.aio import AIOConsumer
from confluent_kafka.error import KafkaException
from health_check.base import HealthCheck
from health_check.exceptions import ServiceUnavailable
logger = logging.getLogger(__name__)
@dataclasses.dataclass
class Kafka(HealthCheck):
"""
Check Kafka service by connecting to a Kafka broker and listing topics.
Args:
bootstrap_servers: List of Kafka bootstrap servers, e.g., ['localhost:9092'].
timeout: Timeout duration for the connection check as a datetime.timedelta.
"""
bootstrap_servers: list[str]
timeout: datetime.timedelta = dataclasses.field(
default=datetime.timedelta(seconds=10), repr=False
)
async def run(self):
logger.debug(
"Connecting to Kafka bootstrap servers %r ...",
self.bootstrap_servers,
)
# Create a consumer with minimal configuration for health check
timeout_ms = int(self.timeout.total_seconds() * 1000)
consumer = AIOConsumer(
{
"bootstrap.servers": ",".join(self.bootstrap_servers),
"client.id": "health-check",
"group.id": "health-check",
"session.timeout.ms": timeout_ms,
"socket.timeout.ms": timeout_ms,
}
)
try:
if not (
(
cluster_metadata := await consumer.list_topics(
timeout=self.timeout.total_seconds()
)
)
and cluster_metadata.topics
):
raise ServiceUnavailable("Failed to retrieve Kafka topics.")
except KafkaException as e:
raise ServiceUnavailable("Unable to connect") from e
else:
logger.debug(
"Connection established. Kafka is healthy. Found %d topics.",
len(cluster_metadata.topics),
)
finally:
await consumer.close()
django-health-check-4.5.1/health_check/contrib/psutil.py 0000664 0000000 0000000 00000016213 15242136254 0023244 0 ustar 00root root 0000000 0000000 import dataclasses
import datetime
import os
import pathlib
import socket
import psutil
from health_check import HealthCheck
from health_check.exceptions import (
ServiceReturnedUnexpectedResult,
ServiceUnavailable,
ServiceWarning,
)
@dataclasses.dataclass
class Battery(HealthCheck):
"""
Warn about system battery status and power connection.
Args:
min_percent_available: Minimum battery percentage available or None to disable the check.
power_plugged: Whether to warn if the power is unplugged.
"""
min_percent_available: float | None = dataclasses.field(default=20.0, repr=False)
power_plugged: bool = dataclasses.field(default=False, repr=False)
hostname: str = dataclasses.field(default_factory=socket.gethostname, init=False)
def run(self):
try:
battery = psutil.sensors_battery()
except AttributeError as e:
raise ServiceUnavailable("Battery information not available") from e
except ValueError as e:
raise ServiceReturnedUnexpectedResult("ValueError") from e
else:
if (
self.min_percent_available
and battery.percent <= self.min_percent_available
):
raise ServiceWarning(f"Battery {battery.percent:.1f}\u202f%")
if self.power_plugged and not battery.power_plugged:
raise ServiceWarning(
f"Power unplugged with battery at {battery.percent:.1f}\u202f%"
)
@dataclasses.dataclass
class CPU(HealthCheck):
"""
Warn about system CPU utilization.
The utilization represents an interval rather than a point in time measurement.
The interval starts with the previous execution of this check and with the current
unless an explicit interval is provided. An explicit interval will cause a blocking
measurement and increases the execution time of the check by the length of the interval.
Args:
max_usage_percent: Maximum CPU usage in percent or None to disable the check.
interval: The interval to measure CPU usage over or None to use the interval since the last check execution.
"""
max_usage_percent: float | None = dataclasses.field(default=80.0, repr=False)
interval: datetime.timedelta | None = dataclasses.field(default=None, repr=False)
hostname: str = dataclasses.field(default_factory=socket.gethostname, init=False)
def run(self):
try:
usage_percent = psutil.cpu_percent(
interval=self.interval.total_seconds() if self.interval else None
)
msg = f"CPU {usage_percent:.1f}\u202f%"
if self.max_usage_percent and usage_percent >= self.max_usage_percent:
raise ServiceWarning(msg)
except ValueError as e:
raise ServiceReturnedUnexpectedResult("ValueError") from e
@dataclasses.dataclass()
class Disk(HealthCheck):
"""
Warn about disk usage for a given system path.
It can be setup multiple times at different system paths,
e.g. one at your application root and one at your media storage root.
Args:
path: Path to check disk usage for.
max_disk_usage_percent: Maximum disk usage in percent or None to disable the check.
"""
path: pathlib.Path | str = dataclasses.field(default_factory=os.getcwd)
max_disk_usage_percent: float | None = dataclasses.field(default=90.0, repr=False)
hostname: str = dataclasses.field(default_factory=socket.gethostname, init=False)
def run(self):
try:
du = psutil.disk_usage(str(self.path))
if (
self.max_disk_usage_percent
and du.percent >= self.max_disk_usage_percent
):
raise ServiceWarning(f"{du.percent}\u202f% disk usage")
except ValueError as e:
raise ServiceReturnedUnexpectedResult("ValueError") from e
@dataclasses.dataclass()
class Memory(HealthCheck):
"""
Warn about system memory utilization.
Args:
min_gibibytes_available: Minimum available memory in gibibytes or None to disable the check.
max_memory_usage_percent: Maximum memory usage in percent or None to disable the check.
"""
min_gibibytes_available: float | None = dataclasses.field(default=None, repr=False)
max_memory_usage_percent: float | None = dataclasses.field(default=90.0, repr=False)
hostname: str = dataclasses.field(default_factory=socket.gethostname, init=False)
def run(self):
try:
memory = psutil.virtual_memory()
available_gibi = memory.available / (1024**3)
total_gibi = memory.total / (1024**3)
msg = f"RAM {available_gibi:.1f}/{total_gibi:.1f}GiB ({memory.percent}\u202f%)"
if (
self.min_gibibytes_available
and available_gibi < self.min_gibibytes_available
):
raise ServiceWarning(msg)
if (
self.max_memory_usage_percent
and memory.percent >= self.max_memory_usage_percent
):
raise ServiceWarning(msg)
except ValueError as e:
raise ServiceReturnedUnexpectedResult("ValueError") from e
@dataclasses.dataclass
class Temperature(HealthCheck):
"""
Warn about system temperature.
If no maximum temperature is specified, the sensor's high threshold will be used.
Args:
device: The device to check temperature for, e.g. 'coretemp' for CPU temperature on many systems. If None, check all available sensors.
max_temperature_celsius: Maximum temperature in degree Celsius or None to disable the check.
"""
device: str | None = dataclasses.field(default="coretemp")
max_temperature_celsius: float | None = dataclasses.field(default=None, repr=False)
hostname: str = dataclasses.field(default_factory=socket.gethostname, init=False)
def run(self):
try:
temperatures = psutil.sensors_temperatures()
except AttributeError as e:
raise ServiceUnavailable("Temperature information not available") from e
else:
if self.device:
try:
sensors = temperatures[self.device]
except KeyError:
raise ServiceUnavailable(
f"Sensor {self.device!r} not found"
) from None
else:
for sensor in sensors:
if sensor.current >= (
self.max_temperature_celsius or sensor.high
):
raise ServiceWarning(
f"{sensor.label} {sensor.current:.1f}\u202fΒ°C"
)
else:
for device, sensors in temperatures.items():
for sensor in sensors:
if sensor.current >= (
self.max_temperature_celsius or sensor.high
):
raise ServiceWarning(
f"{device} {sensor.label} {sensor.current:.1f}\u202fΒ°C"
)
django-health-check-4.5.1/health_check/contrib/rabbitmq.py 0000664 0000000 0000000 00000002471 15242136254 0023526 0 ustar 00root root 0000000 0000000 """RabbitMQ health check."""
import dataclasses
import logging
import aio_pika
from health_check.base import HealthCheck
from health_check.exceptions import ServiceUnavailable
logger = logging.getLogger(__name__)
@dataclasses.dataclass
class RabbitMQ(HealthCheck):
"""
Check RabbitMQ service by opening and closing a broker channel.
Args:
amqp_url (str): The URL of the RabbitMQ broker to connect to, e.g., 'amqp://guest:guest@localhost:5672//'.
"""
amqp_url: str
async def run(self):
logger.debug("Attempting to connect to %r...", self.amqp_url)
try:
# conn is used as a context to release opened resources later
connection = await aio_pika.connect_robust(self.amqp_url)
await connection.close()
except ConnectionRefusedError as e:
raise ServiceUnavailable(
"Unable to connect to RabbitMQ: Connection was refused."
) from e
except aio_pika.exceptions.ProbableAuthenticationError as e:
raise ServiceUnavailable(
"Unable to connect to RabbitMQ: Authentication error."
) from e
except OSError as e:
raise ServiceUnavailable("IOError") from e
else:
logger.debug("Connection established. RabbitMQ is healthy.")
django-health-check-4.5.1/health_check/contrib/redis.py 0000664 0000000 0000000 00000011645 15242136254 0023036 0 ustar 00root root 0000000 0000000 """Redis health check."""
import dataclasses
import logging
import typing
import warnings
from redis import exceptions
from redis.asyncio import Redis as RedisClient
from redis.asyncio import RedisCluster
from health_check.base import HealthCheck
from health_check.exceptions import ServiceUnavailable
logger = logging.getLogger(__name__)
@dataclasses.dataclass
class Redis(HealthCheck):
"""
Check Redis service by pinging a Redis client.
This check works with any Redis client that implements the ping() method,
including standard Redis, Sentinel, and Cluster clients.
Args:
client_factory: A callable that returns an instance of a Redis client.
client: Deprecated, use `client_factory` instead.
Examples:
Using a standard Redis client:
>>> from redis.asyncio import Redis as RedisClient
>>> Redis(client_factory=lambda: RedisClient(host='localhost', port=6379))
Using from_url to create a client:
>>> from redis.asyncio import Redis as RedisClient
>>> Redis(client_factory=lambda: RedisClient.from_url('redis://localhost:6379'))
Using a Cluster client:
>>> from redis.asyncio import RedisCluster
>>> Redis(client_factory=lambda: RedisCluster(host='localhost', port=7000))
Using a Sentinel client:
>>> from redis.asyncio import Sentinel
>>> Redis(client_factory=lambda: Sentinel([('localhost', 26379)]).master_for('mymaster'))
"""
client: RedisClient | RedisCluster | None = dataclasses.field(
repr=False, default=None
)
client_factory: typing.Callable[[], RedisClient | RedisCluster] | None = (
dataclasses.field(repr=False, default=None)
)
def _connection_kwargs(self) -> dict[str, object]:
"""Return identifying Redis connection parameters for repr and labels."""
client = (
self.client_factory() if self.client_factory is not None else self.client
)
try:
return {
key: value
for key, value in sorted(
client.connection_pool.connection_kwargs.items()
)
if key in {"host", "port", "db"}
}
except AttributeError:
pass
try:
return {"hosts": [node.name for node in client.startup_nodes]}
except AttributeError:
return {}
def __repr__(self):
match self._connection_kwargs():
case {"hosts": hosts}:
return f"Redis(client=RedisCluster(hosts={hosts!r}))"
case kwargs if kwargs:
return f"Redis({', '.join(f'{key}={value!r}' for key, value in kwargs.items())})"
case _:
return super().__repr__()
@property
def labels(self) -> dict[str, str]:
return super().labels | {
key: str(value) for key, value in self._connection_kwargs().items()
}
def __post_init__(self):
# Validate that exactly one of client or client_factory is provided
if self.client is not None and self.client_factory is not None:
raise ValueError(
"Provide exactly one of `client` or `client_factory`, not both."
)
if self.client is None and self.client_factory is None:
raise ValueError(
"You must provide either `client` (deprecated) or `client_factory` "
"when instantiating `Redis`."
)
# Emit deprecation warning if using the old client parameter
if self.client is not None:
warnings.warn(
"The `client` argument is deprecated and will be removed in a future version. "
"Please use `client_factory` instead.",
DeprecationWarning,
stacklevel=2,
)
async def run(self):
# Create a new client for this health check request
if self.client_factory is not None:
client = self.client_factory()
should_close = True
else:
# Use the deprecated client parameter (user manages lifecycle)
client = self.client
should_close = False
logger.debug("Pinging Redis client...")
try:
await client.ping()
except ConnectionRefusedError as e:
raise ServiceUnavailable(
"Unable to connect to Redis: Connection was refused."
) from e
except exceptions.TimeoutError as e:
raise ServiceUnavailable("Unable to connect to Redis: Timeout.") from e
except exceptions.ConnectionError as e:
raise ServiceUnavailable(
"Unable to connect to Redis: Connection Error"
) from e
else:
logger.debug("Connection established. Redis is healthy.")
finally:
# Only close clients created by client_factory
if should_close:
await client.aclose()
django-health-check-4.5.1/health_check/contrib/rss.py 0000664 0000000 0000000 00000020226 15242136254 0022532 0 ustar 00root root 0000000 0000000 """RSS feed health checks for cloud provider status pages."""
import dataclasses
import datetime
import logging
import feedparser
import httpx
from health_check import HealthCheck, __version__
from health_check.exceptions import ServiceUnavailable, StatusPageWarning
logger = logging.getLogger(__name__)
class Feed(HealthCheck):
"""
Base class for cloud provider status feed health checks.
Monitor cloud provider service health via their public RSS or Atom status feeds.
Subclasses must provide:
Attributes:
feed_url: The full URL of the RSS or Atom feed to monitor.
timeout: Maximum duration to wait for the HTTP request before failing.
max_age: Maximum age for an incident entry to be considered active.
The `timeout` and `max_age` values are used to control how long the
health check waits for the feed and how far back in time incidents are
considered relevant. The `feed_url` is used to fetch the status feed.
Examples:
>>> import dataclasses
>>> import datetime
>>> import typing
>>> from health_check.contrib.rss import Feed
>>> @dataclasses.dataclass
... class MyProviderStatus(Feed):
... \"\"\"Check MyProvider status via its public RSS feed.\"\"\"
... timeout: datetime.timedelta = dataclasses.field(
... default=datetime.timedelta(seconds=10),
... repr=False,
... )
... max_age: datetime.timedelta = dataclasses.field(
... default=datetime.timedelta(hours=4),
... repr=False,
... )
... feed_url: typing.ClassVar[str] = "https://status.myprovider.com/feed"
"""
feed_url: str = NotImplemented
timeout: datetime.timedelta = NotImplemented
max_age: datetime.timedelta = NotImplemented
async def run(self):
logger.debug("Fetching feed from %s", self.feed_url)
async with httpx.AsyncClient() as client:
try:
response = await client.get(
self.feed_url,
headers={"User-Agent": f"django-health-check@{__version__}"},
timeout=self.timeout.total_seconds(),
follow_redirects=True,
)
except httpx.TimeoutException as e:
raise ServiceUnavailable("Feed request timed out") from e
except httpx.RequestError as e:
raise ServiceUnavailable(f"Failed to fetch feed: {e}") from e
try:
response.raise_for_status()
except httpx.HTTPStatusError as e:
raise ServiceUnavailable(
f"HTTP error {e.response.status_code} fetching feed from {self.feed_url!r}"
) from e
content = response.text
feed = feedparser.parse(content)
if feed.bozo:
# feedparser sets bozo=1 for malformed feeds
logger.warning("Feed parsing encountered errors: %s", feed.bozo_exception)
if not feed.entries:
logger.debug("No entries found in feed")
return
if incidents := list(self._recent_incidents(feed.entries)):
raise StatusPageWarning(
"\n".join(
f"{getattr(entry, 'title', 'Unknown Incident') or 'Unknown Incident'}:"
f" {getattr(entry, 'link', self.feed_url) or self.feed_url}"
for entry, _ in incidents
),
timestamp=max(
filter(None, (date for _, date in incidents)), default=None
),
)
logger.debug("No recent incidents found in feed")
def _recent_incidents(self, entries):
"""Yield recent (entry, timestamp) pairs from feed entries."""
for entry in entries:
date = self._extract_date(entry)
if date is None or self._is_date_recent(date):
yield entry, date
def _is_date_recent(self, date):
"""Check if a timestamp falls within the configured max_age window."""
now = datetime.datetime.now(tz=datetime.timezone.utc)
return now >= date > now - self.max_age
def _extract_date(self, entry):
# feedparser normalizes both RSS and Atom dates to struct_time
# Try published first, then updated
for date_field in ["published_parsed", "updated_parsed"]:
if date_tuple := getattr(entry, date_field, None):
try:
# Convert struct_time to datetime
return datetime.datetime(
*date_tuple[:6], tzinfo=datetime.timezone.utc
)
except (ValueError, TypeError):
logger.warning(
"Failed to parse date from entry %r for %r",
date_tuple,
self.feed_url,
exc_info=True,
)
return None
@dataclasses.dataclass
class AWS(Feed):
"""
Check AWS service status via their public RSS status feeds.
Args:
region: AWS region code (e.g., 'us-east-1', 'eu-west-1').
service: AWS service name (e.g., 'ec2', 's3', 'rds').
timeout: Request timeout duration.
max_age: Maximum age for an incident to be considered active.
"""
region: str
service: str
timeout: datetime.timedelta = dataclasses.field(
default=datetime.timedelta(seconds=10), repr=False
)
max_age: datetime.timedelta = dataclasses.field(
default=datetime.timedelta(hours=8), repr=False
)
def __post_init__(self):
self.feed_url: str = (
f"https://status.aws.amazon.com/rss/{self.service}-{self.region}.rss"
)
@dataclasses.dataclass
class Heroku(Feed):
"""
Check Heroku platform status via their public RSS status feed.
Args:
timeout: Request timeout duration.
max_age: Maximum age for an incident to be considered active.
"""
timeout: datetime.timedelta = dataclasses.field(
default=datetime.timedelta(seconds=10), repr=False
)
max_age: datetime.timedelta = dataclasses.field(
default=datetime.timedelta(hours=8), repr=False
)
feed_url: str = dataclasses.field(
default="https://status.heroku.com/feed", init=False, repr=False
)
@dataclasses.dataclass
class Hetzner(Feed):
"""
Check Hetzner platform status via their public ATOM status feed.
Args:
timeout: Request timeout duration.
max_age: Maximum age for an incident to be considered active.
"""
timeout: datetime.timedelta = dataclasses.field(
default=datetime.timedelta(seconds=10), repr=False
)
max_age: datetime.timedelta = dataclasses.field(
default=datetime.timedelta(hours=8), repr=False
)
feed_url: str = dataclasses.field(
default="https://status.hetzner.com/en.atom", init=False, repr=False
)
@dataclasses.dataclass
class Azure(Feed):
"""
Check Azure platform status via their public RSS status feed.
Args:
timeout: Request timeout duration.
max_age: Maximum age for an incident to be considered active.
"""
timeout: datetime.timedelta = dataclasses.field(
default=datetime.timedelta(seconds=10), repr=False
)
max_age: datetime.timedelta = dataclasses.field(
default=datetime.timedelta(hours=8), repr=False
)
feed_url: str = dataclasses.field(
default="https://rssfeed.azure.status.microsoft/en-us/status/feed/",
init=False,
repr=False,
)
@dataclasses.dataclass
class GoogleCloud(Feed):
"""
Check Google Cloud platform status via their public Atom status feed.
Args:
timeout: Request timeout duration.
max_age: Maximum age for an incident to be considered active.
"""
timeout: datetime.timedelta = dataclasses.field(
default=datetime.timedelta(seconds=10), repr=False
)
max_age: datetime.timedelta = dataclasses.field(
default=datetime.timedelta(hours=8), repr=False
)
feed_url: str = dataclasses.field(
default="https://status.cloud.google.com/en/feed.atom", init=False, repr=False
)
django-health-check-4.5.1/health_check/exceptions.py 0000664 0000000 0000000 00000001423 15242136254 0022442 0 ustar 00root root 0000000 0000000 from django.utils import timezone
class HealthCheckException(Exception):
message_type: str = "Unknown Error"
def __init__(self, message, *, timestamp=None):
self.message = message
self.timestamp = timezone.now() if timestamp is None else timestamp
def __str__(self):
return f"{self.message_type}: {self.message}"
class ServiceWarning(HealthCheckException):
"""Warning of service misbehavior."""
message_type = "Warning"
class ServiceUnavailable(HealthCheckException):
message_type = "Unavailable"
class ServiceReturnedUnexpectedResult(HealthCheckException):
message_type = "Unexpected Result"
class StatusPageWarning(ServiceWarning):
"""Warning from an external status page, carrying the source incident timestamp."""
django-health-check-4.5.1/health_check/management/ 0000775 0000000 0000000 00000000000 15242136254 0022023 5 ustar 00root root 0000000 0000000 django-health-check-4.5.1/health_check/management/__init__.py 0000664 0000000 0000000 00000000000 15242136254 0024122 0 ustar 00root root 0000000 0000000 django-health-check-4.5.1/health_check/management/commands/ 0000775 0000000 0000000 00000000000 15242136254 0023624 5 ustar 00root root 0000000 0000000 django-health-check-4.5.1/health_check/management/commands/__init__.py 0000664 0000000 0000000 00000000000 15242136254 0025723 0 ustar 00root root 0000000 0000000 django-health-check-4.5.1/health_check/management/commands/health_check.py 0000664 0000000 0000000 00000013240 15242136254 0026600 0 ustar 00root root 0000000 0000000 import asyncio
import os
import sys
import urllib.error
import urllib.request
from django.conf import settings
from django.core.management.base import BaseCommand
from django.urls import NoReverseMatch, resolve, reverse
class Command(BaseCommand):
help = "Run health checks and exit 0 if everything went well."
@property
def default_forwarded_host(self):
return (
settings.ALLOWED_HOSTS[0].strip(".")
if settings.ALLOWED_HOSTS and settings.ALLOWED_HOSTS[0] != "*"
else None
)
@property
def default_addrport(self):
return ":".join([os.getenv("HOST", "127.0.0.1"), os.getenv("PORT", "8000")])
def add_arguments(self, parser):
parser.add_argument(
"endpoint",
type=str,
help="URL-pattern name of health check endpoint to test",
)
parser.add_argument(
"addrport",
nargs="?",
type=str,
default=self.default_addrport,
help=f"Optional port number, or ipaddr:port (default: {self.default_addrport})",
)
parser.add_argument(
"--forwarded-host",
type=str,
default=self.default_forwarded_host,
help=f"Value for X-Forwarded-Host header (default: {self.default_forwarded_host})",
)
parser.add_argument(
"--forwarded-proto",
type=str,
choices=["http", "https"],
default="https",
help="Value for X-Forwarded-Proto header (default: https)",
)
parser.add_argument(
"--timeout",
type=int,
default=5,
help="Timeout in seconds for the health check request (default: 5 seconds)",
)
http_group = parser.add_mutually_exclusive_group()
http_group.add_argument(
"--http",
action="store_true",
dest="use_http",
default=True,
help="Run checks via HTTP server (default)",
)
http_group.add_argument(
"--no-http",
action="store_false",
dest="use_http",
help="Run checks directly without HTTP server",
)
def handle(self, *args, **options):
endpoint = options.get("endpoint")
try:
path = reverse(endpoint)
except NoReverseMatch as e:
self.stderr.write(
f"Could not resolve endpoint {endpoint!r}: {e}\n"
"Please provide a valid URL pattern name for the health check endpoint."
)
sys.exit(2)
if not options.get("use_http"):
asyncio.run(self._run_checks_directly(path))
addrport = options.get("addrport")
# Use HTTPS only when SSL redirect is enabled without forwarded headers (direct HTTPS required).
# Otherwise, use HTTP (typical for containers with X-Forwarded-Proto header support).
proto = (
"https"
if settings.SECURE_SSL_REDIRECT and not settings.USE_X_FORWARDED_HOST
else "http"
)
url = f"{proto}://{addrport}{path}"
headers = {"Accept": "text/plain"}
# Add X-Forwarded-Host header
if forwarded_host := options.get("forwarded_host"):
headers["X-Forwarded-Host"] = forwarded_host
# Add X-Forwarded-Proto header
if forwarded_proto := options.get("forwarded_proto"):
headers["X-Forwarded-Proto"] = forwarded_proto
if options.get("verbosity", 1) >= 2:
self.stdout.write(
f"Checking health endpoint at {url!r} with headers: {headers}"
)
request = urllib.request.Request(url, headers=headers) # noqa: S310
try:
response = urllib.request.urlopen(request, timeout=options["timeout"]) # noqa: S310
except urllib.error.HTTPError as e:
match e.code:
case 500: # Health check failed
self.stdout.write(e.read().decode("utf-8"))
sys.exit(1)
case 400:
self.stderr.write(
f"{url!r} is not reachable: {e.reason}\nPlease check your ALLOWED_HOSTS setting or use the --forwarded-host option."
)
sys.exit(2)
case _:
self.stderr.write(
"Unexpected HTTP error "
f"when trying to reach {url!r}: {e}\n"
f"You may have selected an invalid endpoint {endpoint!r}"
f" or another application is running on {addrport!r}."
)
sys.exit(2)
except urllib.error.URLError as e:
self.stderr.write(
f"{url!r} is not reachable: {e.reason}\nPlease check your server is running and reachable."
)
sys.exit(2)
except TimeoutError as e:
self.stderr.write(f"Timeout when trying to reach {url!r}: {e}")
sys.exit(2)
else:
self.stdout.write(response.read().decode("utf-8"))
async def _run_checks_directly(self, path):
"""Run health checks directly without HTTP server."""
resolver_match = resolve(path)
view = resolver_match.func.view_class(**resolver_match.func.view_initkwargs)
results = await asyncio.gather(
*(check.get_result() for check in view.get_checks())
)
error = False
for result in results:
self.stdout.write(
f"{result.check!r}: {'OK' if not result.error else result.error!s}"
)
error |= bool(result.error)
sys.exit(int(error))
django-health-check-4.5.1/health_check/templates/ 0000775 0000000 0000000 00000000000 15242136254 0021705 5 ustar 00root root 0000000 0000000 django-health-check-4.5.1/health_check/templates/health_check/ 0000775 0000000 0000000 00000000000 15242136254 0024307 5 ustar 00root root 0000000 0000000 django-health-check-4.5.1/health_check/templates/health_check/index.html 0000664 0000000 0000000 00000010675 15242136254 0026315 0 ustar 00root root 0000000 0000000
{% block title %}
System status
{% endblock title %}
{% block extra_head %}
{% endblock extra_head %}
{% block content %}
{% if errors %}
π₯
{% endif %}
System status
{% if errors %}
π₯
{% endif %}