Skip to content

SMTP Email Alerter integration + make generic Alerter steps #3379

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 62 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
69b98d9
Add SMTP Email Alerter integration
strickvl Feb 25, 2025
7f9dac7
Merge branch 'develop' into feature/email-alerter
strickvl Mar 5, 2025
56feb23
Improve SMTP Email Alerter ask() method error handling
strickvl Mar 5, 2025
1c7a814
add quotes to registration command
strickvl Mar 5, 2025
64f7f1a
Add email-specific alerter hooks with improved formatting
strickvl Mar 5, 2025
2dcb785
Replace Rich tracebacks with standard Python tracebacks in email alerter
strickvl Mar 5, 2025
a9a3dda
Completely replace Rich tracebacks with standard Python tracebacks
strickvl Mar 5, 2025
d2e88d4
Remove Markdown formatting from email subjects
strickvl Mar 5, 2025
0637e3e
formatting
strickvl Mar 5, 2025
2364bde
Fix mypy type issues in SMTP email alerter
strickvl Mar 5, 2025
4403a4f
Improve OpenAI failure hook formatting for SMTP email alerter
strickvl Mar 5, 2025
df7d8d3
Remove unused 'language' variable in OpenAI failure hook
strickvl Mar 5, 2025
f780e63
Fix darglint error by removing unnecessary Returns section in docstring
strickvl Mar 5, 2025
ffaa583
Merge branch 'develop' into feature/email-alerter
strickvl Mar 6, 2025
b0d0f67
Merge remote-tracking branch 'origin/develop' into feature/email-alerter
strickvl Mar 19, 2025
44fa4a9
Refactor SMTPEmailIntegration: Remove circular import handling and un…
strickvl Mar 19, 2025
9408792
Update type hint for `ask` method in `SMTPEmailAlerter` to `Never`
strickvl Mar 19, 2025
8746ca0
Refactor recipient email validation in `SMTPEmailAlerter`
strickvl Mar 19, 2025
928b193
Refactor SMTPEmailAlerter: Add generic attribute retrieval method
strickvl Mar 19, 2025
90b5537
Refactor: Create a shared email template system for alerters
strickvl Mar 19, 2025
7ceaa8b
Merge remote-tracking branch 'origin/develop' into feature/email-alerter
strickvl Mar 20, 2025
8a69a34
fix(typehints): Fix mypy errors in SMTP email alerter
strickvl Mar 20, 2025
cb0c66d
docs: Update alerter documentation for unified approach
strickvl Mar 20, 2025
9241d92
refactor: Update Discord alerter for unified alerter approach
strickvl Mar 20, 2025
177c977
refactor: Update Slack alerter for unified alerter approach
strickvl Mar 20, 2025
d4cc00c
refactor: Update SMTP Email alerter for unified alerter approach
strickvl Mar 20, 2025
dbc2426
feat: Implement unified alerter approach with AlerterMessage
strickvl Mar 20, 2025
131fb9a
refactor: Move AlerterMessage model to models/v2/misc structure
strickvl Mar 20, 2025
a3fa2fc
fix: Resolve mypy type errors in alerter components
strickvl Mar 20, 2025
3d9663c
format script
strickvl Mar 20, 2025
451a469
formatting script
strickvl Mar 20, 2025
e55c6c1
Merge branch 'develop' into feature/email-alerter
strickvl Mar 20, 2025
21d0b27
fix: Add license headers and docstrings to alerter step files
strickvl Mar 20, 2025
4da5ac2
Merge remote-tracking branch 'origin/develop' into feature/email-alerter
strickvl Apr 20, 2025
633eb20
Merge branch 'develop' into feature/email-alerter
strickvl May 22, 2025
9301994
Add unit tests for HTML email template generation functions
strickvl May 22, 2025
d8a0b92
Add email address validation for SMTP alerter
strickvl May 22, 2025
d5d8d04
Fix HTML escaping to prevent XSS vulnerabilities
strickvl May 22, 2025
7699d51
Improve error handling for SMTP connection failures
strickvl May 22, 2025
810bb07
Add unit tests for parameter inheritance in SMTP alerter
strickvl May 22, 2025
924aca3
Add unit tests for _format_markdown_for_html function
strickvl May 22, 2025
77e5f46
Improve alerter_ask_step error handling
strickvl May 22, 2025
7cc46fa
Fix Slack/Discord alerters and add SMTP exports
strickvl May 22, 2025
844d0a6
Fix Discord import and verify logo URL
strickvl May 22, 2025
81790fb
Formatting
strickvl May 22, 2025
42a88ab
Fix mypy str-bytes-safe error in SMTP alerter
strickvl May 22, 2025
9b6187e
Fix mypy errors in Discord integration
strickvl May 22, 2025
c9907d2
Formatting
strickvl May 22, 2025
2e0c318
Fix Discord client cleanup to prevent unclosed connector warnings
strickvl May 22, 2025
4f56da3
Formatting
strickvl May 22, 2025
301ae93
linting fix
strickvl May 22, 2025
9bfc4c5
Fix broken relative link in custom alerter documentation
strickvl May 22, 2025
1a040e8
Fix links
strickvl May 22, 2025
e08f06b
Fix mypy discord issue
strickvl May 22, 2025
a963c29
Merge develop branch into feature/email-alerter
strickvl May 22, 2025
1c6906a
Fix imports and formatting
strickvl May 22, 2025
8e364ab
Refactor _format_markdown_for_html into smaller helper functions
strickvl May 22, 2025
dade9e3
Formatting
strickvl May 22, 2025
857e88a
Update alerter hooks to use structured AlerterMessage format
strickvl May 23, 2025
0ead6ee
Fix mypy errors
strickvl May 23, 2025
e417927
Merge remote-tracking branch 'origin/develop' into feature/email-alerter
strickvl May 23, 2025
e633564
Fix mypy error by importing Never from typing_extensions
strickvl May 23, 2025
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
18 changes: 9 additions & 9 deletions docs/book/component-guide/alerters/alerters.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
---
description: Sending automated alerts to chat services.
description: Sending automated alerts to chat services and email.
icon: message-exclamation
---

