From aff09287514a13cd3f6ceaa84cfc84448d70dc7b Mon Sep 17 00:00:00 2001 From: Nico Virkki Date: Tue, 31 Mar 2026 15:05:57 +0300 Subject: [PATCH] feat: use django-helsinki-health-endpoints for readiness and healthz django-health-check is replaced by django-helsinki-health-endpoints. Refs: KEH-242 --- custom_health_checks/README.md | 64 --------------------- custom_health_checks/__init__.py | 0 custom_health_checks/apps.py | 16 ------ custom_health_checks/backends.py | 24 -------- custom_health_checks/tests/test_backends.py | 44 -------------- custom_health_checks/tests/test_healthz.py | 29 ---------- custom_health_checks/views.py | 33 ----------- palvelutarjotin/settings.py | 8 ++- palvelutarjotin/urls.py | 23 +------- requirements.in | 2 +- requirements.txt | 8 +-- 11 files changed, 11 insertions(+), 240 deletions(-) delete mode 100644 custom_health_checks/README.md delete mode 100644 custom_health_checks/__init__.py delete mode 100644 custom_health_checks/apps.py delete mode 100644 custom_health_checks/backends.py delete mode 100644 custom_health_checks/tests/test_backends.py delete mode 100644 custom_health_checks/tests/test_healthz.py delete mode 100644 custom_health_checks/views.py diff --git a/custom_health_checks/README.md b/custom_health_checks/README.md deleted file mode 100644 index 02ee2e61..00000000 --- a/custom_health_checks/README.md +++ /dev/null @@ -1,64 +0,0 @@ -# Custom health checks - -## Table of Contents - - - -- [Table of Contents](#table-of-contents) -- [Requirements](#requirements) -- [What it does](#what-it-does) -- [Installation](#installation) - - - - -## Requirements - -- [django-health-check](https://pypi.org/project/django-health-check/) - -## What it does - -In [backends.py](backends.py) there are custom health checks for backend, like database health check, that checks whether the connection to the database is OK. - -## Installation - -1. Install the requirements - - ```python - INSTALLED_APPS = [ - 'health_check', # requirement - "custom_health_checks", # this app - ] - ``` - -2. Register the custom health check to the `health_check` from [apps.py](apps.py) - - ```python - class CustomHealthChecksAppConfig(AppConfig): - name = 'custom_health_checks' - - def ready(self): - from .backends import DatabaseHealthCheck - plugin_dir.register(DatabaseHealthCheck) - ``` - -3. Map the `health_check.urls` in the project's `urls.py`. - - If you want to have the default `health_check` view, map it like this: - - ```python - urlpatterns = [ - # ... - path("healthz/", include('health_check.urls')) - ] - ``` - - If you want to have a custom, e.g. a JSON only view, map it like this: - - ```python - import views - urlpatterns = [ - # ... - path(r'healthz', views.HealthCheckCustomView.as_view(), name='healthz'), - ] - ``` diff --git a/custom_health_checks/__init__.py b/custom_health_checks/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/custom_health_checks/apps.py b/custom_health_checks/apps.py deleted file mode 100644 index 45109650..00000000 --- a/custom_health_checks/apps.py +++ /dev/null @@ -1,16 +0,0 @@ -import logging - -from django.apps import AppConfig -from health_check.plugins import plugin_dir - -logger = logging.getLogger(__name__) - - -class CustomHealthChecksAppConfig(AppConfig): - name = "custom_health_checks" - - def ready(self): - from .backends import DatabaseHealthCheck - - plugin_dir.register(DatabaseHealthCheck) - logger.info("Registered DatabaseHealthCheck to health_check plugins.") diff --git a/custom_health_checks/backends.py b/custom_health_checks/backends.py deleted file mode 100644 index fa112559..00000000 --- a/custom_health_checks/backends.py +++ /dev/null @@ -1,24 +0,0 @@ -from django.db import connection -from health_check.backends import BaseHealthCheckBackend -from health_check.exceptions import ServiceUnavailable - - -class DatabaseHealthCheck(BaseHealthCheckBackend): - """ - Custom health check for the database connection. - """ - - critical_service = True - - def check_status(self): - """ - Checks the database connection by executing a simple query. - """ - try: - with connection.cursor() as cursor: - cursor.execute("SELECT 1 FROM organisations_user LIMIT 1") - except Exception as e: - raise ServiceUnavailable(f"Database connection failed: {e}") - - def identifier(self): - return self.__class__.__name__ # Display name on the endpoint. diff --git a/custom_health_checks/tests/test_backends.py b/custom_health_checks/tests/test_backends.py deleted file mode 100644 index 1cdb7c62..00000000 --- a/custom_health_checks/tests/test_backends.py +++ /dev/null @@ -1,44 +0,0 @@ -from unittest.mock import patch - -import pytest -from django.db import OperationalError -from health_check.exceptions import ServiceUnavailable - -from custom_health_checks.backends import DatabaseHealthCheck - - -@patch("django.db.connection.cursor") -def test_database_check_status_success(mock_cursor): - """ - Test that check_status passes when the database connection is successful. - """ - mock_cursor.return_value.__enter__.return_value.execute.return_value = True - health_check = DatabaseHealthCheck() - health_check.check_status() # Should not raise an exception - - -@patch("django.db.connection.cursor") -def test_database_check_status_failure(mock_cursor): - """ - Test that check_status raises ServiceUnavailable when the database - connection fails. - """ - mock_cursor.return_value.__enter__.return_value.execute.side_effect = ( - OperationalError("Database error") - ) - health_check = DatabaseHealthCheck() - with pytest.raises(ServiceUnavailable) as exc_info: - health_check.check_status() - assert "Database connection failed:" in str(exc_info.value) - - -def test_database_check_status_with_real_database(db): # Use the 'db' fixture - """ - Test check_status with the actual database connection. - This assumes your test database is set up correctly. - """ - try: - health_check = DatabaseHealthCheck() - health_check.check_status() # Should not raise an exception - except ServiceUnavailable as e: - pytest.fail(f"Database health check failed: {e}") diff --git a/custom_health_checks/tests/test_healthz.py b/custom_health_checks/tests/test_healthz.py deleted file mode 100644 index b7f6c62b..00000000 --- a/custom_health_checks/tests/test_healthz.py +++ /dev/null @@ -1,29 +0,0 @@ -from unittest.mock import patch - -from django.test import Client -from django.urls import reverse -from health_check.exceptions import ServiceUnavailable - - -@patch("custom_health_checks.backends.DatabaseHealthCheck.check_status") -def test_healthz_success(mock_check_status, client: Client): # Use the 'client' fixture - """ - Test /healthz endpoint with successful health checks. - """ - mock_check_status.return_value = None # Simulate successful check - url = reverse("healthz") - response = client.get(url) - assert response.status_code == 200 - assert response.content == b'{"DatabaseHealthCheck": "working"}' - - -@patch("custom_health_checks.backends.DatabaseHealthCheck.check_status") -def test_healthz_database_error(mock_check_status, client: Client): - """ - Test /healthz endpoint with a database error. - """ - mock_check_status.side_effect = ServiceUnavailable("Database error") - url = reverse("healthz") - response = client.get(url) - assert response.status_code == 500 - assert b"Database error" in response.content diff --git a/custom_health_checks/views.py b/custom_health_checks/views.py deleted file mode 100644 index b8fb9a07..00000000 --- a/custom_health_checks/views.py +++ /dev/null @@ -1,33 +0,0 @@ -from django.utils.decorators import method_decorator -from django.views.decorators.cache import never_cache -from health_check.views import MainView - - -class HealthCheckJSONView(MainView): - """ - The original `health_check` view uses format-attribute or headers to determine - what response type is used. The HealthCheckJSONView can be used to ensure - that a JSON Response is always used (for health check reporting). - - To apply it, in project's `urls.py`, add the custom JSON view in use like this: - >>> # doctest: +SKIP - ... import views - ... - ... urlpatterns = [ - ... # ... - ... path( - ... r"healthz", - ... views.HealthCheckCustomView.as_view(), - ... name="health_check_custom", - ... ), - ... ] - """ - - @method_decorator(never_cache) - def get(self, request, *args, **kwargs): - subset = kwargs.get("subset", None) - health_check_has_error = self.check(subset) - status_code = 500 if health_check_has_error else 200 - return self.render_to_response_json( - self.filter_plugins(subset=subset), status_code - ) diff --git a/palvelutarjotin/settings.py b/palvelutarjotin/settings.py index 8ca46cb6..0e876a94 100644 --- a/palvelutarjotin/settings.py +++ b/palvelutarjotin/settings.py @@ -177,6 +177,9 @@ COMMIT_HASH = env("OPENSHIFT_BUILD_COMMIT") VERSION = __version__ +# Django helsinki health endpoints +SENTRY_RELEASE = env("SENTRY_RELEASE") + SENTRY_TRACES_SAMPLE_RATE = env("SENTRY_TRACES_SAMPLE_RATE") SENTRY_TRACES_IGNORE_PATHS = env.list("SENTRY_TRACES_IGNORE_PATHS") @@ -199,7 +202,7 @@ def sentry_traces_sampler(sampling_context: SamplingContext) -> float: sentry_sdk.init( dsn=env("SENTRY_DSN"), environment=env("SENTRY_ENVIRONMENT"), - release=env("SENTRY_RELEASE"), + release=SENTRY_RELEASE, integrations=[DjangoIntegration()], traces_sampler=sentry_traces_sampler, profile_session_sample_rate=env("SENTRY_PROFILE_SESSION_SAMPLE_RATE"), @@ -249,9 +252,8 @@ def sentry_traces_sampler(sampling_context: SamplingContext) -> float: "auditlog", "auditlog_extra", "resilient_logger", - "health_check", + "helsinki_health_endpoints", # local apps under this line - "custom_health_checks", "utils", "organisations", "occurrences", diff --git a/palvelutarjotin/urls.py b/palvelutarjotin/urls.py index 17d50ebb..12f2a1db 100644 --- a/palvelutarjotin/urls.py +++ b/palvelutarjotin/urls.py @@ -2,11 +2,9 @@ from csp.decorators import csp_update from django.conf import settings from django.conf.urls.static import static -from django.http import JsonResponse from django.urls import include, path, re_path from django.utils.translation import gettext from django.views.decorators.csrf import csrf_exempt -from django.views.decorators.http import require_http_methods from drf_spectacular.views import ( SpectacularAPIView, SpectacularRedocView, @@ -14,8 +12,6 @@ from helusers.admin_site import admin from common.utils import get_api_version -from custom_health_checks.views import HealthCheckJSONView -from palvelutarjotin import __version__ from palvelutarjotin.views import SentryGraphQLView admin.site.index_title = " ".join([gettext("Kultus API"), get_api_version()]) @@ -57,25 +53,8 @@ def graphql_view(request, *args, **kwargs): ] -# # Kubernetes liveness & readiness probes -# -@require_http_methods(["GET", "HEAD"]) -def readiness(*args, **kwargs): - response_json = { - "status": "ok", - "release": settings.APP_RELEASE, - "packageVersion": __version__, - "commitHash": settings.COMMIT_HASH, - "buildTime": settings.APP_BUILD_TIME.strftime("%Y-%m-%dT%H:%M:%S.000Z"), - } - return JsonResponse(response_json, status=200) - - -urlpatterns += [ - path(r"healthz", HealthCheckJSONView.as_view(), name="healthz"), - path("readiness", readiness), -] +urlpatterns += [path("", include("helsinki_health_endpoints.urls"))] if settings.DEBUG: diff --git a/requirements.in b/requirements.in index 2670a0dd..ba915c99 100644 --- a/requirements.in +++ b/requirements.in @@ -9,8 +9,8 @@ django-csp django-environ django-filter django-graphql-jwt -django-health-check django-helusers +django-helsinki-health-endpoints django-ilmoitin django-ipware django-logger-extra diff --git a/requirements.txt b/requirements.txt index 968e8f74..7351f2ae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -290,7 +290,7 @@ django==5.2.12 \ # django-csp # django-filter # django-graphql-jwt - # django-health-check + # django-helsinki-health-endpoints # django-helusers # django-ilmoitin # django-logger-extra @@ -344,9 +344,9 @@ django-graphql-jwt==0.4.0 \ --hash=sha256:537972519f0deeec7a0e4a306ddfed1fe385266ef61c9f78c54cd04ac01a171e \ --hash=sha256:5823aa8ac9bf0b7a6e3b2febd029598b332c41fe9043d89900c116fcecd23f5e # via -r requirements.in -django-health-check==3.20.0 \ - --hash=sha256:bcb2b8f36f463cead0564a028345c5b17e2a2d18e9cc88ecd611b13a26521926 \ - --hash=sha256:cd69ac5facf73fe7241d9492d939b57bd20e24f46c4edea91e6a900bf22c2d8e +django-helsinki-health-endpoints==1.0.0 \ + --hash=sha256:75670113e99bfd1fc23e317a3a22cff563140f495655c2a5f6ab219389dafd52 \ + --hash=sha256:eebf1baa6917d7b0e11123aec191df773883102ec1653025a86d7037dba389e2 # via -r requirements.in django-helusers==1.0.0 \ --hash=sha256:0e33e238347a4088927675574a7dad179ee1f9d9e958daecfa4862ad6f63f79c \