Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/oss/deployment-and-configuration/slack.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,15 @@ The alert format is:
```

---

## Full-width alerts

By default, Slack alerts use a narrower layout with some content in attachments. To use the full message width and show test results as a markdown table in the main message body, pass the flag when running the monitor:

```shell
edr monitor --slack-token <your_slack_token> --slack-channel-name <channel> --slack-full-width
```

With `--slack-full-width`, alerts are sent using Slack Block Kit in the main message body instead of attachments, so the full channel width is used and test result samples appear as formatted markdown tables.

---
Comment on lines +56 to +67
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Capitalize "Markdown" as a proper noun.

Static analysis correctly flags that "markdown" should be "Markdown" (lines 59 and 65).

Proposed fix
-By default, Slack alerts use a narrower layout with some content in attachments. To use the full message width and show test results as a markdown table in the main message body, pass the flag when running the monitor:
+By default, Slack alerts use a narrower layout with some content in attachments. To use the full message width and show test results as a Markdown table in the main message body, pass the flag when running the monitor:
-With `--slack-full-width`, alerts are sent using Slack Block Kit in the main message body instead of attachments, so the full channel width is used and test result samples appear as formatted markdown tables.
+With `--slack-full-width`, alerts are sent using Slack Block Kit in the main message body instead of attachments, so the full channel width is used and test result samples appear as formatted Markdown tables.
🧰 Tools
🪛 LanguageTool

[uncategorized] ~59-~59: Did you mean the formatting language “Markdown” (= proper noun)?
Context: ...essage width and show test results as a markdown table in the main message body, pass th...

(MARKDOWN_NNP)


[uncategorized] ~65-~65: Did you mean the formatting language “Markdown” (= proper noun)?
Context: ...test result samples appear as formatted markdown tables. ---

(MARKDOWN_NNP)

🤖 Prompt for AI Agents
In `@docs/oss/deployment-and-configuration/slack.mdx` around lines 56 - 67, Change
the lowercase "markdown" occurrences to the proper noun "Markdown" in the Slack
docs section that describes full-width alerts (the paragraph referencing
`--slack-full-width` and the sentence about test result samples). Locate the two
instances where "markdown table" and "markdown tables" are used and update them
to "Markdown table" and "Markdown tables" respectively so the term is
capitalized consistently.

2 changes: 2 additions & 0 deletions docs/oss/guides/alerts/send-slack-alerts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ Make sure to run the following command after your dbt runs and tests:
edr monitor --slack-token <your_slack_token> --slack-channel-name <slack_channel_to_post_at> --group-by [table | alert]
```