# Alerters

**Alerters** allow you to send messages to chat services (like Slack, Discord, Mattermost, etc.) from within your
**Alerters** allow you to send messages to chat services (like Slack, Discord, Mattermost, etc.) or via email from within your
pipelines. This is useful to immediately get notified when failures happen, for general monitoring/reporting, and also
for building human-in-the-loop ML.

## Alerter Flavors

Currently, the [SlackAlerter](slack.md) and [DiscordAlerter](discord.md) are the available alerter integrations. However, it is straightforward to
extend ZenML and [build an alerter for other chat services](custom.md).
Currently, the [SlackAlerter](slack.md), [DiscordAlerter](discord.md), and [SMTP Email Alerter](smtp_email.md) are the available alerter integrations. However, it is straightforward to extend ZenML and [build an alerter for other services](custom.md).

| Alerter | Flavor | Integration | Notes |
|------------------------------------|-----------|-------------|--------------------------------------------------------------------|
| [Slack](slack.md) | `slack` | `slack` | Interacts with a Slack channel |
| [Discord](discord.md) | `discord` | `discord` | Interacts with a Discord channel |
| [Custom Implementation](custom.md) | _custom_ | | Extend the alerter abstraction and provide your own implementation |
| Alerter | Flavor | Integration | Notes |
|------------------------------------|---------------|---------------|--------------------------------------------------------------------|
| [Slack](slack.md) | `slack` | `slack` | Interacts with a Slack channel |
| [Discord](discord.md) | `discord` | `discord` | Interacts with a Discord channel |
| [SMTP Email](smtp_email.md) | `smtp_email` | `smtp_email` | Sends email notifications via SMTP |
| [Custom Implementation](custom.md) | _custom_ | | Extend the alerter abstraction and provide your own implementation |

{% hint style="info" %}
If you would like to see the available flavors of alerters in your terminal, you can use the following command:
Expand Down
280 changes: 280 additions & 0 deletions docs/book/component-guide/alerters/smtp_email.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
---
description: Sending automated alerts via email using SMTP.
---

# SMTP Email Alerter

The `SMTPEmailAlerter` enables you to send email notifications directly from within your ZenML pipelines and steps. It supports both plain text and HTML-formatted emails, making it ideal for sending rich, visually appealing notifications.

## How to Create

### Generating an App Password (for Gmail)

If you plan to use Gmail as your SMTP server, you'll need to generate an App Password rather than using your regular account password:

