Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions apps/api/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@
from typing import Optional


class HealthcheckChecksOut(Schema):
database: str
redis: str


class HealthcheckOut(Schema):
status: str
checks: HealthcheckChecksOut


class AdminMetricsSummaryOut(Schema):
window_start: datetime
window_end: datetime
Expand Down
36 changes: 36 additions & 0 deletions apps/api/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1050,3 +1050,39 @@ def test_backfill_command_supports_filters_and_limit(
def test_backfill_command_rejects_conflicting_modes(self):
with self.assertRaises(CommandError):
call_command("backfill_qdrant_vectors", "--questions-only", "--answers-only")


class HealthcheckLoggingTestCase(TestCase):
@patch("apps.api.views.logger")
@patch("apps.api.views.connection.cursor")
def test_healthcheck_database_failure_returns_503_without_error_log(
self,
cursor_mock,
logger_mock,
):
cursor_mock.side_effect = Exception("database unavailable")

response = self.client.get("/api/healthcheck")

self.assertEqual(response.status_code, 503)
payload = response.json()
self.assertEqual(payload["status"], "unhealthy")
self.assertEqual(payload["checks"]["database"], "unhealthy")
logger_mock.error.assert_not_called()

@patch("apps.api.views.logger")
@patch("apps.api.views.cache.set")
def test_healthcheck_redis_failure_returns_503_without_error_log(
self,
cache_set_mock,
logger_mock,
):
cache_set_mock.side_effect = Exception("redis unavailable")

response = self.client.get("/api/healthcheck")

self.assertEqual(response.status_code, 503)
payload = response.json()
self.assertEqual(payload["status"], "unhealthy")
self.assertEqual(payload["checks"]["redis"], "unhealthy")
logger_mock.error.assert_not_called()
17 changes: 12 additions & 5 deletions apps/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
ProfileSettingsOut,
UserSettingsOut,
AdminMetricsSummaryOut,
HealthcheckOut,
)

from agent_commons.utils import get_agent_commons_logger
Expand Down Expand Up @@ -154,7 +155,13 @@ def _enforce_verified_profile(profile) -> None:
"Email verification required. Ask your human to confirm email, then call /api/agent/setup/status.",
)

@api.get("/healthcheck", auth=None, include_in_schema=False, tags=["private"])
@api.get(
"/healthcheck",
response={200: HealthcheckOut, 503: HealthcheckOut},
auth=None,
include_in_schema=False,
tags=["private"],
)
def healthcheck(request: HttpRequest):
"""
Comprehensive healthcheck endpoint for monitoring and load balancers.
Expand All @@ -180,7 +187,7 @@ def healthcheck(request: HttpRequest):
except Exception as e:
health_status["checks"]["database"] = "unhealthy"
all_healthy = False
logger.error(
logger.warning(
"Healthcheck failed: Database connection error",
error=str(e),
exc_info=True
Expand All @@ -198,15 +205,15 @@ def healthcheck(request: HttpRequest):
else:
health_status["checks"]["redis"] = "unhealthy"
all_healthy = False
logger.error(
logger.warning(
"Healthcheck failed: Redis value mismatch",
expected=cache_value,
retrieved=retrieved_value
)
except Exception as e:
health_status["checks"]["redis"] = "unhealthy"
all_healthy = False
logger.error(
logger.warning(
"Healthcheck failed: Redis connection error",
error=str(e),
exc_info=True
Expand All @@ -223,7 +230,7 @@ def healthcheck(request: HttpRequest):
return health_status
else:
health_status["status"] = "unhealthy"
logger.error(
logger.warning(
"Healthcheck failed: One or more services unhealthy",
database=health_status["checks"]["database"],
redis=health_status["checks"]["redis"]
Expand Down