Skip to content
Merged
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
7 changes: 7 additions & 0 deletions elementary/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ def __init__(
azure_container_name: Optional[str] = None,
report_url: Optional[str] = None,
teams_webhook: Optional[str] = None,
maximum_columns_in_alert_samples: Optional[int] = None,
env: str = DEFAULT_ENV,
run_dbt_deps_if_needed: Optional[bool] = None,
project_name: Optional[str] = None,
Expand Down Expand Up @@ -103,6 +104,12 @@ def __init__(
False,
)

self.maximum_columns_in_alert_samples = self._first_not_none(
maximum_columns_in_alert_samples,
config.get("maximum_columns_in_alert_samples"),
4,
)

self.timezone = self._first_not_none(
timezone,
config.get("timezone"),
Expand Down
4 changes: 3 additions & 1 deletion elementary/messages/formats/adaptive_cards.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@ def format_table_block(block: TableBlock) -> Dict[str, Any]:
"items": [
{
"type": "TextBlock",
"text": str(cell),
"text": str(cell) if cell is not None else "",
"wrap": True,
}
],
}
Expand Down Expand Up @@ -256,4 +257,5 @@ def format_adaptive_card(message: MessageBody, version: str = "1.5") -> Dict[str
"type": "AdaptiveCard",
"body": format_adaptive_card_body(message),
"version": version,
"msteams": {"width": "Full"},
}
27 changes: 23 additions & 4 deletions elementary/messages/messaging_integrations/teams_webhook.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from datetime import datetime
from http import HTTPStatus
from typing import Any, Optional

import requests
from ratelimit import limits, sleep_and_retry
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Rate limiting import added — verify dependency is declared/pinned.
Ensure ratelimit is listed in your dependency files (and pinned) to avoid runtime ImportError.


🏁 Script executed:

#!/bin/bash
# Verify ratelimit is declared in common dependency files
fd -a '(^requirements.*\.txt$|^pyproject\.toml$|^setup\.(cfg|py)$)' -HI \
  | xargs -I{} sh -c 'echo "--- {} ---"; rg -n "ratelimit" "{}" || true'

Length of output: 207


Pin ratelimit dependency to a specific version
Replace ratelimit = "*" in pyproject.toml (line 22) with a fixed version specifier (e.g. ratelimit = "==1.0.0") to ensure reproducible builds.

🤖 Prompt for AI Agents
In elementary/messages/messaging_integrations/teams_webhook.py around line 6,
the project currently depends on a non-pinned ratelimit version; update
pyproject.toml (line 22) to replace ratelimit = "*" with a specific version pin
such as ratelimit = "==1.0.0" (or the tested working version), then run your
dependency lock/update command (poetry lock or pip-compile) and install to
refresh the lockfile and verify imports and tests still pass.

from typing_extensions import TypeAlias

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


Channel: TypeAlias = Optional[str]
ONE_SECOND = 1


def send_adaptive_card(webhook_url: str, card: dict) -> requests.Response:
Expand All @@ -42,8 +45,10 @@ def send_adaptive_card(webhook_url: str, card: dict) -> requests.Response:
headers={"Content-Type": "application/json"},
)
response.raise_for_status()
if response.status_code == 202:
logger.debug("Got 202 response from Teams webhook, assuming success")
if response.status_code == HTTPStatus.ACCEPTED:
logger.debug(
f"Got {HTTPStatus.ACCEPTED} response from Teams webhook, assuming success"
)
return response


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

@sleep_and_retry
@limits(calls=1, period=ONE_SECOND)
def send_message(
self,
destination: None,
body: MessageBody,
) -> MessageSendResult[EmptyMessageContext]:
card = format_adaptive_card(body)
try:
send_adaptive_card(self.url, card)
response = send_adaptive_card(self.url, card)
# For the old teams webhook version of Teams simply returning status code 200
# is not indicating that it was successful.
# In that version they return some text if it was NOT successful, otherwise
# they return the number 1 in the text. For the new teams webhook version they always
# return a 202 and nothing else can be used to determine if the message was
# sent successfully.
if response.status_code not in (HTTPStatus.OK, HTTPStatus.ACCEPTED) or (
response.status_code == HTTPStatus.OK and len(response.text) > 1
):
raise MessagingIntegrationError(
f"Could not post message to Teams via webhook. Status code: {response.status_code}, Error: {response.text}"
)
return MessageSendResult(
message_context=EmptyMessageContext(),
timestamp=datetime.utcnow(),
message_format="adaptive_cards",
)
except requests.RequestException as e:
raise MessagingIntegrationError(
"Failed to send message to Teams webhook"
f"An error occurred while posting message to Teams webhook: {str(e)}"
) from e

def supports_reply(self) -> bool:
Expand Down
4 changes: 3 additions & 1 deletion elementary/monitor/alerts/alert_messages/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@

class MessageBuilderConfig(BaseModel):
alert_groups_subscribers: bool = False
maximum_columns_in_alert_samples: int = 4