1. Go to your [Google Account Security settings](https://myaccount.google.com/security)
2. Ensure 2-Step Verification is enabled
3. Under "Signing in to Google," select "App passwords"
4. Generate a new app password for "Mail" and "Other (Custom name)" - you can name it "ZenML"
5. Save the generated 16-character password for use in your alerter configuration

### Registering an SMTP Email Alerter in ZenML

To create an `SMTPEmailAlerter`, no additional integrations need to be installed as it uses the standard library's `smtplib` module.

You can use the ZenML CLI to create a secret and register an alerter:

```shell
# Create a secret for your email password/app password
zenml secret create email_credentials \
--smtp_password=<YOUR_APP_PASSWORD>

# Register the alerter
zenml alerter register email_alerter \
--flavor=smtp_email \
--smtp_server=smtp.gmail.com \
--smtp_port=587 \
--sender_email=<YOUR_EMAIL_ADDRESS> \
--password="{{email_credentials.smtp_password}}" \
--recipient_email=<RECIPIENT_EMAIL_ADDRESS>
```

Here's what the parameters mean:

* `smtp_server`: Your email provider's SMTP server (e.g., `smtp.gmail.com` for Gmail)
* `smtp_port`: The port for your SMTP server (typically 587 for TLS)
* `sender_email`: The email address sending the notifications
* `password`: The password or app password for the sender email account
* `recipient_email`: (Optional) Default recipient email address for alerts

After you have registered the `email_alerter`, you can add it to your stack:

```shell
zenml stack register ... -al email_alerter --set
```

## How to Use

In ZenML, you can use the email alerter in various ways.

### Use the `post()` Method Directly

You can use the client to fetch the active alerter and use the `post` method directly:

```python
from zenml import pipeline, step
from zenml.client import Client

@step
def send_email_alert() -> None:
Client().active_stack.alerter.post("Pipeline step completed successfully!")

@pipeline(enable_cache=False)
def my_pipeline():
send_email_alert()

if __name__ == "__main__":
my_pipeline()
```

### Use with Custom Settings

The SMTP Email alerter comes with several configurable settings:

```python
from zenml import pipeline, step
from zenml.client import Client

# Use different recipient and subject prefix for this step
@step(settings={"alerter": {
"recipient_email": "[email protected]",
"subject_prefix": "[URGENT]",
"include_html": True
}})
def send_urgent_alert() -> None:
Client().active_stack.alerter.post("Critical alert: Database backup required!")

@pipeline(enable_cache=False)
def my_pipeline():
send_urgent_alert()

if __name__ == "__main__":
my_pipeline()
```

### Use with SMTPEmailAlerterParameters and SMTPEmailAlerterPayload

You can customize email content and appearance using parameter and payload classes:

```python
from zenml import pipeline, step, get_step_context
from zenml.client import Client
from zenml.integrations.smtp_email.alerters.smtp_email_alerter import (
SMTPEmailAlerterParameters, SMTPEmailAlerterPayload
)

@step
def send_pipeline_status() -> None:
# Create a payload with pipeline information
payload = SMTPEmailAlerterPayload(
pipeline_name=get_step_context().pipeline.name,
step_name=get_step_context().step_run.name,
stack_name=Client().active_stack.name,
)

# Set up parameters for this specific email
params = SMTPEmailAlerterParameters(
recipient_email="[email protected]",
subject="Pipeline Execution Update",
payload=payload
)

# Send the email with custom parameters
Client().active_stack.alerter.post(
message="The pipeline has completed successfully with all validation tests passing.",
params=params
)

@pipeline(enable_cache=False)
def my_pipeline():
send_pipeline_status()

if __name__ == "__main__":
my_pipeline()
```

You can also provide custom HTML content for complete control over email appearance:

```python
@step
def send_custom_html_email() -> None:
# Create custom HTML
custom_html = """
<html>
<body style="font-family: Arial, sans-serif; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px; background-color: #f7f7f7; border-radius: 10px;">
<h1 style="color: #4A6CFA;">Model Training Complete</h1>
<p>Your machine learning model has completed training with the following metrics:</p>
<ul>
<li><strong>Accuracy:</strong> 92.5%</li>
<li><strong>Precision:</strong> 90.2%</li>
<li><strong>Recall:</strong> 88.7%</li>
</ul>
<p>View the <a href="https://dashboard.zenml.io" style="color: #4A6CFA;">complete results in the dashboard</a>.</p>
</div>
</body>
</html>
"""

params = SMTPEmailAlerterParameters(
subject="Model Training Results",
html_body=custom_html
)

Client().active_stack.alerter.post(
message="Model training complete. Accuracy: 92.5%, Precision: 90.2%, Recall: 88.7%",
params=params
)
```

### Using with Failure and Success Hooks

Email alerts are particularly useful when combined with step hooks for success and failure notifications. There are two types of hooks you can use:

#### Standard Alerter Hooks

These work with any alerter but may not provide optimal formatting for emails:

```python
from zenml import pipeline, step
from zenml.hooks import alerter_failure_hook, alerter_success_hook

@step(on_failure=alerter_failure_hook, on_success=alerter_success_hook)
def risky_step() -> None:
# This step will trigger alerts on either success or failure
import random
if random.random() < 0.5:
raise RuntimeError("Step failed with a simulated error")
return "Step completed successfully"

@pipeline(enable_cache=False)
def my_pipeline():
risky_step()

if __name__ == "__main__":
my_pipeline()
```

#### Email-Specific Hooks (Recommended)

The SMTP Email integration provides specialized hooks that format messages specifically for email, with proper HTML formatting and structure:

```python
from zenml import pipeline, step
from zenml.integrations.smtp_email.hooks import (
smtp_email_alerter_failure_hook,
smtp_email_alerter_success_hook
)

@step(on_failure=smtp_email_alerter_failure_hook, on_success=smtp_email_alerter_success_hook)
def risky_step() -> None:
# This step will trigger email-optimized alerts
import random
if random.random() < 0.5:
raise RuntimeError("Step failed with a simulated error")
return "Step completed successfully"

@pipeline(enable_cache=False)
def my_pipeline():
risky_step()

if __name__ == "__main__":
my_pipeline()
```

These specialized hooks automatically:
- Format messages with proper HTML for email clients
- Include relevant pipeline and step information
- Set descriptive email subjects
- Use structured layout for better readability
- Format error tracebacks for better legibility in email clients
- Support Markdown-style formatting (backticks, asterisks)
- Display traceback code in monospaced font with proper formatting

These hooks are strongly recommended when using SMTP email alerters, as they provide a significantly better user experience compared to the standard hooks which were primarily designed for chat services.

### Using the Predefined Step

For convenience, the SMTP Email integration includes a predefined step for sending email alerts that you can use directly in your pipelines:

```python
from zenml import pipeline
from zenml.integrations.smtp_email.steps import smtp_email_alerter_post_step

@pipeline(enable_cache=False)
def notification_pipeline():
# Send an email notification with pipeline information
smtp_email_alerter_post_step(
message="Pipeline execution started!",
subject="Pipeline Notification",
recipient_email="[email protected]",
include_pipeline_info=True
)

# Perform other pipeline steps...

if __name__ == "__main__":
notification_pipeline()
```

This step provides a simple interface for sending email alerts without having to directly interact with the alerter API.

## Notes

* The `ask()` method is not supported in the SMTP Email alerter, as email doesn't support interactive approvals like chat systems.
* For security best practices, always store your email credentials as secrets rather than hardcoding them.
* You can use this alerter with any SMTP-compliant email service, including Gmail, Outlook, AWS SES, SendGrid, and others.

For more information and a full list of configurable attributes of the SMTP Email alerter, check out the [SDK Docs](https://sdkdocs.zenml.io/latest/integration_code_docs/integrations-smtp_email/#zenml.integrations.smtp_email.alerters.smtp_email_alerter.SMTPEmailAlerter).

<figure><img src="https://static.scarf.sh/a.png?x-pxid=f0b4f458-0a54-4fcd-aa95-d5ee424815bc" alt="ZenML Scarf"><figcaption></figcaption></figure>
22 changes: 9 additions & 13 deletions src/zenml/hooks/alerter_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@
# permissions and limitations under the License.
"""Functionality for standard hooks."""

import io
import sys

from rich.console import Console
import traceback

from zenml import get_step_context
from zenml.client import Client
Expand All @@ -36,23 +33,22 @@ def alerter_failure_hook(exception: BaseException) -> None:
context = get_step_context()
alerter = Client().active_stack.alerter
if alerter:
output_captured = io.StringIO()
original_stdout = sys.stdout
sys.stdout = output_captured
console = Console()
console.print_exception(show_locals=False)

sys.stdout = original_stdout
rich_traceback = output_captured.getvalue()
# Use standard Python traceback instead of Rich traceback
tb_lines = traceback.format_exception(
type(exception), exception, exception.__traceback__
)
plain_traceback = "".join(tb_lines).strip()

message = "*Failure Hook Notification! Step failed!*" + "\n\n"
message += f"Pipeline name: `{context.pipeline.name}`" + "\n"
message += f"Run name: `{context.pipeline_run.name}`" + "\n"
message += f"Step name: `{context.step_run.name}`" + "\n"
message += f"Parameters: `{context.step_run.config.parameters}`" + "\n"
message += (
f"Exception: `({type(exception)}) {rich_traceback}`" + "\n\n"
f"Exception: `({type(exception).__name__}) {str(exception)}`"
+ "\n\n"
)
message += f"{plain_traceback}"
alerter.post(message)
else:
logger.warning(
Expand Down
1 change: 1 addition & 0 deletions src/zenml/integrations/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
SKYPILOT_LAMBDA = "skypilot_lambda"
SKYPILOT_KUBERNETES = "skypilot_kubernetes"
SLACK = "slack"
SMTP_EMAIL = "smtp_email"
SPARK = "spark"
TEKTON = "tekton"
TENSORBOARD = "tensorboard"
Expand Down
Loading
Loading