Skip to content

Commit f2316b7

Browse files
authored
Merge branch 'master' into ele-4029-slack-message-blocks
2 parents eedc3d5 + a04697a commit f2316b7

97 files changed

Lines changed: 11479 additions & 44 deletions

File tree

Some content is hidden

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

elementary/config/config.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from elementary.monitor.alerts.grouping_type import GroupingType
1111
from elementary.utils.ordered_yaml import OrderedYaml
1212

13+
DEFAULT_ENV = "dev"
14+
1315

1416
class Config:
1517
_SLACK = "slack"
@@ -68,7 +70,7 @@ def __init__(
6870
azure_container_name: Optional[str] = None,
6971
report_url: Optional[str] = None,
7072
teams_webhook: Optional[str] = None,
71-
env: str = "dev",
73+
env: str = DEFAULT_ENV,
7274
run_dbt_deps_if_needed: Optional[bool] = None,
7375
project_name: Optional[str] = None,
7476
):
@@ -249,6 +251,10 @@ def has_gcloud(self):
249251
def has_gcs(self):
250252
return self.gcs_bucket_name and self.has_gcloud
251253

254+
@property
255+
def specified_env(self) -> Optional[str]:
256+
return self.env if self.env != DEFAULT_ENV else None
257+
252258
def validate_monitor(self):
253259
provided_integrations = list(
254260
filter(

elementary/messages/block_builders.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,24 @@ def SummaryLineBlock(
8787
return LineBlock(inlines=text_blocks)
8888

8989

90+
def NonPrimaryFactBlock(fact: Tuple[LineBlock, LineBlock]) -> FactBlock:
91+
title, value = fact
92+
return FactBlock(
93+
title=title,
94+
value=value,
95+
primary=False,
96+
)
97+
98+
99+
def PrimaryFactBlock(fact: Tuple[LineBlock, LineBlock]) -> FactBlock:
100+
title, value = fact
101+
return FactBlock(
102+
title=title,
103+
value=value,
104+
primary=True,
105+
)
106+
107+
90108
def FactsBlock(
91109
*,
92110
facts: Sequence[
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Elementary Messaging Integration System
2+
3+
## Overview
4+
5+
The Elementary Messaging Integration system provides a flexible and extensible framework for sending alerts and messages to various messaging platforms (e.g., Slack, Teams). The system is designed to support a gradual migration from the legacy integration system to a more generic messaging-based approach.
6+
7+
## Architecture
8+
9+
### BaseMessagingIntegration
10+
11+
The core of the new messaging system is the `BaseMessagingIntegration` abstract class. This class defines the contract that all messaging integrations must follow:
12+
13+
- `send_message()`: Send a message to a specific destination
14+
- `supports_reply()`: Check if the integration supports message threading/replies
15+
- `reply_to_message()`: Reply to an existing message (if supported)
16+
17+
### Key Components
18+
19+
1. **MessageBody**: A platform-agnostic representation of a message
20+
2. **MessageSendResult**: Contains information about a sent message, including timestamp and platform-specific context
21+
3. **DestinationType**: Generic type representing the destination for a message (e.g., webhook URL, channel)
22+
4. **MessageContextType**: Generic type for platform-specific message context
23+
24+
## Migration Strategy
25+
26+
The system currently supports both:
27+
28+
- Legacy `BaseIntegration` implementations (e.g., Slack)
29+
- New `BaseMessagingIntegration` implementations (e.g., Teams)
30+
31+
This dual support allows for a gradual migration path where:
32+
33+
1. New integrations are implemented using `BaseMessagingIntegration`
34+
2. Existing integrations can be migrated one at a time
35+
3. The legacy `BaseIntegration` will eventually be deprecated
36+
37+
## Implementing a New Integration
38+
39+
To add a new messaging platform integration:
40+
41+
1. Create a new class that extends `BaseMessagingIntegration`
42+
2. Implement the required abstract methods:
43+
```python
44+
def send_message(self, destination: DestinationType, body: MessageBody) -> MessageSendResult
45+
def supports_reply(self) -> bool
46+
def reply_to_message(self, destination, message_context, message_body) -> MessageSendResult # if supported
47+
```
48+
3. Update the `Integrations` factory class to support the new integration
49+
50+
## Current Implementations
51+
52+
- **Teams**: Uses the new `BaseMessagingIntegration` system with webhook support
53+
- **Slack**: Currently uses the legacy `BaseIntegration` system (planned for migration)
54+
55+
## Future Improvements
56+
57+
1. Complete migration of Slack to `BaseMessagingIntegration`
58+
2. Add support for more messaging platforms

elementary/messages/messaging_integrations/__init__.py

Whitespace-only changes.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from abc import ABC, abstractmethod
2+
from datetime import datetime
3+
from typing import Generic, Optional, TypeVar
4+
5+
from pydantic import BaseModel
6+
7+
from elementary.messages.message_body import MessageBody
8+
from elementary.messages.messaging_integrations.exceptions import (
9+
MessageIntegrationReplyNotSupportedError,
10+
)
11+
from elementary.utils.log import get_logger
12+
13+
logger = get_logger(__name__)
14+
15+
16+
T = TypeVar("T")
17+
18+
19+
class MessageSendResult(BaseModel, Generic[T]):
20+
timestamp: datetime
21+
message_context: Optional[T] = None
22+
23+
24+
DestinationType = TypeVar("DestinationType")
25+
MessageContextType = TypeVar("MessageContextType")
26+
27+
28+
class BaseMessagingIntegration(ABC, Generic[DestinationType, MessageContextType]):
29+
@abstractmethod
30+
def send_message(
31+
self,
32+
destination: DestinationType,
33+
body: MessageBody,
34+
) -> MessageSendResult[MessageContextType]:
35+
raise NotImplementedError
36+
37+
@abstractmethod
38+
def supports_reply(self) -> bool:
39+
raise NotImplementedError
40+
41+
def reply_to_message(
42+
self,
43+
destination: DestinationType,
44+
message_context: MessageContextType,
45+
body: MessageBody,
46+
) -> MessageSendResult[MessageContextType]:
47+
if not self.supports_reply():
48+
raise MessageIntegrationReplyNotSupportedError
49+
raise NotImplementedError
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class MessagingIntegrationError(Exception):
2+
pass
3+
4+
5+
class MessageIntegrationReplyNotSupportedError(MessagingIntegrationError):
6+
pass
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from datetime import datetime
2+
from typing import Optional
3+
4+
import requests
5+
from pydantic import BaseModel
6+
7+
from elementary.messages.formats.adaptive_cards import format_adaptive_card
8+
from elementary.messages.message_body import MessageBody
9+
from elementary.messages.messaging_integrations.base_messaging_integration import (
10+
BaseMessagingIntegration,
11+
MessageSendResult,
12+
)
13+
from elementary.messages.messaging_integrations.exceptions import (
14+
MessageIntegrationReplyNotSupportedError,
15+
MessagingIntegrationError,
16+
)
17+
from elementary.utils.log import get_logger
18+
19+
logger = get_logger(__name__)
20+
21+
22+
class ChannelWebhook(BaseModel):
23+
webhook: str
24+
channel: Optional[str] = None
25+
26+
27+
def send_adaptive_card(webhook_url: str, card: dict) -> requests.Response:
28+
"""Sends an Adaptive Card to the specified webhook URL."""
29+
payload = {
30+
"type": "message",
31+
"attachments": [
32+
{
33+
"contentType": "application/vnd.microsoft.card.adaptive",
34+
"contentUrl": None,
35+
"content": card,
36+
}
37+
],
38+
}
39+
40+
response = requests.post(
41+
webhook_url,
42+
json=payload,
43+
headers={"Content-Type": "application/json"},
44+
)
45+
response.raise_for_status()
46+
if response.status_code == 202:
47+
logger.debug("Got 202 response from Teams webhook, assuming success")
48+
return response
49+
50+
51+
class TeamsWebhookMessagingIntegration(
52+
BaseMessagingIntegration[ChannelWebhook, ChannelWebhook]
53+
):
54+
def send_message(
55+
self,
56+
destination: ChannelWebhook,
57+
body: MessageBody,
58+
) -> MessageSendResult[ChannelWebhook]:
59+
card = format_adaptive_card(body)
60+
try:
61+
send_adaptive_card(destination.webhook, card)
62+
return MessageSendResult(
63+
message_context=destination,
64+
timestamp=datetime.utcnow(),
65+
)
66+
except requests.RequestException as e:
67+
raise MessagingIntegrationError(
68+
"Failed to send message to Teams webhook"
69+
) from e
70+
71+
def supports_reply(self) -> bool:
72+
return False
73+
74+
def reply_to_message(
75+
self,
76+
destination: ChannelWebhook,
77+
message_context: ChannelWebhook,
78+
body: MessageBody,
79+
) -> MessageSendResult[ChannelWebhook]:
80+
raise MessageIntegrationReplyNotSupportedError(
81+
"Teams webhook message integration does not support replying to messages"
82+
)

elementary/monitor/alerts/alert.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def __init__(
3131
report_url: Optional[str] = None,
3232
alert_fields: Optional[List[str]] = None,
3333
elementary_database_and_schema: Optional[str] = None,
34+
env: Optional[str] = None,
3435
**kwargs,
3536
):
3637
self.id = id
@@ -63,6 +64,7 @@ def __init__(
6364
self.report_url = report_url
6465
self.alert_fields = alert_fields
6566
self.elementary_database_and_schema = elementary_database_and_schema
67+
self.env = env
6668

6769
@property
6870
def unified_meta(self) -> Dict:

elementary/monitor/alerts/alert_messages/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)