Skip to content
6 changes: 6 additions & 0 deletions elementary/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ def __init__(
report_url: Optional[str] = None,
teams_webhook: Optional[str] = None,
maximum_columns_in_alert_samples: Optional[int] = None,
slack_full_width: Optional[bool] = None,
env: str = DEFAULT_ENV,
run_dbt_deps_if_needed: Optional[bool] = None,
project_name: Optional[str] = None,
Expand Down Expand Up @@ -145,6 +146,11 @@ def __init__(
slack_config.get("group_alerts_threshold"),
self.DEFAULT_GROUP_ALERTS_THRESHOLD,
)
self.slack_full_width = self._first_not_none(
slack_full_width,
slack_config.get("full_width"),
False,
)

teams_config = config.get(self._TEAMS, {})
self.teams_webhook = self._first_not_none(
Expand Down
4 changes: 3 additions & 1 deletion elementary/messages/formats/block_kit.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,9 @@ def _add_table_block(self, block: TableBlock) -> None:
new_headers = [
self._format_table_cell(cell, column_count) for cell in block.headers
]
table_text = tabulate(new_rows, headers=new_headers, tablefmt="simple")
table_text = tabulate(
new_rows, headers=new_headers, tablefmt="simple", disable_numparse=True
)
self._add_block(self._format_markdown_section(f"```{table_text}```"))

def _add_actions_block(self, block: ActionsBlock) -> None:
Expand Down
7 changes: 6 additions & 1 deletion elementary/messages/formats/markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,12 @@ def format_fact_list_block(self, block: FactListBlock) -> str:

def format_table_block(self, block: TableBlock) -> str:
if self._table_style == TableStyle.TABULATE:
table = tabulate(block.rows, headers=block.headers, tablefmt="simple")
table = tabulate(
block.rows,
headers=block.headers,
tablefmt="simple",
disable_numparse=True,
)
return f"```\n{table}\n```"
elif self._table_style == TableStyle.JSON:
dicts = [
Expand Down
7 changes: 6 additions & 1 deletion elementary/messages/formats/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,12 @@ def format_fact_list_block(self, block: FactListBlock) -> str:

def format_table_block(self, block: TableBlock) -> str:
if self._table_style == TableStyle.TABULATE:
return tabulate(block.rows, headers=block.headers, tablefmt="simple")
return tabulate(
block.rows,
headers=block.headers,
tablefmt="simple",
disable_numparse=True,
)
elif self._table_style == TableStyle.JSON:
dicts = [
{header: cell for header, cell in zip(block.headers, row)}
Expand Down
7 changes: 7 additions & 0 deletions elementary/monitor/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,12 @@ def get_cli_properties() -> dict:
default=4,
help="Maximum number of columns to display as a table in alert samples. Above this, the output is shown as raw JSON.",
)
@click.option(
"--slack-full-width",
is_flag=True,
default=False,
help="When set, Slack alerts use rich text to achieve full message width instead of the default narrower layout with attachments.",
)
@click.pass_context
def monitor(
ctx,
Expand Down Expand Up @@ -341,6 +347,7 @@ def monitor(
maximum_columns_in_alert_samples,
quiet_logs,
ssl_ca_bundle,
slack_full_width,
):
"""
Get alerts on failures in dbt jobs.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def get_integration(
) -> Union[BaseMessagingIntegration, BaseIntegration]:
if config.has_slack:
ssl_context = create_ssl_context(config.ssl_ca_bundle)
if config.is_slack_workflow:
if config.is_slack_workflow or config.slack_full_width:
return SlackIntegration(
config=config,
tracking=tracking,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,36 @@ class SlackAlertMessageSchema(BaseModel):


class SlackAlertMessageBuilder(SlackMessageBuilder):
def __init__(self) -> None:
def __init__(self, full_width: bool = False) -> None:
super().__init__()
self.full_width = full_width

def get_slack_message(
self,
alert_schema: SlackAlertMessageSchema,
) -> SlackMessageSchema:
if self.full_width:
# A rich_text block at the start forces Slack to use full message width
# for following blocks instead of the narrower attachment-style layout.
# The elements array must be non-empty per Slack Block Kit API.
self._add_always_displayed_blocks(
[
{
"type": "rich_text",
"elements": [
{
"type": "rich_text_section",
"elements": [{"type": "text", "text": " "}],
}
],
}
]
)
self.add_title_to_slack_alert(alert_schema.title)
self.add_preview_to_slack_alert(alert_schema.preview)
self.add_details_to_slack_alert(alert_schema.details)
if self.full_width:
self.slack_message["attachments"] = []
return super().get_slack_message()

def add_title_to_slack_alert(self, title_blocks: Optional[SlackBlocksType] = None):
Expand All @@ -46,15 +66,23 @@ def add_title_to_slack_alert(self, title_blocks: Optional[SlackBlocksType] = Non
def add_preview_to_slack_alert(
self, preview_blocks: Optional[SlackBlocksType] = None
):
if preview_blocks:
validated_preview_blocks = self._validate_preview_blocks(preview_blocks)
if not preview_blocks:
return
validated_preview_blocks = self._validate_preview_blocks(preview_blocks)
if self.full_width:
self._add_always_displayed_blocks(validated_preview_blocks)
else:
self._add_blocks_as_attachments(validated_preview_blocks)
Comment on lines +69 to 75
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Skip attachment-era preview validation in full_width mode.

Line 71 still runs _validate_preview_blocks() before the full_width branch. That keeps the old 5-block cap and adds padding blocks even when the preview is rendered in top-level blocks, so full-width alerts can still raise PreviewIsTooLongError and end up with extra blank space.

Suggested fix
     if not preview_blocks:
         return
-    validated_preview_blocks = self._validate_preview_blocks(preview_blocks)
     if self.full_width:
-        self._add_always_displayed_blocks(validated_preview_blocks)
+        self._add_always_displayed_blocks(preview_blocks)
     else:
+        validated_preview_blocks = self._validate_preview_blocks(preview_blocks)
         self._add_blocks_as_attachments(validated_preview_blocks)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if not preview_blocks:
return
validated_preview_blocks = self._validate_preview_blocks(preview_blocks)
if self.full_width:
self._add_always_displayed_blocks(validated_preview_blocks)
else:
self._add_blocks_as_attachments(validated_preview_blocks)
if not preview_blocks:
return
if self.full_width:
self._add_always_displayed_blocks(preview_blocks)
else:
validated_preview_blocks = self._validate_preview_blocks(preview_blocks)
self._add_blocks_as_attachments(validated_preview_blocks)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@elementary/monitor/data_monitoring/alerts/integrations/slack/message_builder.py`
around lines 69 - 75, The preview block validation (_validate_preview_blocks) is
being called unconditionally before branching on full_width, which enforces the
attachment-era 5-block cap and padding even for top-level blocks; change the
logic so that when self.full_width is True you skip calling
_validate_preview_blocks and directly call
_add_always_displayed_blocks(preview_blocks), and only call
_validate_preview_blocks(preview_blocks) when taking the else branch that calls
_add_blocks_as_attachments(validated_preview_blocks); remove or relocate the
earlier unconditional call to _validate_preview_blocks to avoid raising
PreviewIsTooLongError or adding padding for full-width previews.


def add_details_to_slack_alert(
self,
detail_blocks: Optional[SlackBlocksType] = None,
):
if detail_blocks:
if not detail_blocks:
return
if self.full_width:
self._add_always_displayed_blocks(detail_blocks)
else:
self._add_blocks_as_attachments(detail_blocks)

@classmethod
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from elementary.clients.slack.schema import SlackBlocksType, SlackMessageSchema
from elementary.clients.slack.slack_message_builder import MessageColor
from elementary.config.config import Config
from elementary.messages.blocks import Icon
from elementary.messages.formats.unicode import ICON_TO_UNICODE
from elementary.monitor.alerts.alerts_groups import AlertsGroup, GroupedByTableAlerts
from elementary.monitor.alerts.alerts_groups.base_alerts_group import BaseAlertsGroup
from elementary.monitor.alerts.model_alert import ModelAlertModel
Expand All @@ -26,6 +28,7 @@
)
from elementary.tracking.tracking_interface import Tracking
from elementary.utils.json_utils import (
list_of_dicts_to_markdown_table,
list_of_lists_of_strings_to_comma_delimited_unique_strings,
)
from elementary.utils.log import get_logger
Expand Down Expand Up @@ -78,7 +81,9 @@ def __init__(
self.config = config
self.tracking = tracking
self.override_config_defaults = override_config_defaults
self.message_builder = SlackAlertMessageBuilder()
self.message_builder = SlackAlertMessageBuilder(
full_width=config.slack_full_width
)
super().__init__()

# Enforce typing
Expand Down Expand Up @@ -116,7 +121,10 @@ def _get_dbt_test_template(
title = [
self.message_builder.create_header_block(
f"{self._get_display_name(alert.status)}: {alert.summary}"
)
),
self.message_builder.create_text_section_block(
"Powered by <https://www.elementary-data.com/|Elementary>"
),
]
if alert.suppression_interval:
title.extend(
Expand Down Expand Up @@ -165,8 +173,11 @@ def _get_dbt_test_template(
)

compacted_sections = []
if COLUMN_FIELD in (alert.alert_fields or DEFAULT_ALERT_FIELDS):
compacted_sections.append(f"*Column*\n{alert.column_name or '_No column_'}")
if (
COLUMN_FIELD in (alert.alert_fields or DEFAULT_ALERT_FIELDS)
and alert.column_name
):
compacted_sections.append(f"*Column*\n{alert.column_name}")
if TAGS_FIELD in (alert.alert_fields or DEFAULT_ALERT_FIELDS):
tags = prettify_and_dedup_list(alert.tags or [])
compacted_sections.append(f"*Tags*\n{tags or '_No tags_'}")
Expand All @@ -186,21 +197,12 @@ def _get_dbt_test_template(
)

if DESCRIPTION_FIELD in (alert.alert_fields or DEFAULT_ALERT_FIELDS):
if alert.test_description:
preview.extend(
[
self.message_builder.create_text_section_block("*Description*"),
self.message_builder.create_context_block(
[alert.test_description]
),
]
)
else:
preview.append(
self.message_builder.create_text_section_block(
"*Description*\n_No description_"
)
description_text = alert.test_description or "_No description_"
preview.append(
self.message_builder.create_text_section_block(
f"*Description*\n{description_text}"
)
)

result = []
if (
Expand All @@ -209,7 +211,7 @@ def _get_dbt_test_template(
):
result.extend(
[
self.message_builder.create_context_block(["*Result message*"]),
self.message_builder.create_text_section_block("*Result message*"),
self.message_builder.create_text_section_block(
f"```{alert.error_message.strip()}```"
),
Expand All @@ -220,13 +222,17 @@ def _get_dbt_test_template(
TEST_RESULTS_SAMPLE_FIELD in (alert.alert_fields or DEFAULT_ALERT_FIELDS)
and alert.test_rows_sample
):
table_max_length = SectionBlock.text_max_length - 6
test_rows_sample_table = list_of_dicts_to_markdown_table(
alert.test_rows_sample, max_length=table_max_length
)
result.extend(
[
self.message_builder.create_context_block(
["*Test results sample*"]
self.message_builder.create_text_section_block(
f"{ICON_TO_UNICODE[Icon.MAGNIFYING_GLASS]} *Test results sample*"
),
self.message_builder.create_text_section_block(
f"```{alert.test_rows_sample}```"
f"```{test_rows_sample_table}```"
),
]
)
Expand All @@ -235,7 +241,9 @@ def _get_dbt_test_template(
TEST_QUERY_FIELD in (alert.alert_fields or DEFAULT_ALERT_FIELDS)
and alert.test_results_query
):
result.append(self.message_builder.create_context_block(["*Test query*"]))
result.append(
self.message_builder.create_text_section_block("*Test query*")
)

msg = f"```{alert.test_results_query}```"
if len(msg) > SectionBlock.text_max_length:
Expand Down Expand Up @@ -330,8 +338,11 @@ def _get_elementary_test_template(
)

compacted_sections = []
if COLUMN_FIELD in (alert.alert_fields or DEFAULT_ALERT_FIELDS):
compacted_sections.append(f"*Column*\n{alert.column_name or '_No column_'}")
if (
COLUMN_FIELD in (alert.alert_fields or DEFAULT_ALERT_FIELDS)
and alert.column_name
):
compacted_sections.append(f"*Column*\n{alert.column_name}")
if TAGS_FIELD in (alert.alert_fields or DEFAULT_ALERT_FIELDS):
tags = prettify_and_dedup_list(alert.tags or [])
compacted_sections.append(f"*Tags*\n{tags or '_No tags_'}")
Expand All @@ -351,21 +362,12 @@ def _get_elementary_test_template(
)

if DESCRIPTION_FIELD in (alert.alert_fields or DEFAULT_ALERT_FIELDS):
if alert.test_description:
preview.extend(
[
self.message_builder.create_text_section_block("*Description*"),
self.message_builder.create_context_block(
[alert.test_description]
),
]
)
else:
preview.append(
self.message_builder.create_text_section_block(
"*Description*\n_No description_"
)
description_text = alert.test_description or "_No description_"
preview.append(
self.message_builder.create_text_section_block(
f"*Description*\n{description_text}"
)
)

result = []
if (
Expand Down Expand Up @@ -1194,7 +1196,9 @@ def _create_single_alert_details_blocks(
if result:
details_blocks.extend(
[
self.message_builder.create_text_section_block(":mag: *Result*"),
self.message_builder.create_text_section_block(
f"{ICON_TO_UNICODE[Icon.INFO]} *Details*"
),
self.message_builder.create_divider_block(),
*result,
]
Expand Down
Loading
Loading