diff --git a/elementary/monitor/alerts/source_freshness_alert.py b/elementary/monitor/alerts/source_freshness_alert.py index 692e79b59..a78bd61dd 100644 --- a/elementary/monitor/alerts/source_freshness_alert.py +++ b/elementary/monitor/alerts/source_freshness_alert.py @@ -163,9 +163,17 @@ def asset_type(self) -> str: def concise_name(self) -> str: return f"{self.source_name}.{self.identifier}" + @property + def is_runtime_error(self) -> bool: + """Whether the freshness check failed to run, in either engine's casing. + + dbt-core writes "runtime error", dbt Fusion writes it capitalized. + """ + return (self.original_status or "").lower() == "runtime error" + @property def error_message(self) -> str: - if self.original_status == "runtime error": + if self.is_runtime_error: return f"Failed to calculate the source freshness\n```{self.error}```" return self.result_description @@ -178,7 +186,7 @@ def summary(self) -> str: if part ] ) - if self.original_status == "runtime error": + if self.is_runtime_error: return f'Failed to calculate the source freshness of "{source_fqn}"' return f'Freshness exceeded the acceptable times on source "{source_fqn}"' diff --git a/elementary/monitor/dbt_project/macros/utils/normalized_source_freshness_status.sql b/elementary/monitor/dbt_project/macros/utils/normalized_source_freshness_status.sql index 20c3e7fb5..c6c0c38be 100644 --- a/elementary/monitor/dbt_project/macros/utils/normalized_source_freshness_status.sql +++ b/elementary/monitor/dbt_project/macros/utils/normalized_source_freshness_status.sql @@ -1,7 +1,12 @@ +{# + dbt-core writes source freshness statuses in lowercase ("pass", "warn", "error", + "runtime error"), while dbt Fusion writes them capitalized ("Pass", "Warn", "Error"). + Compare and return them lowercased, so rows from either engine get the same status. +#} {% macro normalized_source_freshness_status(status_column='status') %} case - when {{ status_column }} = 'error' then 'fail' - when {{ status_column }} = 'runtime error' then 'error' - else {{ status_column }} + when lower({{ status_column }}) = 'error' then 'fail' + when lower({{ status_column }}) = 'runtime error' then 'error' + else lower({{ status_column }}) end as normalized_status {% endmacro %} diff --git a/tests/unit/monitor/alerts/test_alert_models.py b/tests/unit/monitor/alerts/test_alert_models.py index d00bdd0a5..62f72487a 100644 --- a/tests/unit/monitor/alerts/test_alert_models.py +++ b/tests/unit/monitor/alerts/test_alert_models.py @@ -55,13 +55,15 @@ def _make_model_alert( def _make_source_freshness_alert( - source_name: str = "my_source", identifier: str = "my_table" + source_name: str = "my_source", + identifier: str = "my_table", + original_status: str = "fail", ) -> SourceFreshnessAlertModel: return SourceFreshnessAlertModel( id="id", source_name=source_name, identifier=identifier, - original_status="fail", + original_status=original_status, path="models/src.yml", error=None, alert_class_id="acid", @@ -114,6 +116,24 @@ def test_concise_name_is_source_dot_identifier(self) -> None: alert = _make_source_freshness_alert(source_name="raw", identifier="orders") assert alert.concise_name == "raw.orders" + @pytest.mark.parametrize("status", ["runtime error", "Runtime Error"]) + def test_runtime_error_status_is_case_insensitive(self, status: str) -> None: + """dbt-core writes "runtime error", dbt Fusion "Runtime Error": both are runtime errors.""" + alert = _make_source_freshness_alert(original_status=status) + assert alert.is_runtime_error + assert ( + alert.error_message + == "Failed to calculate the source freshness\n```None```" + ) + assert alert.summary.startswith("Failed to calculate the source freshness") + + @pytest.mark.parametrize("status", ["error", "Error", "warn", "Pass"]) + def test_other_statuses_are_not_runtime_errors(self, status: str) -> None: + """Other statuses, in either casing, get the freshness-exceeded summary.""" + alert = _make_source_freshness_alert(original_status=status) + assert not alert.is_runtime_error + assert alert.summary.startswith("Freshness exceeded the acceptable times") + class TestAlertModelDetectedAtTimezone: """detected_at is stored in UTC (naive) in the alerts table and must be