django-health-check 3.23.3__py3-none-any.whl → 4.0rc2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_health_check-3.23.3.dist-info → django_health_check-4.0rc2.dist-info}/METADATA +9 -5
- django_health_check-4.0rc2.dist-info/RECORD +20 -0
- health_check/__init__.py +5 -14
- health_check/_version.py +3 -3
- health_check/base.py +93 -0
- health_check/checks.py +329 -0
- health_check/contrib/celery.py +70 -0
- health_check/contrib/kafka.py +69 -0
- health_check/contrib/rabbitmq.py +43 -0
- health_check/contrib/redis.py +63 -0
- health_check/contrib/rss.py +113 -0
- health_check/exceptions.py +5 -13
- health_check/management/commands/health_check.py +20 -66
- health_check/templates/health_check/index.html +61 -43
- health_check/views.py +176 -75
- django_health_check-3.23.3.dist-info/RECORD +0 -63
- health_check/backends.py +0 -101
- health_check/cache/__init__.py +0 -0
- health_check/cache/apps.py +0 -14
- health_check/cache/backends.py +0 -50
- health_check/conf.py +0 -8
- health_check/contrib/celery/__init__.py +0 -3
- health_check/contrib/celery/apps.py +0 -31
- health_check/contrib/celery/backends.py +0 -46
- health_check/contrib/celery/tasks.py +0 -6
- health_check/contrib/celery_ping/__init__.py +0 -0
- health_check/contrib/celery_ping/apps.py +0 -19
- health_check/contrib/celery_ping/backends.py +0 -74
- health_check/contrib/db_heartbeat/__init__.py +0 -0
- health_check/contrib/db_heartbeat/apps.py +0 -19
- health_check/contrib/db_heartbeat/backends.py +0 -44
- health_check/contrib/mail/__init__.py +0 -0
- health_check/contrib/mail/apps.py +0 -19
- health_check/contrib/mail/backends.py +0 -61
- health_check/contrib/migrations/__init__.py +0 -0
- health_check/contrib/migrations/apps.py +0 -19
- health_check/contrib/migrations/backends.py +0 -31
- health_check/contrib/psutil/__init__.py +0 -0
- health_check/contrib/psutil/apps.py +0 -36
- health_check/contrib/psutil/backends.py +0 -63
- health_check/contrib/rabbitmq/__init__.py +0 -3
- health_check/contrib/rabbitmq/apps.py +0 -19
- health_check/contrib/rabbitmq/backends.py +0 -57
- health_check/contrib/redis/__init__.py +0 -3
- health_check/contrib/redis/apps.py +0 -19
- health_check/contrib/redis/backends.py +0 -75
- health_check/contrib/s3boto3_storage/__init__.py +0 -0
- health_check/contrib/s3boto3_storage/apps.py +0 -19
- health_check/contrib/s3boto3_storage/backends.py +0 -32
- health_check/contrib/s3boto_storage/__init__.py +0 -0
- health_check/contrib/s3boto_storage/apps.py +0 -20
- health_check/contrib/s3boto_storage/backends.py +0 -27
- health_check/db/__init__.py +0 -0
- health_check/db/apps.py +0 -20
- health_check/db/backends.py +0 -23
- health_check/db/migrations/0001_initial.py +0 -34
- health_check/db/migrations/0002_alter_testmodel_options.py +0 -32
- health_check/db/migrations/__init__.py +0 -0
- health_check/db/models.py +0 -9
- health_check/deprecation.py +0 -35
- health_check/mixins.py +0 -86
- health_check/plugins.py +0 -25
- health_check/storage/__init__.py +0 -0
- health_check/storage/apps.py +0 -12
- health_check/storage/backends.py +0 -73
- health_check/urls.py +0 -18
- {django_health_check-3.23.3.dist-info → django_health_check-4.0rc2.dist-info}/WHEEL +0 -0
- {django_health_check-3.23.3.dist-info → django_health_check-4.0rc2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""RabbitMQ health check."""
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
import aio_pika
|
|
7
|
+
|
|
8
|
+
from health_check.base import HealthCheck
|
|
9
|
+
from health_check.exceptions import ServiceUnavailable
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclasses.dataclass
|
|
15
|
+
class RabbitMQ(HealthCheck):
|
|
16
|
+
"""
|
|
17
|
+
Check RabbitMQ service by opening and closing a broker channel.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
amqp_url (str): The URL of the RabbitMQ broker to connect to, e.g., 'amqp://guest:guest@localhost:5672//'.
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
amqp_url: str
|
|
25
|
+
|
|
26
|
+
async def run(self):
|
|
27
|
+
logger.debug("Attempting to connect to %r...", self.amqp_url)
|
|
28
|
+
try:
|
|
29
|
+
# conn is used as a context to release opened resources later
|
|
30
|
+
connection = await aio_pika.connect_robust(self.amqp_url)
|
|
31
|
+
await connection.close()
|
|
32
|
+
except ConnectionRefusedError as e:
|
|
33
|
+
raise ServiceUnavailable(
|
|
34
|
+
"Unable to connect to RabbitMQ: Connection was refused."
|
|
35
|
+
) from e
|
|
36
|
+
except aio_pika.exceptions.ProbableAuthenticationError as e:
|
|
37
|
+
raise ServiceUnavailable(
|
|
38
|
+
"Unable to connect to RabbitMQ: Authentication error."
|
|
39
|
+
) from e
|
|
40
|
+
except OSError as e:
|
|
41
|
+
raise ServiceUnavailable("IOError") from e
|
|
42
|
+
else:
|
|
43
|
+
logger.debug("Connection established. RabbitMQ is healthy.")
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Redis health check."""
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from redis import exceptions
|
|
7
|
+
from redis.asyncio import Redis as RedisClient
|
|
8
|
+
from redis.asyncio import RedisCluster
|
|
9
|
+
|
|
10
|
+
from health_check.base import HealthCheck
|
|
11
|
+
from health_check.exceptions import ServiceUnavailable
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclasses.dataclass
|
|
17
|
+
class Redis(HealthCheck):
|
|
18
|
+
"""
|
|
19
|
+
Check Redis service by pinging a Redis client.
|
|
20
|
+
|
|
21
|
+
This check works with any Redis client that implements the ping() method,
|
|
22
|
+
including standard Redis, Sentinel, and Cluster clients.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
client: A Redis client instance (Redis, Sentinel master, or Cluster).
|
|
26
|
+
If provided, this takes precedence over redis_url.
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
Using a standard Redis client:
|
|
30
|
+
>>> from redis.asyncio import Redis as RedisClient
|
|
31
|
+
>>> Redis(client=RedisClient(host='localhost', port=6379))
|
|
32
|
+
|
|
33
|
+
Using a Cluster client:
|
|
34
|
+
>>> from redis.asyncio import RedisCluster
|
|
35
|
+
>>> Redis(client=RedisCluster(host='localhost', port=7000))
|
|
36
|
+
|
|
37
|
+
Using a Sentinel client:
|
|
38
|
+
>>> from redis.asyncio import Sentinel
|
|
39
|
+
>>> sentinel = Sentinel([('localhost', 26379)])
|
|
40
|
+
>>> Redis(client=sentinel.master_for('mymaster'))
|
|
41
|
+
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
client: RedisClient | RedisCluster = dataclasses.field(default=None, repr=False)
|
|
45
|
+
|
|
46
|
+
async def run(self):
|
|
47
|
+
logger.debug("Pinging Redis client...")
|
|
48
|
+
try:
|
|
49
|
+
await self.client.ping()
|
|
50
|
+
except ConnectionRefusedError as e:
|
|
51
|
+
raise ServiceUnavailable(
|
|
52
|
+
"Unable to connect to Redis: Connection was refused."
|
|
53
|
+
) from e
|
|
54
|
+
except exceptions.TimeoutError as e:
|
|
55
|
+
raise ServiceUnavailable("Unable to connect to Redis: Timeout.") from e
|
|
56
|
+
except exceptions.ConnectionError as e:
|
|
57
|
+
raise ServiceUnavailable(
|
|
58
|
+
"Unable to connect to Redis: Connection Error"
|
|
59
|
+
) from e
|
|
60
|
+
else:
|
|
61
|
+
logger.debug("Connection established. Redis is healthy.")
|
|
62
|
+
finally:
|
|
63
|
+
await self.client.aclose()
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""RSS feed health checks for cloud provider status pages."""
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import datetime
|
|
5
|
+
import email.utils
|
|
6
|
+
import logging
|
|
7
|
+
from xml.etree import ElementTree
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from health_check.base import HealthCheck
|
|
12
|
+
from health_check.exceptions import ServiceUnavailable, ServiceWarning
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclasses.dataclass
|
|
18
|
+
class AWS(HealthCheck):
|
|
19
|
+
"""
|
|
20
|
+
Check AWS service status via their public RSS status feeds.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
region: AWS region code (e.g., 'us-east-1', 'eu-west-1').
|
|
24
|
+
service: AWS service name (e.g., 'ec2', 's3', 'rds').
|
|
25
|
+
timeout: Request timeout duration.
|
|
26
|
+
max_age: Maximum age for an incident to be considered active.
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
region: str
|
|
31
|
+
service: str
|
|
32
|
+
timeout: datetime.timedelta = dataclasses.field(
|
|
33
|
+
default=datetime.timedelta(seconds=10), repr=False
|
|
34
|
+
)
|
|
35
|
+
max_age: datetime.timedelta = dataclasses.field(
|
|
36
|
+
default=datetime.timedelta(days=1), repr=False
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def __post_init__(self):
|
|
40
|
+
self.feed_url: str = (
|
|
41
|
+
f"https://status.aws.amazon.com/rss/{self.service}-{self.region}.rss"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
async def run(self):
|
|
45
|
+
"""Check the RSS feed for incidents."""
|
|
46
|
+
logger.debug("Fetching feed from %s", self.feed_url)
|
|
47
|
+
|
|
48
|
+
async with httpx.AsyncClient() as client:
|
|
49
|
+
try:
|
|
50
|
+
response = await client.get(
|
|
51
|
+
self.feed_url,
|
|
52
|
+
headers={"User-Agent": "django-health-check"},
|
|
53
|
+
timeout=self.timeout.total_seconds(),
|
|
54
|
+
follow_redirects=True,
|
|
55
|
+
)
|
|
56
|
+
except httpx.TimeoutException as e:
|
|
57
|
+
raise ServiceUnavailable("RSS feed request timed out") from e
|
|
58
|
+
except httpx.RequestError as e:
|
|
59
|
+
raise ServiceUnavailable(f"Failed to fetch RSS feed: {e}") from e
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
response.raise_for_status()
|
|
63
|
+
except httpx.HTTPStatusError as e:
|
|
64
|
+
raise ServiceUnavailable(
|
|
65
|
+
f"HTTP error {e.response.status_code} fetching RSS feed"
|
|
66
|
+
) from e
|
|
67
|
+
|
|
68
|
+
content = response.text
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
root = ElementTree.fromstring(content) # noqa: S314
|
|
72
|
+
except ElementTree.ParseError as e:
|
|
73
|
+
raise ServiceUnavailable("Failed to parse RSS feed") from e
|
|
74
|
+
|
|
75
|
+
entries = self._extract_entries(root)
|
|
76
|
+
incidents = [entry for entry in entries if self._is_recent_incident(entry)]
|
|
77
|
+
|
|
78
|
+
if incidents:
|
|
79
|
+
incident_titles = [self._extract_title(entry) for entry in incidents]
|
|
80
|
+
raise ServiceWarning(
|
|
81
|
+
f"Found {len(incidents)} recent incident(s): {', '.join(incident_titles)}"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
logger.debug("No recent incidents found in RSS feed")
|
|
85
|
+
|
|
86
|
+
def _extract_entries(self, root):
|
|
87
|
+
"""Extract entries from RSS 2.0 feed."""
|
|
88
|
+
return root.findall(".//item")
|
|
89
|
+
|
|
90
|
+
def _is_recent_incident(self, entry):
|
|
91
|
+
"""Check if entry is a recent incident."""
|
|
92
|
+
published_at = self._extract_date(entry)
|
|
93
|
+
if not published_at:
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
cutoff = datetime.datetime.now(tz=datetime.timezone.utc) - self.max_age
|
|
97
|
+
return published_at > cutoff
|
|
98
|
+
|
|
99
|
+
def _extract_date(self, entry):
|
|
100
|
+
"""Extract publication date from RSS entry."""
|
|
101
|
+
pub_date = entry.find("pubDate")
|
|
102
|
+
if pub_date is not None and (date_text := pub_date.text):
|
|
103
|
+
try:
|
|
104
|
+
return email.utils.parsedate_to_datetime(date_text)
|
|
105
|
+
except (ValueError, TypeError):
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
def _extract_title(self, entry):
|
|
109
|
+
"""Extract title from RSS entry."""
|
|
110
|
+
if (title := entry.find("title")) is not None:
|
|
111
|
+
return title.text or "Untitled incident"
|
|
112
|
+
|
|
113
|
+
return "Untitled incident"
|
health_check/exceptions.py
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
from django.utils.translation import gettext_lazy as _ # noqa: N812
|
|
2
|
-
|
|
3
|
-
|
|
4
1
|
class HealthCheckException(Exception):
|
|
5
|
-
message_type =
|
|
2
|
+
message_type: str = "Unknown Error"
|
|
6
3
|
|
|
7
4
|
def __init__(self, message):
|
|
8
5
|
self.message = message
|
|
@@ -12,19 +9,14 @@ class HealthCheckException(Exception):
|
|
|
12
9
|
|
|
13
10
|
|
|
14
11
|
class ServiceWarning(HealthCheckException):
|
|
15
|
-
"""
|
|
16
|
-
Warning of service misbehavior.
|
|
17
|
-
|
|
18
|
-
If the ``HEALTH_CHECK['WARNINGS_AS_ERRORS']`` is set to ``False``,
|
|
19
|
-
these exceptions will not case a 500 status response.
|
|
20
|
-
"""
|
|
12
|
+
"""Warning of service misbehavior."""
|
|
21
13
|
|
|
22
|
-
message_type =
|
|
14
|
+
message_type = "Warning"
|
|
23
15
|
|
|
24
16
|
|
|
25
17
|
class ServiceUnavailable(HealthCheckException):
|
|
26
|
-
message_type =
|
|
18
|
+
message_type = "Unavailable"
|
|
27
19
|
|
|
28
20
|
|
|
29
21
|
class ServiceReturnedUnexpectedResult(HealthCheckException):
|
|
30
|
-
message_type =
|
|
22
|
+
message_type = "Unexpected Result"
|
|
@@ -1,25 +1,19 @@
|
|
|
1
|
-
import json
|
|
2
1
|
import sys
|
|
3
2
|
import urllib.error
|
|
4
3
|
import urllib.request
|
|
5
|
-
import warnings
|
|
6
4
|
|
|
7
5
|
from django.core.management.base import BaseCommand
|
|
8
|
-
from django.http import Http404
|
|
9
6
|
from django.urls import reverse
|
|
10
7
|
|
|
11
|
-
from health_check.mixins import CheckMixin
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
class Command(CheckMixin, BaseCommand):
|
|
9
|
+
class Command(BaseCommand):
|
|
15
10
|
help = "Run health checks and exit 0 if everything went well."
|
|
16
11
|
|
|
17
12
|
def add_arguments(self, parser):
|
|
18
13
|
parser.add_argument(
|
|
19
14
|
"endpoint",
|
|
20
|
-
nargs="?",
|
|
21
15
|
type=str,
|
|
22
|
-
help="
|
|
16
|
+
help="URL-pattern name of health check endpoint to test",
|
|
23
17
|
)
|
|
24
18
|
parser.add_argument(
|
|
25
19
|
"addrport",
|
|
@@ -28,65 +22,25 @@ class Command(CheckMixin, BaseCommand):
|
|
|
28
22
|
help="Optional port number, or ipaddr:port (default: localhost:8000)",
|
|
29
23
|
default="localhost:8000",
|
|
30
24
|
)
|
|
31
|
-
parser.add_argument("-s", "--subset", type=str, nargs=1, help="deprecated")
|
|
32
25
|
|
|
33
26
|
def handle(self, *args, **options):
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
27
|
+
endpoint = options.get("endpoint")
|
|
28
|
+
path = reverse(endpoint)
|
|
29
|
+
host, sep, port = options.get("addrport").partition(":")
|
|
30
|
+
url = f"http://{host}:{port}{path}" if sep else f"http://{host}{path}"
|
|
31
|
+
request = urllib.request.Request( # noqa: S310
|
|
32
|
+
url, headers={"Accept": "text/plain"}
|
|
33
|
+
)
|
|
34
|
+
try:
|
|
35
|
+
response = urllib.request.urlopen(request) # noqa: S310
|
|
36
|
+
except urllib.error.HTTPError as e:
|
|
37
|
+
# 500 status codes will raise HTTPError
|
|
38
|
+
self.stdout.write(e.read().decode("utf-8"))
|
|
39
|
+
sys.exit(1)
|
|
40
|
+
except urllib.error.URLError as e:
|
|
41
|
+
self.stderr.write(
|
|
42
|
+
f'"{url}" is not reachable: {e.reason}\nPlease check your ALLOWED_HOSTS setting.'
|
|
40
43
|
)
|
|
41
|
-
|
|
42
|
-
response = urllib.request.urlopen(request) # noqa: S310
|
|
43
|
-
except urllib.error.HTTPError as e:
|
|
44
|
-
content = e.read()
|
|
45
|
-
except urllib.error.URLError as e:
|
|
46
|
-
self.stderr.write(f'"{url}" is not reachable: {e.reason}\nPlease check your ALLOWED_HOSTS setting.')
|
|
47
|
-
sys.exit(2)
|
|
48
|
-
else:
|
|
49
|
-
content = response.read()
|
|
50
|
-
|
|
51
|
-
try:
|
|
52
|
-
json_data = json.loads(content.decode("utf-8"))
|
|
53
|
-
except json.JSONDecodeError as e:
|
|
54
|
-
self.stderr.write(f"Health check endpoint '{endpoint}' did not return valid JSON: {e.msg}\n")
|
|
55
|
-
sys.exit(2)
|
|
56
|
-
else:
|
|
57
|
-
errors = False
|
|
58
|
-
for label, msg in json_data.items():
|
|
59
|
-
if msg == "OK":
|
|
60
|
-
style_func = self.style.SUCCESS
|
|
61
|
-
else:
|
|
62
|
-
style_func = self.style.ERROR
|
|
63
|
-
errors = True
|
|
64
|
-
self.stdout.write(f"{label:<50} {style_func(msg)}\n")
|
|
65
|
-
if errors:
|
|
66
|
-
sys.exit(1)
|
|
67
|
-
|
|
44
|
+
sys.exit(2)
|
|
68
45
|
else:
|
|
69
|
-
|
|
70
|
-
"Explicit endpoint argument will be required in the next major version: pass the endpoint name to the command. Action: call `django-admin health_check <endpoint>` or update scripts to pass the endpoint. See migration guide: https://codingjoe.dev/django-health-check/migrate-to-v4/ (docs/migrate-to-v4.md).",
|
|
71
|
-
UserWarning,
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
if subset := options.get("subset", []):
|
|
75
|
-
warnings.warn(
|
|
76
|
-
"`--subset` option is deprecated: use the endpoint argument instead. Action: call `django-admin health_check <endpoint>` or use `HealthCheckView` with subset-configured checks. See migration guide: https://codingjoe.dev/django-health-check/migrate-to-v4/ (docs/migrate-to-v4.md).",
|
|
77
|
-
DeprecationWarning,
|
|
78
|
-
)
|
|
79
|
-
# perform all checks
|
|
80
|
-
subset = subset[0] if subset else None
|
|
81
|
-
try:
|
|
82
|
-
errors = self.check(subset=subset)
|
|
83
|
-
except Http404 as e:
|
|
84
|
-
self.stdout.write(str(e))
|
|
85
|
-
sys.exit(1)
|
|
86
|
-
|
|
87
|
-
for label, plugin in self.filter_plugins(subset=subset).items():
|
|
88
|
-
style_func = self.style.SUCCESS if not plugin.errors else self.style.ERROR
|
|
89
|
-
self.stdout.write(f"{label:<24} ... {style_func(plugin.pretty_status())}\n")
|
|
90
|
-
|
|
91
|
-
if errors:
|
|
92
|
-
sys.exit(1)
|
|
46
|
+
self.stdout.write(response.read().decode("utf-8"))
|
|
@@ -8,6 +8,22 @@
|
|
|
8
8
|
</title>
|
|
9
9
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
10
10
|
<meta name="robots" content="noindex">
|
|
11
|
+
<link rel="alternate"
|
|
12
|
+
type="application/json"
|
|
13
|
+
title="JSON"
|
|
14
|
+
href="?format=json">
|
|
15
|
+
<link rel="alternate"
|
|
16
|
+
type="application/atom+xml"
|
|
17
|
+
title="Atom Feed"
|
|
18
|
+
href="?format=atom">
|
|
19
|
+
<link rel="alternate"
|
|
20
|
+
type="application/rss+xml"
|
|
21
|
+
title="RSS Feed"
|
|
22
|
+
href="?format=rss">
|
|
23
|
+
<link rel="alternate"
|
|
24
|
+
type="application/openmetrics-text"
|
|
25
|
+
title="OpenMetrics"
|
|
26
|
+
href="?format=openmetrics">
|
|
11
27
|
<style>
|
|
12
28
|
:root {
|
|
13
29
|
color-scheme: light dark;
|
|
@@ -78,44 +94,44 @@
|
|
|
78
94
|
|
|
79
95
|
@media screen and (max-width: 700px) {
|
|
80
96
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
97
|
+
table,
|
|
98
|
+
thead,
|
|
99
|
+
tbody,
|
|
100
|
+
th,
|
|
101
|
+
td,
|
|
102
|
+
tr {
|
|
103
|
+
display: block;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
thead tr {
|
|
107
|
+
position: absolute;
|
|
108
|
+
top: -9999px;
|
|
109
|
+
left: -9999px;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
tr {
|
|
113
|
+
margin-bottom: 1.5em;
|
|
114
|
+
box-shadow: 0 0 5px light-dark(#ccc, #000);
|
|
115
|
+
overflow-x: auto;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
td {
|
|
119
|
+
border: none;
|
|
120
|
+
position: relative;
|
|
121
|
+
padding: 1em 0.5em 1em 6em;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
td:before {
|
|
125
|
+
position: absolute;
|
|
126
|
+
left: 1.5em;
|
|
127
|
+
content: attr(data-label);
|
|
128
|
+
font-weight: bold;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.align-right {
|
|
132
|
+
text-align: unset;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
119
135
|
</style>
|
|
120
136
|
{% block extra_head %}
|
|
121
137
|
{% endblock extra_head %}
|
|
@@ -146,16 +162,16 @@
|
|
|
146
162
|
</tr>
|
|
147
163
|
</thead>
|
|
148
164
|
<tbody>
|
|
149
|
-
{% for
|
|
150
|
-
<tr {% if
|
|
165
|
+
{% for result in results %}
|
|
166
|
+
<tr {% if result.error %}class="error"{% endif %}>
|
|
151
167
|
<td data-label="Service">
|
|
152
|
-
{{
|
|
168
|
+
{{ result.check|stringformat:'r' }}
|
|
153
169
|
</td>
|
|
154
170
|
<td data-label="Status" class="mono">
|
|
155
|
-
{{
|
|
171
|
+
{{ result.error|default_if_none:"OK"|linebreaks }}
|
|
156
172
|
</td>
|
|
157
173
|
<td data-label="Timing" class="align-right mono">
|
|
158
|
-
{{
|
|
174
|
+
{{ result.time_taken|floatformat:3 }} s
|
|
159
175
|
</td>
|
|
160
176
|
</tr>
|
|
161
177
|
{% endfor %}
|
|
@@ -164,6 +180,8 @@
|
|
|
164
180
|
<footer>
|
|
165
181
|
<p class="footer-text">
|
|
166
182
|
Generated at {% now "SHORT_DATETIME_FORMAT" %} with <a href="https://codingjoe.dev/django-health-check/">Django Health Check</a>.
|
|
183
|
+
<br>
|
|
184
|
+
Subscribe: <a href="?format=atom">Atom</a> | <a href="?format=rss">RSS</a> | <a href="?format=openmetrics">OpenMetrics</a>
|
|
167
185
|
</p>
|
|
168
186
|
</footer>
|
|
169
187
|
{% endblock content %}
|