diff --git a/tests/unit/viahtml/views/status_test.py b/tests/unit/viahtml/views/status_test.py index d3aca549..34339561 100644 --- a/tests/unit/viahtml/views/status_test.py +++ b/tests/unit/viahtml/views/status_test.py @@ -1,3 +1,4 @@ +import logging from unittest.mock import create_autospec import pytest @@ -82,6 +83,84 @@ def test_it_sends_test_messages_to_sentry(self, context, view, capture_message): "Test message from Via HTML's status view" ) + def test_it_logs_checkmate_failure_details(self, context, view, checkmate, caplog): + exc = CheckmateException("Connection timed out") + checkmate.check_url.side_effect = exc + context.query_params = {"include-checkmate": [""]} + + with caplog.at_level(logging.ERROR, logger="viahtml.views.status"): + view(context) + + assert any( + "Checkmate status check failed" in record.message + and "Connection timed out" in record.message + for record in caplog.records + ), f"Expected checkmate failure log, got: {[r.message for r in caplog.records]}" + + def test_it_logs_checkmate_success(self, context, view, caplog): + context.query_params = {"include-checkmate": [""]} + + with caplog.at_level(logging.INFO, logger="viahtml.views.status"): + view(context) + + assert any( + "Checkmate status check succeeded" in record.message + for record in caplog.records + ), f"Expected checkmate success log, got: {[r.message for r in caplog.records]}" + + def test_it_creates_sentry_span_on_checkmate_failure( + self, context, view, checkmate, sentry_start_span, sentry_capture_exception + ): # pylint:disable=too-many-arguments + exc = CheckmateException("Connection timed out") + checkmate.check_url.side_effect = exc + context.query_params = {"include-checkmate": [""]} + + view(context) + + sentry_start_span.assert_called_once_with( + op="checkmate.status_check", + name="Check Checkmate health via check_url", + ) + span = sentry_start_span.return_value.__enter__.return_value + span.set_status.assert_called_once_with("internal_error") + span.set_data.assert_any_call("error.type", "CheckmateException") + span.set_data.assert_any_call("error.message", "Connection timed out") + sentry_capture_exception.assert_called_once_with(exc) + + def test_it_creates_sentry_span_on_checkmate_success( + self, context, view, sentry_start_span, sentry_capture_exception + ): + context.query_params = {"include-checkmate": [""]} + + view(context) + + sentry_start_span.assert_called_once_with( + op="checkmate.status_check", + name="Check Checkmate health via check_url", + ) + span = sentry_start_span.return_value.__enter__.return_value + span.set_status.assert_called_once_with("ok") + sentry_capture_exception.assert_not_called() + + def test_it_adds_sentry_breadcrumb_on_checkmate_failure( + self, context, view, checkmate, sentry_add_breadcrumb + ): + exc = CheckmateException("Connection timed out") + checkmate.check_url.side_effect = exc + context.query_params = {"include-checkmate": [""]} + + view(context) + + sentry_add_breadcrumb.assert_called_once_with( + category="checkmate", + message="Checkmate status check failed: Connection timed out", + level="error", + data={ + "exception_type": "CheckmateException", + "exception_message": "Connection timed out", + }, + ) + @pytest.fixture def checkmate(self): return create_autospec(CheckmateClient, instance=True, spec_set=True) @@ -95,6 +174,18 @@ def context(self, context): def view(self, checkmate): return StatusView(checkmate) + @pytest.fixture(autouse=True) + def sentry_start_span(self, patch): + return patch("viahtml.views.status.sentry_sdk.start_span") + + @pytest.fixture(autouse=True) + def sentry_capture_exception(self, patch): + return patch("viahtml.views.status.sentry_sdk.capture_exception") + + @pytest.fixture(autouse=True) + def sentry_add_breadcrumb(self, patch): + return patch("viahtml.views.status.sentry_sdk.add_breadcrumb") + @pytest.fixture(autouse=True) def capture_message(patch): diff --git a/viahtml/views/status.py b/viahtml/views/status.py index 4a3552e8..7722c543 100644 --- a/viahtml/views/status.py +++ b/viahtml/views/status.py @@ -1,8 +1,13 @@ +import logging +import traceback from http import HTTPStatus +import sentry_sdk from checkmatelib import CheckmateException from sentry_sdk import capture_message +LOG = logging.getLogger(__name__) + class StatusView: def __init__(self, checkmate): @@ -22,12 +27,7 @@ def __call__(self, context): http_status = HTTPStatus.OK if "include-checkmate" in context.query_params: - try: - self._checkmate.check_url("https://example.com/") - except CheckmateException: - body["down"] = ["checkmate"] - else: - body["okay"] = ["checkmate"] + self._check_checkmate(body) # If any of the components checked above were down then report the # status check as a whole as being down. @@ -44,3 +44,37 @@ def __call__(self, context): http_status=http_status, headers={"Cache-Control": "max-age=0, must-revalidate, no-cache, no-store"}, ) + + def _check_checkmate(self, body): + with sentry_sdk.start_span( + op="checkmate.status_check", + name="Check Checkmate health via check_url", + ) as span: + LOG.info("Checking checkmate status via check_url") + try: + self._checkmate.check_url("https://example.com/") + except CheckmateException as exc: + LOG.error( + "Checkmate status check failed: %s: %s\n%s", + type(exc).__name__, + exc, + traceback.format_exc(), + ) + span.set_status("internal_error") + span.set_data("error.type", type(exc).__name__) + span.set_data("error.message", str(exc)) + sentry_sdk.add_breadcrumb( + category="checkmate", + message=f"Checkmate status check failed: {exc}", + level="error", + data={ + "exception_type": type(exc).__name__, + "exception_message": str(exc), + }, + ) + sentry_sdk.capture_exception(exc) + body["down"] = ["checkmate"] + else: + LOG.info("Checkmate status check succeeded") + span.set_status("ok") + body["okay"] = ["checkmate"]