Skip to content

Commit d0ae9ea

Browse files
committed
Add OrchestratorInfo class and update alert handling for class
1 parent f721a57 commit d0ae9ea

5 files changed

Lines changed: 90 additions & 79 deletions

File tree

elementary/monitor/alerts/alert.py

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,23 @@
77
ReportLinkData,
88
)
99
from elementary.utils.log import get_logger
10+
from elementary.utils.pydantic_shim import BaseModel
1011
from elementary.utils.time import DATETIME_WITH_TIMEZONE_FORMAT
1112

1213
logger = get_logger(__name__)
1314

1415

16+
class OrchestratorInfo(BaseModel):
17+
"""Structured orchestrator metadata for alerts."""
18+
19+
job_id: Optional[str] = None
20+
job_name: Optional[str] = None
21+
run_id: Optional[str] = None
22+
orchestrator: Optional[str] = None
23+
job_url: Optional[str] = None
24+
run_url: Optional[str] = None
25+
26+
1527
class AlertModel:
1628
def __init__(
1729
self,
@@ -98,7 +110,7 @@ def get_report_link(self) -> Optional[ReportLinkData]:
98110
raise NotImplementedError
99111

100112
@property
101-
def orchestrator_info(self) -> Optional[Dict[str, str]]:
113+
def orchestrator_info(self) -> Optional[OrchestratorInfo]:
102114
"""Returns structured orchestrator metadata if available."""
103115
if not any(
104116
[
@@ -111,18 +123,11 @@ def orchestrator_info(self) -> Optional[Dict[str, str]]:
111123
):
112124
return None
113125

114-
info = {}
115-
if self.job_id:
116-
info["job_id"] = self.job_id
117-
if self.job_name:
118-
info["job_name"] = self.job_name
119-
if self.job_run_id:
120-
info["run_id"] = self.job_run_id
121-
if self.orchestrator:
122-
info["orchestrator"] = self.orchestrator
123-
if self.job_url:
124-
info["job_url"] = self.job_url
125-
if self.job_run_url:
126-
info["run_url"] = self.job_run_url
127-
128-
return info
126+
return OrchestratorInfo(
127+
job_id=self.job_id or None,
128+
job_name=self.job_name or None,
129+
run_id=self.job_run_id or None,
130+
orchestrator=self.orchestrator or None,
131+
job_url=self.job_url or None,
132+
run_url=self.job_run_url or None,
133+
)

elementary/monitor/alerts/alert_messages/builder.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
TextStyle,
3636
)
3737
from elementary.messages.message_body import Color, MessageBlock, MessageBody
38+
from elementary.monitor.alerts.alert import OrchestratorInfo
3839
from elementary.monitor.alerts.alert_messages.alert_fields import AlertField
3940
from elementary.monitor.alerts.alerts_groups.alerts_group import AlertsGroup
4041
from elementary.monitor.alerts.alerts_groups.base_alerts_group import BaseAlertsGroup
@@ -111,7 +112,7 @@ def _get_run_alert_subtitle_block(
111112
suppression_interval: Optional[int] = None,
112113
env: Optional[str] = None,
113114
links: list[ReportLinkData] = [],
114-
orchestrator_info: Optional[Dict[str, str]] = None,
115+
orchestrator_info: Optional[OrchestratorInfo] = None,
115116
) -> LinesBlock:
116117
summary = []
117118
summary.append((type.capitalize() + ":", name))
@@ -124,9 +125,9 @@ def _get_run_alert_subtitle_block(
124125
# Initialize subtitle lines with summary
125126
subtitle_lines = []
126127

127-
if orchestrator_info and orchestrator_info.get("job_name"):
128-
orchestrator_name = orchestrator_info.get("orchestrator", "orchestrator")
129-
job_info_text = f"{orchestrator_info['job_name']} (via {orchestrator_name})"
128+
if orchestrator_info and orchestrator_info.job_name:
129+
orchestrator_name = orchestrator_info.orchestrator or "orchestrator"
130+
job_info_text = f"{orchestrator_info.job_name} (via {orchestrator_name})"
130131

131132
# Create job info with inline orchestrator link
132133
orchestrator_link = create_orchestrator_link(orchestrator_info)
@@ -162,7 +163,7 @@ def _get_run_alert_subtitle_block(
162163
all_links.append((link.text, link.url, link.icon))
163164

164165
# Add orchestrator link if available (only if not already added inline)
165-
if orchestrator_info and not orchestrator_info.get("job_name"):
166+
if orchestrator_info and not orchestrator_info.job_name:
166167
orchestrator_link = create_orchestrator_link(orchestrator_info)
167168
if orchestrator_link:
168169
all_links.append(

elementary/monitor/data_monitoring/alerts/integrations/utils/orchestrator_link.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
from typing import Dict, Optional
1+
from typing import Optional
22

33
from elementary.messages.blocks import Icon
4+
from elementary.monitor.alerts.alert import OrchestratorInfo
45
from elementary.utils.pydantic_shim import BaseModel
56

67

@@ -12,37 +13,37 @@ class OrchestratorLinkData(BaseModel):
1213

1314

1415
def create_orchestrator_link(
15-
orchestrator_info: Dict[str, str]
16+
orchestrator_info: OrchestratorInfo,
1617
) -> Optional[OrchestratorLinkData]:
1718
"""Create an orchestrator link from orchestrator info if URL is available."""
18-
if not orchestrator_info or not orchestrator_info.get("run_url"):
19+
if not orchestrator_info or not orchestrator_info.run_url:
1920
return None
2021

21-
orchestrator = orchestrator_info.get("orchestrator", "orchestrator")
22+
orchestrator = orchestrator_info.orchestrator or "orchestrator"
2223

2324
return OrchestratorLinkData(
24-
url=orchestrator_info["run_url"],
25+
url=orchestrator_info.run_url,
2526
text=f"View in {orchestrator}",
2627
orchestrator=orchestrator,
2728
icon=Icon.LINK,
2829
)
2930

3031

3132
def create_job_link(
32-
orchestrator_info: Dict[str, str]
33+
orchestrator_info: OrchestratorInfo,
3334
) -> Optional[OrchestratorLinkData]:
3435
"""Create a job-level orchestrator link if job URL is available."""
35-
if not orchestrator_info or not orchestrator_info.get("job_url"):
36+
if not orchestrator_info or not orchestrator_info.job_url:
3637
return None
3738

38-
orchestrator = orchestrator_info.get("orchestrator", "orchestrator")
39-
job_name = orchestrator_info.get("job_name", "Job")
39+
orchestrator = orchestrator_info.orchestrator or "orchestrator"
40+
job_name = orchestrator_info.job_name or "Job"
4041

4142
# Capitalize orchestrator name for display
4243
display_name = orchestrator.replace("_", " ").title()
4344

4445
return OrchestratorLinkData(
45-
url=orchestrator_info["job_url"],
46+
url=orchestrator_info.job_url,
4647
text=f"{job_name} in {display_name}",
4748
orchestrator=orchestrator,
4849
icon=Icon.GEAR,

tests/unit/alerts/alert_messages/test_orchestrator_message_simple.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,14 +144,14 @@ def test_orchestrator_info_property_integration(self):
144144
orchestrator_info = alert.orchestrator_info
145145

146146
assert orchestrator_info is not None
147-
assert orchestrator_info["job_name"] == "integration_test"
148-
assert orchestrator_info["run_id"] == "run_123"
149-
assert orchestrator_info["orchestrator"] == "airflow"
147+
assert orchestrator_info.job_name == "integration_test"
148+
assert orchestrator_info.run_id == "run_123"
149+
assert orchestrator_info.orchestrator == "airflow"
150150
assert (
151-
orchestrator_info["job_url"]
151+
orchestrator_info.job_url
152152
== "https://airflow.example.com/job/integration_test"
153153
)
154-
assert orchestrator_info["run_url"] == "https://airflow.example.com/run/123"
154+
assert orchestrator_info.run_url == "https://airflow.example.com/run/123"
155155

156156
# Test message includes this data
157157
alert.get_report_link = Mock(return_value=None)

tests/unit/alerts/test_orchestrator_integration.py

Lines changed: 47 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest
22

33
from elementary.messages.blocks import Icon
4+
from elementary.monitor.alerts.alert import OrchestratorInfo
45
from elementary.monitor.alerts.model_alert import ModelAlertModel
56
from elementary.monitor.alerts.source_freshness_alert import SourceFreshnessAlertModel
67
from elementary.monitor.alerts.test_alert import TestAlertModel
@@ -15,12 +16,12 @@ class TestOrchestratorLinkCreation:
1516
"""Test orchestrator link creation functionality."""
1617

1718
def test_create_orchestrator_link_with_valid_data(self):
18-
orchestrator_info = {
19-
"job_name": "nightly_load",
20-
"run_id": "12345",
21-
"orchestrator": "airflow",
22-
"run_url": "https://airflow.example.com/run/12345",
23-
}
19+
orchestrator_info = OrchestratorInfo(
20+
job_name="nightly_load",
21+
run_id="12345",
22+
orchestrator="airflow",
23+
run_url="https://airflow.example.com/run/12345",
24+
)
2425

2526
link = create_orchestrator_link(orchestrator_info)
2627

@@ -30,25 +31,25 @@ def test_create_orchestrator_link_with_valid_data(self):
3031
assert link.icon == Icon.LINK
3132

3233
def test_create_orchestrator_link_without_url_returns_none(self):
33-
orchestrator_info = {
34-
"job_name": "nightly_load",
35-
"orchestrator": "airflow"
34+
orchestrator_info = OrchestratorInfo(
35+
job_name="nightly_load",
36+
orchestrator="airflow",
3637
# No run_url
37-
}
38+
)
3839

3940
link = create_orchestrator_link(orchestrator_info)
4041
assert link is None
4142

4243
def test_create_orchestrator_link_with_empty_info_returns_none(self):
43-
link = create_orchestrator_link({})
44+
link = create_orchestrator_link(OrchestratorInfo())
4445
assert link is None
4546

4647
def test_create_job_link_with_valid_data(self):
47-
orchestrator_info = {
48-
"job_name": "nightly_load",
49-
"orchestrator": "airflow",
50-
"job_url": "https://airflow.example.com/job/nightly_load",
51-
}
48+
orchestrator_info = OrchestratorInfo(
49+
job_name="nightly_load",
50+
orchestrator="airflow",
51+
job_url="https://airflow.example.com/job/nightly_load",
52+
)
5253

5354
link = create_job_link(orchestrator_info)
5455

@@ -59,11 +60,11 @@ def test_create_job_link_with_valid_data(self):
5960
assert link.icon == Icon.GEAR
6061

6162
def test_create_job_link_without_url_returns_none(self):
62-
orchestrator_info = {
63-
"job_name": "nightly_load",
64-
"orchestrator": "airflow"
63+
orchestrator_info = OrchestratorInfo(
64+
job_name="nightly_load",
65+
orchestrator="airflow",
6566
# No job_url
66-
}
67+
)
6768

6869
link = create_job_link(orchestrator_info)
6970
assert link is None
@@ -92,11 +93,11 @@ def test_test_alert_orchestrator_info_with_complete_data(self):
9293

9394
info = alert.orchestrator_info
9495
assert info is not None
95-
assert info["job_name"] == "nightly_load"
96-
assert info["run_id"] == "12345"
97-
assert info["orchestrator"] == "airflow"
98-
assert info["job_url"] == "https://airflow.example.com/job/nightly_load"
99-
assert info["run_url"] == "https://airflow.example.com/run/12345"
96+
assert info.job_name == "nightly_load"
97+
assert info.run_id == "12345"
98+
assert info.orchestrator == "airflow"
99+
assert info.job_url == "https://airflow.example.com/job/nightly_load"
100+
assert info.run_url == "https://airflow.example.com/run/12345"
100101

101102
def test_test_alert_orchestrator_info_with_minimal_data(self):
102103
alert = TestAlertModel(
@@ -115,8 +116,10 @@ def test_test_alert_orchestrator_info_with_minimal_data(self):
115116

116117
info = alert.orchestrator_info
117118
assert info is not None
118-
assert info["job_name"] == "test_job"
119-
assert len(info) == 1
119+
assert info.job_name == "test_job"
120+
# Other fields should be None
121+
assert info.run_id is None
122+
assert info.orchestrator is None
120123

121124
def test_test_alert_orchestrator_info_with_no_data_returns_none(self):
122125
alert = TestAlertModel(
@@ -152,10 +155,10 @@ def test_model_alert_orchestrator_info(self):
152155

153156
info = alert.orchestrator_info
154157
assert info is not None
155-
assert info["job_name"] == "nightly_build"
156-
assert info["run_id"] == "67890"
157-
assert info["orchestrator"] == "dbt_cloud"
158-
assert info["run_url"] == "https://cloud.getdbt.com/run/67890"
158+
assert info.job_name == "nightly_build"
159+
assert info.run_id == "67890"
160+
assert info.orchestrator == "dbt_cloud"
161+
assert info.run_url == "https://cloud.getdbt.com/run/67890"
159162

160163
def test_source_freshness_alert_orchestrator_info(self):
161164
alert = SourceFreshnessAlertModel(
@@ -174,9 +177,9 @@ def test_source_freshness_alert_orchestrator_info(self):
174177

175178
info = alert.orchestrator_info
176179
assert info is not None
177-
assert info["job_name"] == "freshness_check"
178-
assert info["orchestrator"] == "airflow"
179-
assert info["run_url"] == "https://airflow.example.com/run/111"
180+
assert info.job_name == "freshness_check"
181+
assert info.orchestrator == "airflow"
182+
assert info.run_url == "https://airflow.example.com/run/111"
180183

181184

182185
class TestOrchestratorInfoEdgeCases:
@@ -201,7 +204,7 @@ def test_orchestrator_info_with_empty_strings(self):
201204
info = alert.orchestrator_info
202205
assert info is None
203206

204-
def test_orchestrator_info_filters_none_values(self):
207+
def test_orchestrator_info_sets_none_for_empty_values(self):
205208
alert = TestAlertModel(
206209
id="test_id",
207210
test_unique_id="test_unique_id",
@@ -219,10 +222,9 @@ def test_orchestrator_info_filters_none_values(self):
219222

220223
info = alert.orchestrator_info
221224
assert info is not None
222-
assert "job_name" in info
223-
assert "orchestrator" not in info
224-
assert "run_url" not in info
225-
assert len(info) == 1
225+
assert info.job_name == "valid_job"
226+
assert info.orchestrator is None
227+
assert info.run_url is None
226228

227229
def test_orchestrator_info_with_only_run_id(self):
228230
alert = TestAlertModel(
@@ -240,8 +242,9 @@ def test_orchestrator_info_with_only_run_id(self):
240242

241243
info = alert.orchestrator_info
242244
assert info is not None
243-
assert info["run_id"] == "12345"
244-
assert len(info) == 1
245+
assert info.run_id == "12345"
246+
assert info.job_name is None
247+
assert info.orchestrator is None
245248

246249
def test_orchestrator_info_with_only_orchestrator(self):
247250
alert = TestAlertModel(
@@ -259,8 +262,9 @@ def test_orchestrator_info_with_only_orchestrator(self):
259262

260263
info = alert.orchestrator_info
261264
assert info is not None
262-
assert info["orchestrator"] == "dbt_cloud"
263-
assert len(info) == 1
265+
assert info.orchestrator == "dbt_cloud"
266+
assert info.job_name is None
267+
assert info.run_id is None
264268

265269

266270
class TestOrchestratorLinkDataModel:

0 commit comments

Comments
 (0)