class AlertMessageBuilder:
Expand Down Expand Up @@ -343,7 +344,8 @@ def _get_result_blocks(
elif result_sample:
if (
isinstance(result_sample, list)
and len(result_sample[0].keys()) <= 4
and len(result_sample[0].keys())
<= self.config.maximum_columns_in_alert_samples
):
result_blocks.append(
TableBlock.from_dicts(result_sample),
Expand Down
9 changes: 9 additions & 0 deletions elementary/monitor/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,13 @@ def get_cli_properties() -> dict:
default=None,
help="A Microsoft Teams webhook URL for sending alerts to a specific channel in Teams.",
)
@click.option(
"--maximum-columns-in-alert-samples",
"-mc",
type=int,
default=4,
help="Maximum number of columns to display as a table in alert samples. Above this, the output is shown as raw JSON.",
)
Comment on lines +288 to +294
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fix precedence bug: CLI default overrides config; validate range.

With default=4 here, Click always passes 4, shadowing any value from config.yml despite the intended precedence (CLI explicit > config > default 4). Also, negatives should be rejected.

Apply:

 @click.option(
     "--maximum-columns-in-alert-samples",
     "-mc",
-    type=int,
-    default=4,
-    help="Maximum number of columns to display as a table in alert samples. Above this, the output is shown as raw JSON.",
+    type=click.IntRange(min=0),
+    default=None,
+    show_default=4,
+    help="Maximum number of columns to display as a table in alert samples. Above this, the output is shown as raw JSON. Set to 0 to force JSON for all samples.",
 )
📝 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
@click.option(
"--maximum-columns-in-alert-samples",
"-mc",
type=int,
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(
"--maximum-columns-in-alert-samples",
"-mc",
type=click.IntRange(min=0),
default=None,
show_default=4,
help="Maximum number of columns to display as a table in alert samples. Above this, the output is shown as raw JSON. Set to 0 to force JSON for all samples.",
)
🤖 Prompt for AI Agents
In elementary/monitor/cli.py around lines 288 to 294, the Click option currently
sets default=4 which causes the CLI to always pass 4 and override values from
config.yml; change the option to default=None so absence of a CLI flag allows
the config value to take precedence, and validate the value to reject negatives
(e.g., use click.IntRange(min=0) or a callback that raises a BadParameter for
value < 0); also ensure the downstream code interprets None as "use config or
fallback default 4" rather than treating None as 4.

@click.pass_context
def monitor(
ctx,
Expand Down Expand Up @@ -316,6 +323,7 @@ def monitor(
filters,
excludes,
teams_webhook,
maximum_columns_in_alert_samples,
):
"""
Get alerts on failures in dbt jobs.
Expand Down Expand Up @@ -347,6 +355,7 @@ def monitor(
slack_group_alerts_by=group_by,
report_url=report_url,
teams_webhook=teams_webhook,
maximum_columns_in_alert_samples=maximum_columns_in_alert_samples,
)
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 @@ -22,7 +22,10 @@
from elementary.messages.messaging_integrations.exceptions import (
MessagingIntegrationError,
)
from elementary.monitor.alerts.alert_messages.builder import AlertMessageBuilder
from elementary.monitor.alerts.alert_messages.builder import (
AlertMessageBuilder,
MessageBuilderConfig,
)
from elementary.monitor.alerts.alerts_groups import GroupedByTableAlerts
from elementary.monitor.alerts.alerts_groups.alerts_group import AlertsGroup
from elementary.monitor.alerts.grouping_type import GroupingType
Expand Down Expand Up @@ -124,6 +127,11 @@ def __init__(
self.send_test_message_on_success = send_test_message_on_success
self.override_config_defaults = override_config
self.alerts_integration = self._get_integration_client()
self.alert_message_builder = AlertMessageBuilder(
MessageBuilderConfig(
maximum_columns_in_alert_samples=self.config.maximum_columns_in_alert_samples
)
)

def _get_integration_client(
self,
Expand Down Expand Up @@ -346,8 +354,7 @@ def _send_alert(
) -> bool:
if isinstance(self.alerts_integration, BaseIntegration):
return self.alerts_integration.send_alert(alert)
alert_message_builder = AlertMessageBuilder()
alert_message_body = alert_message_builder.build(
alert_message_body = self.alert_message_builder.build(
alert=alert,
)
alert_message_body = _add_elementary_attribution(alert_message_body)
Expand Down
1 change: 1 addition & 0 deletions elementary/monitor/dbt_project/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ target/
dbt_modules/
dbt_packages*/
logs/
dbt_internal_packages/
Original file line number Diff line number Diff line change
Expand Up @@ -148,5 +148,8 @@
]
}
],
"version": "1.5"
"version": "1.5",
"msteams": {
"width": "Full"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,8 @@
]
}
],
"version": "1.5"
"version": "1.5",
"msteams": {
"width": "Full"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -148,5 +148,8 @@
]
}
],
"version": "1.5"
"version": "1.5",
"msteams": {
"width": "Full"
}
Comment on lines +151 to +154
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

LGTM: Teams full-width hint added correctly.

The top-level msteams.width="Full" is valid for Teams and the JSON remains well-formed after the version comma.

Optional: verify all AdaptiveCard fixtures include the new msteams hint when version=1.5.


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Lists JSON fixtures under tests/unit that declare version 1.5 but lack the Teams width hint.
missing=()
while IFS= read -r -d '' f; do
  if rg -q '"version":\s*"1\.5"' "$f"; then
    if ! rg -q '"msteams":\s*\{\s*"width":\s*"Full"\s*\}' "$f"; then
      missing+=("$f")
    fi
  fi
done < <(fd -t f -e json tests/unit | tr '\n' '\0')
printf "%s\n" "${missing[@]:-}" | sort

Length of output: 534


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Lists JSON fixtures under tests/unit that declare version 1.5 but lack the Teams width hint.
missing=()
while IFS= read -r -d '' f; do
  if grep -q '"version":\s*"1\.5"' "$f"; then
    if ! grep -q '"msteams":\s*{[^}]*"width":\s*"Full"' "$f"; then
      missing+=("$f")
    fi
  fi
done < <(find tests/unit -type f -name '*.json' -print0)
printf "%s\n" "${missing[@]:-}" | sort

Length of output: 192


#!/bin/bash
set -euo pipefail
# Find all JSON fixtures under tests/unit and list those that have version "1.5" but lack the msteams width hint
find tests/unit -type f -name '*.json' -exec sh -c '
  for f; do
    if grep -Eq '"version":[[:space:]]*"1\.5"' "$f"; then
      if ! grep -Eq '"msteams":[[:space:]]*{[^}]*"width":[[:space:]]*"Full"' "$f"; then
        echo "$f"
      fi
    fi
  done
' sh {} +

🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Find JSON fixtures under tests/unit with version "1.5" missing msteams.width="Full"
find tests/unit -type f -name '*.json' -exec sh -c '
  f="$1"
  if grep -Eq "\"version\":[[:space:]]*\"1\.5\"" "$f" && \
     ! grep -Eq "\"msteams\":[[:space:]]*{[^}]*\"width\":[[:space:]]*\"Full\"" "$f"; then
    echo "$f"
  fi
' _ {} \;

Length of output: 16609


Ensure all version “1.5” AdaptiveCard fixtures include the Teams width hint
Search across tests/unit JSON fixtures found numerous files declaring "version": "1.5" but missing the top-level

"msteams": { "width": "Full" }

block. Add this hint to every AdaptiveCard fixture with version 1.5.

🤖 Prompt for AI Agents
In
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
around lines 151-154, the AdaptiveCard fixture declares "version": "1.5" but is
missing the required top-level "msteams": { "width": "Full" } hint; add that
exact block as a sibling to "version" (i.e., immediately adjacent at the top
level of the JSON object) so every version "1.5" fixture includes the Teams
width hint and ensure JSON remains valid (commas, ordering).

}
Original file line number Diff line number Diff line change
Expand Up @@ -209,5 +209,8 @@
]
}
],
"version": "1.5"
"version": "1.5",
"msteams": {
"width": "Full"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -148,5 +148,8 @@
]
}
],
"version": "1.5"
"version": "1.5",
"msteams": {
"width": "Full"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -148,5 +148,8 @@
]
}
],
"version": "1.5"
"version": "1.5",
"msteams": {
"width": "Full"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -209,5 +209,8 @@
]
}
],
"version": "1.5"
"version": "1.5",
"msteams": {
"width": "Full"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -209,5 +209,8 @@
]
}
],
"version": "1.5"
"version": "1.5",
"msteams": {
"width": "Full"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -209,5 +209,8 @@
]
}
],
"version": "1.5"
"version": "1.5",
"msteams": {
"width": "Full"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -270,5 +270,8 @@
]
}
],
"version": "1.5"
"version": "1.5",
"msteams": {
"width": "Full"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -270,5 +270,8 @@
]
}
],
"version": "1.5"
"version": "1.5",
"msteams": {
"width": "Full"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -202,5 +202,8 @@
]
}
],
"version": "1.5"
"version": "1.5",
"msteams": {
"width": "Full"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -202,5 +202,8 @@
]
}
],
"version": "1.5"
"version": "1.5",
"msteams": {
"width": "Full"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -202,5 +202,8 @@
]
}
],
"version": "1.5"
"version": "1.5",
"msteams": {
"width": "Full"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -185,5 +185,8 @@
]
}
],
"version": "1.5"
"version": "1.5",
"msteams": {
"width": "Full"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -202,5 +202,8 @@
]
}
],
"version": "1.5"
"version": "1.5",
"msteams": {
"width": "Full"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -202,5 +202,8 @@
]
}
],
"version": "1.5"
"version": "1.5",
"msteams": {
"width": "Full"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -202,5 +202,8 @@
]
}
],
"version": "1.5"
"version": "1.5",
"msteams": {
"width": "Full"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -160,5 +160,8 @@
]
}
],
"version": "1.5"
"version": "1.5",
"msteams": {
"width": "Full"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -185,5 +185,8 @@
]
}
],
"version": "1.5"
"version": "1.5",
"msteams": {
"width": "Full"
}
}
Loading
Loading