Skip to content

Commit

Permalink
feat: health check
Browse files Browse the repository at this point in the history
  • Loading branch information
SKairinos committed Nov 15, 2024
1 parent 8ccf3d8 commit 48b2399
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 1 deletion.
4 changes: 4 additions & 0 deletions codeforlife/settings/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,7 @@

# The name of the session metadata cookie.
SESSION_METADATA_COOKIE_NAME = "session_metadata"

# App deployment details.
APP_ID = os.getenv("APP_ID", "REPLACE_ME")
APP_VERSION = os.getenv("APP_VERSION", "REPLACE_ME")
7 changes: 6 additions & 1 deletion codeforlife/urls/patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from rest_framework import status

from ..settings import SERVICE_IS_ROOT, SERVICE_NAME
from ..views import CsrfCookieView, LogoutView
from ..views import CsrfCookieView, HealthCheckView, LogoutView

UrlPatterns = t.List[t.Union[URLResolver, URLPattern]]

Expand All @@ -36,6 +36,11 @@ def get_urlpatterns(
admin.site.urls,
name="admin",
),
path(
"health-check/",
HealthCheckView.as_view(),
name="health-check",
),
path(
"api/csrf/cookie/",
CsrfCookieView.as_view(),
Expand Down
1 change: 1 addition & 0 deletions codeforlife/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
from .base_login import BaseLoginView
from .common import CsrfCookieView, LogoutView
from .decorators import action, cron_job
from .health_check import HealthCheckView
from .model import BaseModelViewSet, ModelViewSet
126 changes: 126 additions & 0 deletions codeforlife/views/health_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
"""
© Ocado Group
Created on 14/11/2024 at 16:31:56(+00:00).
"""

import typing as t
from dataclasses import dataclass
from datetime import datetime

from django.apps import apps
from django.conf import settings
from django.contrib.sites.models import Site
from django.views.decorators.cache import cache_page
from rest_framework import status
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView

from ..permissions import AllowAny

HealthStatus = t.Literal[
"healthy",
"startingUp",
"shuttingDown",
"unhealthy",
"unknown",
]


@dataclass(frozen=True)
class HealthCheck:
"""The health of the current service."""

@dataclass(frozen=True)
class Detail:
"""A health detail."""

name: str
description: str
health: HealthStatus

health_status: HealthStatus
additional_info: str
details: t.Optional[t.List[Detail]] = None


class HealthCheckView(APIView):
"""A view for load balancers to determine whether the app is healthy."""

http_method_names = ["get"]
permission_classes = [AllowAny]
startup_timestamp = datetime.now().isoformat()
cache_timeout: float = 30

def get_health_check(self, request: Request) -> HealthCheck:
"""Check the health of the current service."""
try:
if not apps.ready or not apps.apps_ready or not apps.models_ready:
return HealthCheck(
health_status="startingUp",
additional_info="Apps not ready.",
)

host = request.get_host()
if not Site.objects.filter(domain=host).exists():
return HealthCheck(
health_status="unhealthy",
additional_info=f'Site "{host}" does not exist.',
)

return HealthCheck(
health_status="healthy",
additional_info="All healthy.",
)
# pylint: disable-next=broad-exception-caught
except Exception as ex:
return HealthCheck(
health_status="unknown",
additional_info=str(ex),
)

def get(self, request: Request):
"""Return a health check for the current service."""
health_check = self.get_health_check(request)

return Response(
data={
"appId": settings.APP_ID,
"healthStatus": health_check.health_status,
"lastCheckedTimestamp": datetime.now().isoformat(),
"additionalInformation": health_check.additional_info,
"startupTimestamp": self.startup_timestamp,
"appVersion": settings.APP_VERSION,
"details": [
{
"name": detail.name,
"description": detail.description,
"health": detail.health,
}
for detail in (health_check.details or [])
],
},
status={
# The app is running normally.
"healthy": status.HTTP_200_OK,
# The app is performing app-specific initialisation which must
# complete before it will serve normal application requests
# (perhaps the app is warming a cache or something similar). You
# only need to use this status if your app will be in a start-up
# mode for a prolonged period of time.
"startingUp": status.HTTP_503_SERVICE_UNAVAILABLE,
# The app is shutting down. As with startingUp, you only need to
# use this status if your app takes a prolonged amount of time
# to shutdown, perhaps because it waits for a long-running
# process to complete before shutting down.
"shuttingDown": status.HTTP_503_SERVICE_UNAVAILABLE,
# The app is not running normally.
"unhealthy": status.HTTP_503_SERVICE_UNAVAILABLE,
# The app is not able to report its own state.
"unknown": status.HTTP_503_SERVICE_UNAVAILABLE,
}[health_check.health_status],
)

@classmethod
def as_view(cls, **initkwargs):
return cache_page(cls.cache_timeout)(super().as_view(**initkwargs))

0 comments on commit 48b2399

Please sign in to comment.