Skip to content

feat(BA-1156): Customize SMTP template #4207

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

Merged
merged 6 commits into from
Apr 17, 2025
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
1 change: 1 addition & 0 deletions changes/4207.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Customize SMTP mail template.
16 changes: 14 additions & 2 deletions configs/manager/sample.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ password = "DB_PASSWORD" # env: BACKEND_DB_PASSWORD
# lock-conn-timeout = 30

# This setting causes the pool to recycle connections after the given number of seconds has passed.
# Default is -1, which means infinite.
# Default is -1, which means infinite.
# https://docs.sqlalchemy.org/en/14/core/engines.html#sqlalchemy.create_engine.params.pool_recycle
# https://docs.sqlalchemy.org/en/14/core/pooling.html#setting-pool-recycle
# pool-recycle = -1
Expand Down Expand Up @@ -258,9 +258,21 @@ password = "some_password"
sender = "[email protected]"
recipients = ["[email protected]", "[email protected]"]
use-tls = true
trigger-policy = "ERROR"
trigger-policy = "ON_ERROR"
max-workers = 5

template = """
Action type: {{ action_type }}
Entity ID: {{ entity_id }}
Status: {{ status }}
Description: {{ description }}
Started at: {{ created_at }}
Finished at: {{ ended_at }}
Duration: {{ duration }} seconds

This email is sent from Backend.AI SMTP Reporter.
"""


# The list of actions to be monitored by the audit log reporter.
[[reporter.action-monitors]]
Expand Down
13 changes: 13 additions & 0 deletions src/ai/backend/manager/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,18 @@
"action-monitors": [],
}

_default_smtp_template = """
Action type: {{ action_type }}
Entity ID: {{ entity_id }}
Status: {{ status }}
Description: {{ description }}
Started at: {{ created_at }}
Finished at: {{ ended_at }}
Duration: {{ duration }} seconds

This email is sent from Backend.AI SMTP Reporter.
"""

manager_local_config_iv = (
t.Dict({
t.Key("db"): t.Dict({
Expand Down Expand Up @@ -346,6 +358,7 @@
t.Key("recipients"): t.List(t.String),
t.Key("use-tls"): t.ToBool,
t.Key("max-workers", default=5): t.Int,
t.Key("template", default=_default_smtp_template): t.String,
t.Key("trigger-policy"): t.Enum("ALL", "ON_ERROR"),
})
),
Expand Down
59 changes: 26 additions & 33 deletions src/ai/backend/manager/reporters/smtp.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import smtplib
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from datetime import datetime
from email.mime.text import MIMEText
from typing import Final, override

Expand All @@ -18,7 +17,7 @@
log = BraceStyleAdapter(logging.getLogger(__spec__.name))


UNKNOWN_ENTITY_ID: Final[str] = "(unknown)"
_UNDEFINED_VALUE: Final[str] = "(undefined)"


@dataclass
Expand All @@ -30,6 +29,7 @@ class SMTPSenderArgs:
sender: str
recipients: list[str]
use_tls: bool
template: str
max_workers: int


Expand All @@ -42,8 +42,6 @@ def send_email(self, subject: str, email_body: str) -> None:
self._executor.submit(self._send_email, subject, email_body)

def _send_email(self, subject: str, email_body: str) -> None:
email_body += "\nThis email is sent from Backend.AI SMTP Reporter"

message = MIMEText(email_body, "plain", "utf-8")
message["Subject"] = subject
message["From"] = self._config.sender
Expand All @@ -64,29 +62,39 @@ def _send_email(self, subject: str, email_body: str) -> None:


class SMTPReporter(AbstractReporter):
_mail_template: str
_smtp_sender: SMTPSender

def __init__(self, args: SMTPSenderArgs, trigger_policy: SMTPTriggerPolicy) -> None:
self._smtp_sender = SMTPSender(args)
self._trigger_policy = trigger_policy
self._mail_template = args.template

def _create_body_from_template(self, message: FinishedActionMessage) -> str:
template = self._mail_template

template = template.replace("{{ action_id }}", str(message.action_id))
template = template.replace("{{ action_type }}", message.action_type)
template = template.replace(
"{{ entity_id }}", str(message.entity_id) if message.entity_id else _UNDEFINED_VALUE
)
template = template.replace("{{ request_id }}", message.request_id or _UNDEFINED_VALUE)
template = template.replace("{{ entity_type }}", message.entity_type)
template = template.replace("{{ operation_type }}", message.operation_type)
template = template.replace("{{ created_at }}", str(message.created_at))
template = template.replace("{{ ended_at }}", str(message.ended_at))
template = template.replace("{{ duration }}", str(message.duration))
template = template.replace("{{ status }}", message.status.value)
template = template.replace("{{ description }}", message.description)

return template

def _make_subject(self, action_type: str) -> str:
return f"Backend.AI SMTP Log Alert ({action_type})"

@override
async def report_started(self, message: StartedActionMessage) -> None:
if self._trigger_policy == SMTPTriggerPolicy.ON_ERROR:
return

subject = self._make_subject(message.action_type)
body = (
"Action has been triggered.\n\n"
f"Action type: ({message.action_type})\n"
f"Status: {OperationStatus.RUNNING}\n"
f"Description: Task is running...\n"
f"Started at: {datetime.now()}\n"
)
self._smtp_sender.send_email(subject, body)
pass

@override
async def report_finished(self, message: FinishedActionMessage) -> None:
Expand All @@ -95,26 +103,11 @@ async def report_finished(self, message: FinishedActionMessage) -> None:
subject = self._make_subject(message.action_type)
body = (
"Action has resulted in an error.\n\n"
f"Action type: ({message.action_type})\n"
f"Entity ID: {message.entity_id or UNKNOWN_ENTITY_ID}\n"
f"Status: {message.status}\n"
f"Description: {message.description}\n"
f"Started at: {message.created_at}\n"
f"Ended at: {message.ended_at}\n"
f"Duration: {message.duration} seconds\n"
f"{self._create_body_from_template(message)}"
)
self._smtp_sender.send_email(subject, body)
return

subject = self._make_subject(message.action_type)
body = (
"Action has been completed.\n\n"
f"Action type: ({message.action_type})\n"
f"Entity ID: {message.entity_id or UNKNOWN_ENTITY_ID}\n"
f"Status: {message.status}\n"
f"Description: {message.description}\n"
f"Started at: {message.created_at}\n"
f"Ended at: {message.ended_at}\n"
f"Duration: {message.duration} seconds\n"
)
body = f"Action has finished.\n\n{self._create_body_from_template(message)}"
self._smtp_sender.send_email(subject, body)
1 change: 1 addition & 0 deletions src/ai/backend/manager/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ def _make_registered_reporters(
recipients=smtp_conf["recipients"],
use_tls=smtp_conf["use-tls"],
max_workers=smtp_conf["max-workers"],
template=smtp_conf["template"],
)
trigger_policy = SMTPTriggerPolicy[smtp_conf["trigger-policy"]]
reporters[smtp_conf["name"]] = SMTPReporter(smtp_args, trigger_policy)
Expand Down
Loading