From 9c0b27da4607dffb489f035e98f5c8a74bf88077 Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Thu, 5 Dec 2024 21:11:53 +1300 Subject: [PATCH 1/2] Initial commit --- config/settings.py | 5 +++ config/urls.py | 1 + healthcheck_app/__init__.py | 1 + healthcheck_app/apps.py | 10 ++++++ healthcheck_app/checks.py | 23 +++++++++++++ healthcheck_app/migrations/__init__.py | 0 healthcheck_app/urls.py | 14 ++++++++ healthcheck_app/views.py | 47 ++++++++++++++++++++++++++ 8 files changed, 101 insertions(+) create mode 100644 healthcheck_app/__init__.py create mode 100644 healthcheck_app/apps.py create mode 100644 healthcheck_app/checks.py create mode 100644 healthcheck_app/migrations/__init__.py create mode 100644 healthcheck_app/urls.py create mode 100644 healthcheck_app/views.py diff --git a/config/settings.py b/config/settings.py index 546a0c5..330df93 100644 --- a/config/settings.py +++ b/config/settings.py @@ -41,6 +41,7 @@ DEBUGGING_APP_PATH=(str, "this-is-just-a-temporary-debugging-app-path"), LOGFIRE_ENVIRONMENT=(str, "dev"), BLUESKY_APP_PASSWORD=(str, ""), + HEALTHCHECK_PATH=(str, ""), ) environ.Env.read_env(Path(BASE_DIR / ".env")) @@ -76,6 +77,7 @@ "django.contrib.staticfiles", "django.contrib.sitemaps", "djpress.apps.DjpressConfig", + "healthcheck_app", "timezone_converter", "markdown_editor", "shell", @@ -326,3 +328,6 @@ LOGFIRE_ENVIRONMENT = env("LOGFIRE_ENVIRONMENT") logfire.configure(environment=LOGFIRE_ENVIRONMENT) logfire.instrument_django() + +# Healthcheck app +HEALTHCHECK_PATH = env("HEALTHCHECK_PATH") diff --git a/config/urls.py b/config/urls.py index aab5a0f..bdd23c3 100644 --- a/config/urls.py +++ b/config/urls.py @@ -33,6 +33,7 @@ urlpatterns += [ path(f"{settings.ADMIN_URL}/", admin.site.urls), + path("healthcheck/", view=include("healthcheck_app.urls")), path("contact/", view=include("contact_form.urls")), path("utils/timezones/", view=include("timezone_converter.urls")), path("utils/markdown-editor/", view=include("markdown_editor.urls")), diff --git a/healthcheck_app/__init__.py b/healthcheck_app/__init__.py new file mode 100644 index 0000000..69a4929 --- /dev/null +++ b/healthcheck_app/__init__.py @@ -0,0 +1 @@ +"""healthcheck_app package.""" diff --git a/healthcheck_app/apps.py b/healthcheck_app/apps.py new file mode 100644 index 0000000..e6e65ff --- /dev/null +++ b/healthcheck_app/apps.py @@ -0,0 +1,10 @@ +"""App configuration for healthcheck_app.""" + +from django.apps import AppConfig + + +class HealthcheckAppConfig(AppConfig): + """App configuration for the healthcheck app.""" + + default_auto_field = "django.db.models.BigAutoField" + name = "healthcheck_app" diff --git a/healthcheck_app/checks.py b/healthcheck_app/checks.py new file mode 100644 index 0000000..f68d8f4 --- /dev/null +++ b/healthcheck_app/checks.py @@ -0,0 +1,23 @@ +"""Health checks for the healthcheck_app.""" + +import logging + +from django.db import connections +from django.db.utils import OperationalError + +logger = logging.getLogger(__name__) + + +def check_database() -> tuple[str, str | None]: + """Perform the database connectivity check. + + Returns a tuple of the status ('healthy' or 'unhealthy') and an optional error + message. + """ + try: + connections["default"].cursor() # You could use a specific database alias if needed + except OperationalError as e: + logger.debug(f"Health check found an error with the database: {e!s}") + return "unhealthy", "Error connecting to the database." + else: + return "healthy", None diff --git a/healthcheck_app/migrations/__init__.py b/healthcheck_app/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/healthcheck_app/urls.py b/healthcheck_app/urls.py new file mode 100644 index 0000000..3030806 --- /dev/null +++ b/healthcheck_app/urls.py @@ -0,0 +1,14 @@ +"""URLs for the healthcheck_app.""" + +from django.conf import settings +from django.urls import path + +from .views import health_check + +app_name = "healthcheck_app" + +url_path = f"{settings.HEALTHCHECK_PATH}/" if settings.HEALTHCHECK_PATH else "" + +urlpatterns = [ + path(url_path, health_check, name="healthcheck"), +] diff --git a/healthcheck_app/views.py b/healthcheck_app/views.py new file mode 100644 index 0000000..7dd0801 --- /dev/null +++ b/healthcheck_app/views.py @@ -0,0 +1,47 @@ +"""Views for the healthcheck_app.""" + +import logging + +from django.http import HttpRequest, JsonResponse +from django.utils import timezone + +from healthcheck_app.checks import check_database + +logger = logging.getLogger(__name__) + +# Checks to perform, with their corresponding check functions +checks = { + "database": check_database, +} + + +def health_check(_: HttpRequest) -> JsonResponse: + """Main function that returns the healthcheck. + + Perform a health check on the application by checking various components. + Returns a JsonResponse with the health information of the components. + """ + # Initial health data structure + health_data = { + "status": "healthy", # Starts assuming everything is healthy + "timestamp": timezone.now().isoformat(), + "details": {}, + } + + # Iterate over the checks and perform them + for check_name, check_function in checks.items(): + status, error = check_function() + + # Update the component's status in the health data + health_data["details"][check_name] = {"status": status} + + # If there's an error, add it to the component's data + if error: + health_data["details"][check_name]["error"] = error + + # If any component is unhealthy, set overall status to 'unhealthy' + if status == "unhealthy": + health_data["status"] = "unhealthy" + + # Return the JsonResponse with the health data and status code + return JsonResponse(health_data) From d2ae01cd8bc6256f6e04f9d41c9532976ecbfc2c Mon Sep 17 00:00:00 2001 From: Stuart Maxwell Date: Thu, 5 Dec 2024 21:17:29 +1300 Subject: [PATCH 2/2] Exclude healthcheck URL from logfire --- config/settings.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/config/settings.py b/config/settings.py index 330df93..85086db 100644 --- a/config/settings.py +++ b/config/settings.py @@ -327,7 +327,10 @@ # Logfire LOGFIRE_ENVIRONMENT = env("LOGFIRE_ENVIRONMENT") logfire.configure(environment=LOGFIRE_ENVIRONMENT) -logfire.instrument_django() +logfire.instrument_django( + capture_headers=True, + excluded_urls="/healthcheck", +) # Healthcheck app HEALTHCHECK_PATH = env("HEALTHCHECK_PATH")