Add `--slack-full-width` to use the full message width and show test results as markdown tables. See [Slack setup - Full-width alerts](/oss/deployment-and-configuration/slack#full-width-alerts).

Comment on lines +27 to +28
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Same capitalization nit: "markdown" → "Markdown".

Proposed fix
-Add `--slack-full-width` to use the full message width and show test results as markdown tables. See [Slack setup - Full-width alerts](/oss/deployment-and-configuration/slack#full-width-alerts).
+Add `--slack-full-width` to use the full message width and show test results as Markdown tables. See [Slack setup - Full-width alerts](/oss/deployment-and-configuration/slack#full-width-alerts).
📝 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
Add `--slack-full-width` to use the full message width and show test results as markdown tables. See [Slack setup - Full-width alerts](/oss/deployment-and-configuration/slack#full-width-alerts).
Add `--slack-full-width` to use the full message width and show test results as Markdown tables. See [Slack setup - Full-width alerts](/oss/deployment-and-configuration/slack#full-width-alerts).
🧰 Tools
🪛 LanguageTool

[uncategorized] ~27-~27: Did you mean the formatting language “Markdown” (= proper noun)?
Context: ... message width and show test results as markdown tables. See [Slack setup - Full-width a...

(MARKDOWN_NNP)

🤖 Prompt for AI Agents
In `@docs/oss/guides/alerts/send-slack-alerts.mdx` around lines 27 - 28, Replace
the lowercase word "markdown" with capitalized "Markdown" in the sentence that
mentions `--slack-full-width` so the doc reads "show test results as Markdown
tables"; update the phrase near the `--slack-full-width` reference to use the
corrected capitalization.

Or just `edr monitor` if you used `config.yml`. Please note that when you specify the --slack-channel-name, it's the
default channel name to which all the alerts will be sent that are not attributed to any custom channel. Therefore,
if you execute several `edr monitor` commands at the same time with different `slack-channel-name` arguments, they can
Expand Down
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 @@ -144,6 +145,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
8 changes: 8 additions & 0 deletions elementary/monitor/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,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 @@ -331,6 +337,7 @@ def monitor(
teams_webhook,
maximum_columns_in_alert_samples,
quiet_logs,
slack_full_width,
):
"""
Get alerts on failures in dbt jobs.
Expand Down Expand Up @@ -365,6 +372,7 @@ def monitor(
teams_webhook=teams_webhook,
maximum_columns_in_alert_samples=maximum_columns_in_alert_samples,
quiet_logs=quiet_logs,
slack_full_width=slack_full_width,
)
anonymous_tracking = AnonymousCommandLineTracking(config)
anonymous_tracking.set_env("use_select", bool(select))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def get_integration(
tracking: Optional[Tracking] = None,
) -> Union[BaseMessagingIntegration, BaseIntegration]:
if config.has_slack:
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 @@ -27,16 +27,23 @@ 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:
# Empty rich_text block forces Slack to use full message width for following
# blocks instead of the narrower attachment-style layout.
self._add_always_displayed_blocks([{"type": "rich_text", "elements": []}])
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 @@ -47,15 +54,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:
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)

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,16 @@ def _get_dbt_test_template(
TEST_RESULTS_SAMPLE_FIELD in (alert.alert_fields or DEFAULT_ALERT_FIELDS)
and alert.test_rows_sample
):
test_rows_sample_table = list_of_dicts_to_markdown_table(
alert.test_rows_sample
)
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}```"
Comment on lines +225 to +234
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find and examine get_limited_markdown_msg function
rg -n --type=py -A 15 'def get_limited_markdown_msg'

Repository: elementary-data/elementary

Length of output: 1539


🏁 Script executed:

#!/bin/bash
# Get the constants and more context
rg -n --type=py 'CONTINUATION_SYMBOL|_LONGEST_MARKDOWN_SUFFIX_LEN' elementary/clients/slack/slack_message_builder.py | head -20

# Also check the lines in the slack.py around 248-252 to see test query handling
sed -n '240,260p' elementary/monitor/data_monitoring/alerts/integrations/slack/slack.py

Repository: elementary-data/elementary

Length of output: 1347


🏁 Script executed:

#!/bin/bash
# Check the create_text_section_block implementation
rg -n --type=py -A 5 'def create_text_section_block' elementary/clients/slack/slack_message_builder.py

# Also get the full context around lines 225-234 in slack.py
sed -n '215,240p' elementary/monitor/data_monitoring/alerts/integrations/slack/slack.py

Repository: elementary-data/elementary

Length of output: 1294


🏁 Script executed:

#!/bin/bash
# Let's see what SectionBlock.text_max_length is and understand the full flow
rg -n --type=py 'text_max_length|class SectionBlock' | head -20

# Also check if there are other similar patterns for graceful handling
rg -n --type=py 'The test query was too long' -B 2 -A 2

Repository: elementary-data/elementary

Length of output: 1952


🏁 Script executed:

#!/bin/bash
# Let's examine the list_of_dicts_to_markdown_table function to understand table structure
rg -n --type=py -A 15 'def list_of_dicts_to_markdown_table' 

# Also check if there's any handling for table size elsewhere
rg -n --type=py 'test_rows_sample_table' elementary/monitor/data_monitoring/alerts/integrations/slack/slack.py -B 2 -A 8

Repository: elementary-data/elementary

Length of output: 1957


🏁 Script executed:

#!/bin/bash
# Let's check if there's any test for how get_limited_markdown_msg handles tables
rg -n --type=py 'get_limited_markdown_msg' tests/ -B 2 -A 5

# Also check the implementation once more to understand the exact truncation
sed -n '58,69p' elementary/clients/slack/slack_message_builder.py

# Check what happens with markdown in truncation
sed -n '20,25p' elementary/clients/slack/slack_message_builder.py

Repository: elementary-data/elementary

Length of output: 2240


Markdown table truncation can produce malformed output — consider graceful handling for large tables.

list_of_dicts_to_markdown_table output is wrapped in a code block and passed to create_text_section_block, which calls get_limited_markdown_msg. This function truncates at exactly 3000 characters without respecting markdown structure. A table with many rows/columns will be cut mid-row, producing malformed markdown (e.g., unclosed pipes).

The test query handling (lines 248–252) shows a better pattern: detect when content exceeds the limit and provide an alternative explanation or note like "(truncated)" rather than rendering broken markdown. Consider applying a similar approach to tables.

🤖 Prompt for AI Agents
In `@elementary/monitor/data_monitoring/alerts/integrations/slack/slack.py` around
lines 225 - 234, The markdown table from list_of_dicts_to_markdown_table can be
truncated mid-row by get_limited_markdown_msg causing malformed markdown; update
the Slack message construction in slack.py (the code that calls
create_text_section_block with the code-fenced test_rows_sample) to first check
the full table length/character limit using the same logic as the test-query
branch, and if it exceeds the limit, either (a) render a safe summary by
converting only the first N rows (or columns) and append a clear "(truncated)"
note, or (b) replace the code block with a short text section that says the
table was truncated and provide row/column counts; ensure this uses
list_of_dicts_to_markdown_table for the sampled subset and then pass the safe
string to create_text_section_block/get_limited_markdown_msg so no markdown is
cut mid-structure.

),
]
)
Expand All @@ -235,7 +240,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 +337,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 Down Expand Up @@ -1194,7 +1204,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
37 changes: 36 additions & 1 deletion elementary/utils/json_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import json
import math
from typing import Any, List, Optional, Union
from typing import Any, Dict, List, Optional, Union

from tabulate import tabulate


def try_load_json(value: Optional[Union[str, dict, list]]):
Expand Down Expand Up @@ -94,3 +96,36 @@ def inf_and_nan_to_str(obj) -> Any:
return [inf_and_nan_to_str(i) for i in obj]
else:
return obj


def _format_value(value: Any) -> str:
"""Format a value for table display, avoiding scientific notation for floats."""
if value is None:
return ""
if isinstance(value, float):
if math.isinf(value) or math.isnan(value):
return str(value)
# Format floats without scientific notation
if value == int(value) and abs(value) < 1e15:
return str(int(value))
return f"{value:.10f}".rstrip("0").rstrip(".")
return str(value)


def list_of_dicts_to_markdown_table(data: List[Dict[str, Any]]) -> str:
"""
Convert a list of dictionaries with consistent keys to a markdown table string.

Args:
data: List of dictionaries

Returns:
A markdown-formatted table string using GitHub table format
"""
if not data:
return ""

processed_data = [{k: _format_value(v) for k, v in row.items()} for row in data]
return tabulate(
processed_data, headers="keys", tablefmt="github", disable_numparse=True
)
Loading