Skip to content

Commit 26a2294

Browse files
authored
Merge branch 'master' into yu-iskw-patch-1
2 parents cdb87b2 + 5cdc1dd commit 26a2294

22 files changed

Lines changed: 777 additions & 29 deletions

File tree

elementary/messages/blocks.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class Icon(Enum):
2020
BELL = "bell"
2121
GEM = "gem"
2222
SPARKLES = "sparkles"
23+
LINK = "link"
2324

2425

2526
class TextStyle(Enum):

elementary/messages/formats/unicode.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
Icon.BELL: "🔔",
1616
Icon.GEM: "💎",
1717
Icon.SPARKLES: "✨",
18+
Icon.LINK: "🔗",
1819
}
1920

2021
for icon in Icon:

elementary/monitor/alerts/alert.py

Lines changed: 47 additions & 0 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,
@@ -32,6 +44,12 @@ def __init__(
3244
alert_fields: Optional[List[str]] = None,
3345
elementary_database_and_schema: Optional[str] = None,
3446
env: Optional[str] = None,
47+
job_id: Optional[str] = None,
48+
job_name: Optional[str] = None,
49+
job_run_id: Optional[str] = None,
50+
job_url: Optional[str] = None,
51+
job_run_url: Optional[str] = None,
52+
orchestrator: Optional[str] = None,
3553
**kwargs,
3654
):
3755
self.id = id
@@ -65,6 +83,12 @@ def __init__(
6583
self.alert_fields = alert_fields
6684
self.elementary_database_and_schema = elementary_database_and_schema
6785
self.env = env
86+
self.job_id = job_id
87+
self.job_name = job_name
88+
self.job_run_id = job_run_id
89+
self.job_url = job_url
90+
self.job_run_url = job_run_url
91+
self.orchestrator = orchestrator
6892

6993
@property
7094
def unified_meta(self) -> Dict:
@@ -84,3 +108,26 @@ def summary(self) -> str:
84108

85109
def get_report_link(self) -> Optional[ReportLinkData]:
86110
raise NotImplementedError
111+
112+
@property
113+
def orchestrator_info(self) -> Optional[OrchestratorInfo]:
114+
"""Returns structured orchestrator metadata if available."""
115+
if not any(
116+
[
117+
self.job_name,
118+
self.job_run_id,
119+
self.orchestrator,
120+
self.job_url,
121+
self.job_run_url,
122+
]
123+
):
124+
return None
125+
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: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
from pydantic import BaseModel
55

66
from elementary.messages.block_builders import (
7+
BoldTextBlock,
78
BoldTextLineBlock,
89
BulletListBlock,
910
FactsBlock,
1011
ItalicTextLineBlock,
1112
JsonCodeBlock,
13+
LinkInlineBlocks,
1214
LinksLineBlock,
1315
MentionLineBlock,
1416
NonPrimaryFactBlock,
@@ -33,6 +35,7 @@
3335
TextStyle,
3436
)
3537
from elementary.messages.message_body import Color, MessageBlock, MessageBody
38+
from elementary.monitor.alerts.alert import OrchestratorInfo
3639
from elementary.monitor.alerts.alert_messages.alert_fields import AlertField
3740
from elementary.monitor.alerts.alerts_groups.alerts_group import AlertsGroup
3841
from elementary.monitor.alerts.alerts_groups.base_alerts_group import BaseAlertsGroup
@@ -42,6 +45,9 @@
4245
from elementary.monitor.alerts.model_alert import ModelAlertModel
4346
from elementary.monitor.alerts.source_freshness_alert import SourceFreshnessAlertModel
4447
from elementary.monitor.alerts.test_alert import TestAlertModel
48+
from elementary.monitor.data_monitoring.alerts.integrations.utils.orchestrator_link import (
49+
create_orchestrator_link,
50+
)
4551
from elementary.monitor.data_monitoring.alerts.integrations.utils.report_link import (
4652
ReportLinkData,
4753
)
@@ -106,6 +112,7 @@ def _get_run_alert_subtitle_block(
106112
suppression_interval: Optional[int] = None,
107113
env: Optional[str] = None,
108114
links: list[ReportLinkData] = [],
115+
orchestrator_info: Optional[OrchestratorInfo] = None,
109116
) -> LinesBlock:
110117
summary = []
111118
summary.append((type.capitalize() + ":", name))
@@ -114,16 +121,54 @@ def _get_run_alert_subtitle_block(
114121
summary.append(("Status:", status or "Unknown"))
115122
if detected_at_str:
116123
summary.append(("Time:", detected_at_str))
124+
125+
subtitle_lines = []
126+
127+
if orchestrator_info and orchestrator_info.job_name:
128+
orchestrator_name = orchestrator_info.orchestrator or "orchestrator"
129+
job_info_text = f"{orchestrator_info.job_name} (via {orchestrator_name})"
130+
131+
orchestrator_link = create_orchestrator_link(orchestrator_info)
132+
if orchestrator_link:
133+
job_inlines: List[InlineBlock] = [
134+
BoldTextBlock(text="Job:"),
135+
TextBlock(text=job_info_text + " | "),
136+
]
137+
job_inlines.extend(
138+
LinkInlineBlocks(
139+
text=orchestrator_link.text,
140+
url=orchestrator_link.url,
141+
icon=orchestrator_link.icon,
142+
)
143+
)
144+
145+
subtitle_lines.append(LineBlock(inlines=job_inlines))
146+
else:
147+
summary.append(("Job:", job_info_text))
117148
if suppression_interval:
118149
summary.append(("Suppression interval:", str(suppression_interval)))
119-
subtitle_lines = [SummaryLineBlock(summary=summary)]
120150

121-
if links:
122-
subtitle_lines.append(
123-
LinksLineBlock(
124-
links=[(link.text, link.url, link.icon) for link in links]
151+
subtitle_lines.append(SummaryLineBlock(summary=summary))
152+
153+
all_links = []
154+
155+
for link in links:
156+
all_links.append((link.text, link.url, link.icon))
157+
158+
if orchestrator_info and not orchestrator_info.job_name:
159+
orchestrator_link = create_orchestrator_link(orchestrator_info)
160+
if orchestrator_link:
161+
all_links.append(
162+
(
163+
orchestrator_link.text,
164+
orchestrator_link.url,
165+
orchestrator_link.icon,
166+
)
125167
)
126-
)
168+
169+
if all_links:
170+
subtitle_lines.append(LinksLineBlock(links=all_links))
171+
127172
return LinesBlock(lines=subtitle_lines)
128173

129174
def _get_run_alert_subtitle_links(
@@ -151,6 +196,7 @@ def _get_run_alert_subtitle_blocks(
151196
asset_type = "snapshot" if alert.materialization == "snapshot" else "model"
152197
asset_name = alert.alias
153198
links = self._get_run_alert_subtitle_links(alert)
199+
orchestrator_info = alert.orchestrator_info
154200
return [
155201
self._get_run_alert_subtitle_block(
156202
type=asset_type,
@@ -160,6 +206,7 @@ def _get_run_alert_subtitle_blocks(
160206
suppression_interval=alert.suppression_interval,
161207
env=alert.env,
162208
links=links,
209+
orchestrator_info=orchestrator_info,
163210
)
164211
]
165212

elementary/monitor/alerts/model_alert.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ def __init__(
3737
alert_fields: Optional[List[str]] = None,
3838
elementary_database_and_schema: Optional[str] = None,
3939
env: Optional[str] = None,
40+
job_id: Optional[str] = None,
41+
job_name: Optional[str] = None,
42+
job_run_id: Optional[str] = None,
43+
job_url: Optional[str] = None,
44+
job_run_url: Optional[str] = None,
45+
orchestrator: Optional[str] = None,
4046
**kwargs,
4147
):
4248
super().__init__(
@@ -57,6 +63,13 @@ def __init__(
5763
alert_fields,
5864
elementary_database_and_schema,
5965
env=env,
66+
job_id=job_id,
67+
job_name=job_name,
68+
job_run_id=job_run_id,
69+
job_url=job_url,
70+
job_run_url=job_run_url,
71+
orchestrator=orchestrator,
72+
**kwargs,
6073
)
6174
self.alias = alias
6275
self.path = path

elementary/monitor/alerts/source_freshness_alert.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ def __init__(
4646
alert_fields: Optional[List[str]] = None,
4747
elementary_database_and_schema: Optional[str] = None,
4848
env: Optional[str] = None,
49+
job_id: Optional[str] = None,
50+
job_name: Optional[str] = None,
51+
job_run_id: Optional[str] = None,
52+
job_url: Optional[str] = None,
53+
job_run_url: Optional[str] = None,
54+
orchestrator: Optional[str] = None,
4955
**kwargs,
5056
):
5157
super().__init__(
@@ -66,6 +72,13 @@ def __init__(
6672
alert_fields,
6773
elementary_database_and_schema,
6874
env=env,
75+
job_id=job_id,
76+
job_name=job_name,
77+
job_run_id=job_run_id,
78+
job_url=job_url,
79+
job_run_url=job_run_url,
80+
orchestrator=orchestrator,
81+
**kwargs,
6982
)
7083
self.snapshotted_at_str = (
7184
convert_datetime_utc_str_to_timezone_str(

elementary/monitor/alerts/test_alert.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ def __init__(
4848
alert_fields: Optional[List[str]] = None,
4949
elementary_database_and_schema: Optional[str] = None,
5050
env: Optional[str] = None,
51+
job_id: Optional[str] = None,
52+
job_name: Optional[str] = None,
53+
job_run_id: Optional[str] = None,
54+
job_url: Optional[str] = None,
55+
job_run_url: Optional[str] = None,
56+
orchestrator: Optional[str] = None,
5157
**kwargs,
5258
):
5359
super().__init__(
@@ -68,6 +74,13 @@ def __init__(
6874
alert_fields,
6975
elementary_database_and_schema,
7076
env=env,
77+
job_id=job_id,
78+
job_name=job_name,
79+
job_run_id=job_run_id,
80+
job_url=job_url,
81+
job_run_url=job_run_url,
82+
orchestrator=orchestrator,
83+
**kwargs,
7184
)
7285
self.table_name = table_name
7386
self.test_type = test_type
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from typing import Optional
2+
3+
from elementary.messages.blocks import Icon
4+
from elementary.monitor.alerts.alert import OrchestratorInfo
5+
from elementary.utils.pydantic_shim import BaseModel
6+
7+
8+
class OrchestratorLinkData(BaseModel):
9+
url: str
10+
text: str
11+
orchestrator: str
12+
icon: Optional[Icon] = None
13+
14+
15+
def create_orchestrator_link(
16+
orchestrator_info: OrchestratorInfo,
17+
) -> Optional[OrchestratorLinkData]:
18+
"""Create an orchestrator link from orchestrator info if URL is available."""
19+
if not orchestrator_info or not orchestrator_info.run_url:
20+
return None
21+
22+
orchestrator = orchestrator_info.orchestrator or "orchestrator"
23+
24+
return OrchestratorLinkData(
25+
url=orchestrator_info.run_url,
26+
text=f"View in {orchestrator}",
27+
orchestrator=orchestrator,
28+
icon=Icon.LINK,
29+
)
30+
31+
32+
def create_job_link(
33+
orchestrator_info: OrchestratorInfo,
34+
) -> Optional[OrchestratorLinkData]:
35+
"""Create a job-level orchestrator link if job URL is available."""
36+
if not orchestrator_info or not orchestrator_info.job_url:
37+
return None
38+
39+
orchestrator = orchestrator_info.orchestrator or "orchestrator"
40+
job_name = orchestrator_info.job_name or "Job"
41+
42+
display_name = orchestrator.replace("_", " ").title()
43+
44+
return OrchestratorLinkData(
45+
url=orchestrator_info.job_url,
46+
text=f"{job_name} in {display_name}",
47+
orchestrator=orchestrator,
48+
icon=Icon.GEAR,
49+
)

0 commit comments

Comments
 (0)