Skip to content

Commit

Permalink
Moved the message building part of the Teams alerting to a seperate c…
Browse files Browse the repository at this point in the history
…lass
  • Loading branch information
Frank Tubbing committed Feb 8, 2024
1 parent cd17dc3 commit a7278c4
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 55 deletions.
7 changes: 0 additions & 7 deletions elementary/clients/teams/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,6 @@ def addPotentialAction(self, action: potentialaction):


class TeamsWebhookClient(TeamsClient):
def __init__(
self,
webhook: str,
tracking: Optional[Tracking] = None,
):
super().__init__(webhook, tracking)

def _initial_client(self):
return connectorcard(self.webhook)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from pymsteams import cardsection, potentialaction # type: ignore

from elementary.clients.teams.client import TeamsClient


class TeamsAlertMessageBuilder:
def __init__(self, client: TeamsClient) -> None:
self.client = client

def title(self, title: str):
self.client.title(title)

def text(self, text: str):
self.client.text(text)

def addSection(self, section: cardsection):
self.client.addSection(section)

def addPotentialAction(self, action: potentialaction):
self.client.addPotentialAction(action)
113 changes: 65 additions & 48 deletions elementary/monitor/data_monitoring/alerts/integrations/teams/teams.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
from elementary.monitor.data_monitoring.alerts.integrations.base_integration import (
BaseIntegration,
)
from elementary.monitor.data_monitoring.alerts.integrations.teams.message_builder import (
TeamsAlertMessageBuilder,
)
from elementary.monitor.data_monitoring.alerts.integrations.utils.report_link import (
ReportLinkData,
)
Expand Down Expand Up @@ -72,6 +75,7 @@ def __init__(

# Enforce typing
self.client: TeamsClient
self.message_builder = TeamsAlertMessageBuilder(self.client)

def _initial_client(self, *args, **kwargs) -> TeamsClient:
teams_client = TeamsClient.create_client(
Expand Down Expand Up @@ -130,17 +134,17 @@ def _add_report_link_if_applicable(
report_link = alert.get_report_link()
if report_link:
action = self._get_potential_action(report_link)
self.client.addPotentialAction(action)
self.message_builder.addPotentialAction(action)

def _add_table_field_section_if_applicable(self, alert: TestAlertModel):
if TABLE_FIELD in (alert.alert_fields or DEFAULT_ALERT_FIELDS):
self.client.addSection(
self.message_builder.addSection(
self._get_section("*Table*", f"_{alert.table_full_name}_")
)

def _add_column_field_section_if_applicable(self, alert: TestAlertModel):
if COLUMN_FIELD in (alert.alert_fields or DEFAULT_ALERT_FIELDS):
self.client.addSection(
self.message_builder.addSection(
self._get_section("*Column*", f'_{alert.column_name or "No column"}_')
)

Expand All @@ -154,7 +158,7 @@ def _add_tags_field_section_if_applicable(
):
if TAGS_FIELD in (alert.alert_fields or DEFAULT_ALERT_FIELDS):
tags = prettify_and_dedup_list(alert.tags or [])
self.client.addSection(
self.message_builder.addSection(
self._get_section("*Tags*", f'_{tags or "No tags"}_')
)

Expand All @@ -168,7 +172,7 @@ def _add_owners_field_section_if_applicable(
):
if OWNERS_FIELD in (alert.alert_fields or DEFAULT_ALERT_FIELDS):
owners = prettify_and_dedup_list(alert.owners or [])
self.client.addSection(
self.message_builder.addSection(
self._get_section("*Owners*", f'_{owners or "No owners"}_')
)

Expand All @@ -182,15 +186,15 @@ def _add_subscribers_field_section_if_applicable(
):
if SUBSCRIBERS_FIELD in (alert.alert_fields or DEFAULT_ALERT_FIELDS):
subscribers = prettify_and_dedup_list(alert.subscribers or [])
self.client.addSection(
self.message_builder.addSection(
self._get_section(
"*Subscribers*", f'_{subscribers or "No subscribers"}_'
)
)

def _add_description_field_section_if_applicable(self, alert: TestAlertModel):
if DESCRIPTION_FIELD in (alert.alert_fields or DEFAULT_ALERT_FIELDS):
self.client.addSection(
self.message_builder.addSection(
self._get_section(
"*Description*", f'_{alert.test_description or "No description"}_'
)
Expand All @@ -213,7 +217,7 @@ def _add_result_message_field_section_if_applicable(
message = alert.error_message.strip()
if not message:
message = "No result message"
self.client.addSection(
self.message_builder.addSection(
self._get_section("*Result message*", f"_{message}_")
)

Expand All @@ -223,7 +227,7 @@ def _add_test_query_field_section_if_applicable(self, alert: TestAlertModel):
TEST_QUERY_FIELD in (alert.alert_fields or DEFAULT_ALERT_FIELDS)
and alert.test_results_query
):
self.client.addSection(
self.message_builder.addSection(
self._get_section(
"*Test query*", f"```{alert.test_results_query.strip()}"
)
Expand All @@ -234,7 +238,7 @@ def _add_test_params_field_section_if_applicable(self, alert: TestAlertModel):
TEST_PARAMS_FIELD in (alert.alert_fields or DEFAULT_ALERT_FIELDS)
and alert.test_params
):
self.client.addSection(
self.message_builder.addSection(
self._get_section("*Test parameters*", f"```{alert.test_params}```")
)

Expand All @@ -253,7 +257,7 @@ def _add_test_results_sample_field_section_if_applicable(
else:
df = pd.DataFrame(alert.test_rows_sample)
message = df.to_markdown(index=False)
self.client.addSection(
self.message_builder.addSection(
self._get_section("*Test results sample*", f"{message}")
)

Expand All @@ -263,8 +267,8 @@ def _get_dbt_test_template(self, alert: TestAlertModel, *args, **kwargs):

self._add_report_link_if_applicable(alert)

self.client.title(title)
self.client.text(subtitle)
self.message_builder.title(title)
self.message_builder.text(subtitle)

self._add_table_field_section_if_applicable(alert)
self._add_column_field_section_if_applicable(alert)
Expand All @@ -287,8 +291,8 @@ def _get_elementary_test_template(self, alert: TestAlertModel, *args, **kwargs):

self._add_report_link_if_applicable(alert)

self.client.title(title)
self.client.text(subtitle)
self.message_builder.title(title)
self.message_builder.text(subtitle)

self._add_table_field_section_if_applicable(alert)
self._add_column_field_section_if_applicable(alert)
Expand All @@ -306,42 +310,44 @@ def _get_model_template(self, alert: ModelAlertModel, *args, **kwargs):

self._add_report_link_if_applicable(alert)

self.client.title(title)
self.client.text(subtitle)
self.message_builder.title(title)
self.message_builder.text(subtitle)
self._add_tags_field_section_if_applicable(alert)
self._add_owners_field_section_if_applicable(alert)
self._add_subscribers_field_section_if_applicable(alert)
self._add_result_message_field_section_if_applicable(alert)

if alert.materialization:
self.client.addSection(
self.message_builder.addSection(
self._get_section(
"*Materialization*", f"`{str(alert.materialization)}`"
)
)
if alert.full_refresh:
self.client.addSection(
self.message_builder.addSection(
self._get_section("*Full refresh*", f"`{alert.full_refresh}`")
)
if alert.path:
self.client.addSection(self._get_section("*Path*", f"`{alert.path}`"))
self.message_builder.addSection(
self._get_section("*Path*", f"`{alert.path}`")
)

def _get_snapshot_template(self, alert: ModelAlertModel, *args, **kwargs):
title = f"{self._get_display_name(alert.status)}: {alert.summary}"
subtitle = self._get_alert_sub_title(alert)

self._add_report_link_if_applicable(alert)

self.client.title(title)
self.client.text(subtitle)
self.message_builder.title(title)
self.message_builder.text(subtitle)

self._add_tags_field_section_if_applicable(alert)
self._add_owners_field_section_if_applicable(alert)
self._add_subscribers_field_section_if_applicable(alert)
self._add_result_message_field_section_if_applicable(alert)

if alert.original_path:
self.client.addSection(
self.message_builder.addSection(
self._get_section("*Path*", f"`{alert.original_path}`")
)

Expand All @@ -353,68 +359,72 @@ def _get_source_freshness_template(

self._add_report_link_if_applicable(alert)

self.client.title(title)
self.client.text(subtitle)
self.message_builder.title(title)
self.message_builder.text(subtitle)

self._add_tags_field_section_if_applicable(alert)
self._add_owners_field_section_if_applicable(alert)
self._add_subscribers_field_section_if_applicable(alert)

if alert.freshness_description:
self.client.addSection(
self.message_builder.addSection(
self._get_section(
"*Description*",
f'_{alert.freshness_description or "No description"}_',
)
)

if alert.status == "runtime error":
self.client.addSection(
self.message_builder.addSection(
self._get_section(
"*Result message*",
f"Failed to calculate the source freshness\n```{alert.error}```",
)
)
else:
self.client.addSection(
self.message_builder.addSection(
self._get_section(
"*Result message*", f"```{alert.result_description}```"
)
)

if alert.status != "runtime error":
self.client.addSection(
self.message_builder.addSection(
self._get_section(
"*Time Elapsed*",
f"{timedelta(seconds=alert.max_loaded_at_time_ago_in_s) if alert.max_loaded_at_time_ago_in_s else 'N/A'}",
)
)

if alert.status != "runtime error":
self.client.addSection(
self.message_builder.addSection(
self._get_section("*Last Record At*", f"{alert.max_loaded_at}")
)

if alert.status != "runtime error":
self.client.addSection(
self.message_builder.addSection(
self._get_section("*Sampled At*", f"{alert.snapshotted_at_str}")
)

if alert.error_after:
self.client.addSection(
self.message_builder.addSection(
self._get_section("*Error after*", f"`{alert.error_after}`")
)

if alert.error_after:
self.client.addSection(
self.message_builder.addSection(
self._get_section("*Warn after*", f"`{alert.warn_after}`")
)

if alert.error_after:
self.client.addSection(self._get_section("*Filter*", f"`{alert.filter}`"))
self.message_builder.addSection(
self._get_section("*Filter*", f"`{alert.filter}`")
)

if alert.path:
self.client.addSection(self._get_section("*Path*", f"`{alert.path}`"))
self.message_builder.addSection(
self._get_section("*Path*", f"`{alert.path}`")
)

def _get_group_by_table_template(
self, alert: GroupedByTableAlerts, *args, **kwargs
Expand Down Expand Up @@ -453,8 +463,8 @@ def _get_group_by_table_template(

self._add_report_link_if_applicable(alert)

self.client.title(title)
self.client.text(subtitle)
self.message_builder.title(title)
self.message_builder.text(subtitle)

tags = list_of_lists_of_strings_to_comma_delimited_unique_strings(
[alert.tags or [] for alert in alerts]
Expand All @@ -466,13 +476,13 @@ def _get_group_by_table_template(
[alert.subscribers or [] for alert in alerts]
)

self.client.addSection(
self.message_builder.addSection(
self._get_section("*Tags*", f'_{tags if tags else "No tags"}_')
)
self.client.addSection(
self.message_builder.addSection(
self._get_section("*Owners*", f'_{owners if owners else "No owners"}_')
)
self.client.addSection(
self.message_builder.addSection(
self._get_section(
"*Subscribers*", f'_{subscribers if subscribers else "No subscribers"}_'
)
Expand All @@ -487,22 +497,28 @@ def _get_group_by_table_template(
section.activityText(
f"{self._get_model_error_block_body(alert.model_errors)}"
)
self.client.addSection(section)
self.message_builder.addSection(section)

if alert.test_failures:
rows = [alert.concise_name for alert in alert.test_failures]
text = "\n".join([f"🔺 {row}" for row in rows])
self.client.addSection(self._get_section("*Test failures*", f"{text}"))
self.message_builder.addSection(
self._get_section("*Test failures*", f"{text}")
)

if alert.test_warnings:
rows = [alert.concise_name for alert in alert.test_warnings]
text = "\n".join([f"⚠ {row}" for row in rows])
self.client.addSection(self._get_section("*Test warnings*", f"{text}"))
self.message_builder.addSection(
self._get_section("*Test warnings*", f"{text}")
)

if alert.test_errors:
rows = [alert.concise_name for alert in alert.test_errors]
text = "\n".join([f"❗ {row}" for row in rows])
self.client.addSection(self._get_section("*Test errors*", f"{text}"))
self.message_builder.addSection(
self._get_section("*Test errors*", f"{text}")
)

def _get_fallback_template(
self,
Expand All @@ -516,14 +532,14 @@ def _get_fallback_template(
**kwargs,
):
# Since the title can never be truncated and the text can be truncated by Teams, I think it is good to have a title + text in the fallback template
self.client.title(
self.message_builder.title(
"Oops, we failed to format the alert ! -_-' Please share this with the Elementary team via <https://elementary-data.com/community> or a <https://github.com/elementary-data/elementary/issues/new|GitHub> issue."
)
self.client.text(f"```{json.dumps(alert.data, indent=2)}")
self.message_builder.text(f"```{json.dumps(alert.data, indent=2)}")

def _get_test_message_template(self, *args, **kwargs):
self.client.title("This is a test message generated by Elementary!")
self.client.text(
self.message_builder.title("This is a test message generated by Elementary!")
self.message_builder.text(
f"Elementary monitor ran successfully on {datetime.now().strftime('%Y-%m-%d %H:%M')}"
)

Expand Down Expand Up @@ -557,6 +573,7 @@ def send_alert(
sent_successfully = fallback_sent_successfully
# Resetting the client so that it does not cache the message of other alerts
self.client = self._initial_client()
self.message_builder = TeamsAlertMessageBuilder(self.client)

return sent_successfully

Expand Down

0 comments on commit a7278c4

Please sign in to comment.