Skip to content

Commit 8c8c8b2

Browse files
FrankTubFrank Tubbingharitamar
authored
Feature: make the number of columns that are formatted as a table in Teams alerts a cli flag so that users have more control over the formatting in Teams (#1958)
* Introduced a new configuration setting in which one can change the number of columns that are allowed in a result sample that will be displayed as a table. If it exceeds this number of columns than it will be parsed as a JSON * Use full width for Teams message to allow beter formatting of a table * Depending on size of table format it differently so that large tables are better readable * Refactor code + implement a sleep to prevent rate limits from being hit in Teams for scenarios where a lot of alerts need to be sent * Error handling for the teams webhook alerting to handle cases like network exceptions * Correct data type in which the message configuration is populating the message that is sent to MS Teams * Refactor coderabbitai suggestion * Implemented feedback of Elazar * Adapt new full widt layout for MS Teams alerting in unit tests * Fix failing unit tests --------- Co-authored-by: Frank Tubbing <[email protected]> Co-authored-by: Itamar Hartstein <[email protected]>
1 parent 7409388 commit 8c8c8b2

File tree

121 files changed

+1084
-409
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

121 files changed

+1084
-409
lines changed

elementary/config/config.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def __init__(
7171
azure_container_name: Optional[str] = None,
7272
report_url: Optional[str] = None,
7373
teams_webhook: Optional[str] = None,
74+
maximum_columns_in_alert_samples: Optional[int] = None,
7475
env: str = DEFAULT_ENV,
7576
run_dbt_deps_if_needed: Optional[bool] = None,
7677
project_name: Optional[str] = None,
@@ -103,6 +104,12 @@ def __init__(
103104
False,
104105
)
105106

107+
self.maximum_columns_in_alert_samples = self._first_not_none(
108+
maximum_columns_in_alert_samples,
109+
config.get("maximum_columns_in_alert_samples"),
110+
4,
111+
)
112+
106113
self.timezone = self._first_not_none(
107114
timezone,
108115
config.get("timezone"),

elementary/messages/formats/adaptive_cards.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,8 @@ def format_table_block(block: TableBlock) -> Dict[str, Any]:
144144
"items": [
145145
{
146146
"type": "TextBlock",
147-
"text": str(cell),
147+
"text": str(cell) if cell is not None else "",
148+
"wrap": True,
148149
}
149150
],
150151
}
@@ -256,4 +257,5 @@ def format_adaptive_card(message: MessageBody, version: str = "1.5") -> Dict[str
256257
"type": "AdaptiveCard",
257258
"body": format_adaptive_card_body(message),
258259
"version": version,
260+
"msteams": {"width": "Full"},
259261
}

elementary/messages/messaging_integrations/teams_webhook.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from datetime import datetime
2+
from http import HTTPStatus
23
from typing import Any, Optional
34

45
import requests
6+
from ratelimit import limits, sleep_and_retry
57
from typing_extensions import TypeAlias
68

79
from elementary.messages.formats.adaptive_cards import format_adaptive_card
@@ -22,6 +24,7 @@
2224

2325

2426
Channel: TypeAlias = Optional[str]
27+
ONE_SECOND = 1
2528

2629

2730
def send_adaptive_card(webhook_url: str, card: dict) -> requests.Response:
@@ -42,8 +45,10 @@ def send_adaptive_card(webhook_url: str, card: dict) -> requests.Response:
4245
headers={"Content-Type": "application/json"},
4346
)
4447
response.raise_for_status()
45-
if response.status_code == 202:
46-
logger.debug("Got 202 response from Teams webhook, assuming success")
48+
if response.status_code == HTTPStatus.ACCEPTED:
49+
logger.debug(
50+
f"Got {HTTPStatus.ACCEPTED} response from Teams webhook, assuming success"
51+
)
4752
return response
4853

4954

@@ -56,22 +61,36 @@ def __init__(self, url: str) -> None:
5661
def parse_message_context(self, context: dict[str, Any]) -> EmptyMessageContext:
5762
return EmptyMessageContext(**context)
5863

64+
@sleep_and_retry
65+
@limits(calls=1, period=ONE_SECOND)
5966
def send_message(
6067
self,
6168
destination: None,
6269
body: MessageBody,
6370
) -> MessageSendResult[EmptyMessageContext]:
6471
card = format_adaptive_card(body)
6572
try:
66-
send_adaptive_card(self.url, card)
73+
response = send_adaptive_card(self.url, card)
74+
# For the old teams webhook version of Teams simply returning status code 200
75+
# is not indicating that it was successful.
76+
# In that version they return some text if it was NOT successful, otherwise
77+
# they return the number 1 in the text. For the new teams webhook version they always
78+
# return a 202 and nothing else can be used to determine if the message was
79+
# sent successfully.
80+
if response.status_code not in (HTTPStatus.OK, HTTPStatus.ACCEPTED) or (
81+
response.status_code == HTTPStatus.OK and len(response.text) > 1
82+
):
83+
raise MessagingIntegrationError(
84+
f"Could not post message to Teams via webhook. Status code: {response.status_code}, Error: {response.text}"
85+
)
6786
return MessageSendResult(
6887
message_context=EmptyMessageContext(),
6988
timestamp=datetime.utcnow(),
7089
message_format="adaptive_cards",
7190
)
7291
except requests.RequestException as e:
7392
raise MessagingIntegrationError(
74-
"Failed to send message to Teams webhook"
93+
f"An error occurred while posting message to Teams webhook: {str(e)}"
7594
) from e
7695

7796
def supports_reply(self) -> bool:

elementary/monitor/alerts/alert_messages/builder.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656

5757
class MessageBuilderConfig(BaseModel):
5858
alert_groups_subscribers: bool = False
59+
maximum_columns_in_alert_samples: int = 4
5960

6061

6162
class AlertMessageBuilder:
@@ -343,7 +344,8 @@ def _get_result_blocks(
343344
elif result_sample:
344345
if (
345346
isinstance(result_sample, list)
346-
and len(result_sample[0].keys()) <= 4
347+
and len(result_sample[0].keys())
348+
<= self.config.maximum_columns_in_alert_samples
347349
):
348350
result_blocks.append(
349351
TableBlock.from_dicts(result_sample),

elementary/monitor/cli.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,13 @@ def get_cli_properties() -> dict:
285285
default=None,
286286
help="A Microsoft Teams webhook URL for sending alerts to a specific channel in Teams.",
287287
)
288+
@click.option(
289+
"--maximum-columns-in-alert-samples",
290+
"-mc",
291+
type=int,
292+
default=4,
293+
help="Maximum number of columns to display as a table in alert samples. Above this, the output is shown as raw JSON.",
294+
)
288295
@click.pass_context
289296
def monitor(
290297
ctx,
@@ -316,6 +323,7 @@ def monitor(
316323
filters,
317324
excludes,
318325
teams_webhook,
326+
maximum_columns_in_alert_samples,
319327
):
320328
"""
321329
Get alerts on failures in dbt jobs.
@@ -347,6 +355,7 @@ def monitor(
347355
slack_group_alerts_by=group_by,
348356
report_url=report_url,
349357
teams_webhook=teams_webhook,
358+
maximum_columns_in_alert_samples=maximum_columns_in_alert_samples,
350359
)
351360
anonymous_tracking = AnonymousCommandLineTracking(config)
352361
anonymous_tracking.set_env("use_select", bool(select))

elementary/monitor/data_monitoring/alerts/data_monitoring_alerts.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@
2222
from elementary.messages.messaging_integrations.exceptions import (
2323
MessagingIntegrationError,
2424
)
25-
from elementary.monitor.alerts.alert_messages.builder import AlertMessageBuilder
25+
from elementary.monitor.alerts.alert_messages.builder import (
26+
AlertMessageBuilder,
27+
MessageBuilderConfig,
28+
)
2629
from elementary.monitor.alerts.alerts_groups import GroupedByTableAlerts
2730
from elementary.monitor.alerts.alerts_groups.alerts_group import AlertsGroup
2831
from elementary.monitor.alerts.grouping_type import GroupingType
@@ -124,6 +127,11 @@ def __init__(
124127
self.send_test_message_on_success = send_test_message_on_success
125128
self.override_config_defaults = override_config
126129
self.alerts_integration = self._get_integration_client()
130+
self.alert_message_builder = AlertMessageBuilder(
131+
MessageBuilderConfig(
132+
maximum_columns_in_alert_samples=self.config.maximum_columns_in_alert_samples
133+
)
134+
)
127135

128136
def _get_integration_client(
129137
self,
@@ -346,8 +354,7 @@ def _send_alert(
346354
) -> bool:
347355
if isinstance(self.alerts_integration, BaseIntegration):
348356
return self.alerts_integration.send_alert(alert)
349-
alert_message_builder = AlertMessageBuilder()
350-
alert_message_body = alert_message_builder.build(
357+
alert_message_body = self.alert_message_builder.build(
351358
alert=alert,
352359
)
353360
alert_message_body = _add_elementary_attribution(alert_message_body)

elementary/monitor/dbt_project/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ target/
33
dbt_modules/
44
dbt_packages*/
55
logs/
6+
dbt_internal_packages/

tests/unit/alerts/alert_messages/fixtures/adaptive_card/alerts_group_model-errors-False_test-failures-False_test-warnings-True_test-errors-True_link-False_env-False_subscribers-True.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,5 +148,8 @@
148148
]
149149
}
150150
],
151-
"version": "1.5"
151+
"version": "1.5",
152+
"msteams": {
153+
"width": "Full"
154+
}
152155
}

tests/unit/alerts/alert_messages/fixtures/adaptive_card/alerts_group_model-errors-False_test-failures-True_test-warnings-False_test-errors-False_link-False_env-False_subscribers-True.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,5 +87,8 @@
8787
]
8888
}
8989
],
90-
"version": "1.5"
90+
"version": "1.5",
91+
"msteams": {
92+
"width": "Full"
93+
}
9194
}

tests/unit/alerts/alert_messages/fixtures/adaptive_card/alerts_group_model-errors-False_test-failures-True_test-warnings-False_test-errors-True_link-False_env-False_subscribers-True.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,5 +148,8 @@
148148
]
149149
}
150150
],
151-
"version": "1.5"
151+
"version": "1.5",
152+
"msteams": {
153+
"width": "Full"
154+
}
152155
}

0 commit comments

Comments
 (0)