From 6e3f0b668c57539ded8f3bd8a39abb29eaac8161 Mon Sep 17 00:00:00 2001 From: sbgap Date: Fri, 28 Aug 2026 09:31:49 +0200 Subject: [PATCH 1/3] feat: add subject field for notification rules --- alerta/database/backends/postgres/base.py | 6 ++++-- alerta/models/notification_rule.py | 5 +++++ alerta/plugins/notification_rule.py | 25 +++++++++++++---------- alerta/sql/schema.sql | 6 ++++++ 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/alerta/database/backends/postgres/base.py b/alerta/database/backends/postgres/base.py index cbe038f4e..7b92e9d71 100644 --- a/alerta/database/backends/postgres/base.py +++ b/alerta/database/backends/postgres/base.py @@ -1356,9 +1356,9 @@ def delete_delayed_notification(self, id): def create_notification_rule(self, notification_rule): insert = """ INSERT INTO notification_rules (id, name, active, priority, environment, service, resource, event, tags, reactivate, excluded_tags, delay_time, - customer, "user", create_time, start_time, end_time, days, receivers, users_emails, group_ids, use_oncall, text, channel_id, triggers) + customer, "user", create_time, start_time, end_time, days, receivers, users_emails, group_ids, use_oncall, text, subject, channel_id, triggers) VALUES (%(id)s, %(name)s, %(active)s, %(priority)s, %(environment)s, %(service)s, %(resource)s, %(event)s, %(tags)s, %(reactivate)s, %(excluded_tags)s, %(delay_time)s, - %(customer)s, %(user)s, %(create_time)s, %(start_time)s, %(end_time)s, %(days)s, %(receivers)s, %(users_emails)s, %(group_ids)s, %(use_oncall)s, %(text)s, %(channel_id)s, %(triggers)s::notification_triggers[] ) + %(customer)s, %(user)s, %(create_time)s, %(start_time)s, %(end_time)s, %(days)s, %(receivers)s, %(users_emails)s, %(group_ids)s, %(use_oncall)s, %(text)s, %(subject)s, %(channel_id)s, %(triggers)s::notification_triggers[] ) RETURNING * """ return self._insert(insert, vars(notification_rule)) @@ -1617,6 +1617,8 @@ def update_notification_rule(self, id, **kwargs): update += 'triggers=%(triggers)s::notification_triggers[], ' if 'text' in kwargs: update += 'text=%(text)s, ' + if 'subject' in kwargs: + update += 'subject=%(subject)s, ' if 'channelId' in kwargs: update += 'channel_id=%(channelId)s,' if 'active' in kwargs: diff --git a/alerta/models/notification_rule.py b/alerta/models/notification_rule.py index 77cc7d2f9..e71fc2379 100644 --- a/alerta/models/notification_rule.py +++ b/alerta/models/notification_rule.py @@ -190,6 +190,7 @@ def __init__( kwargs['create_time'] if 'create_time' in kwargs else datetime.now(UTC) ) self.text = kwargs.get('text', None) + self.subject = kwargs.get('subject', None) if self.environment: self.priority = 1 @@ -269,6 +270,7 @@ def parse(cls, json: JSON) -> 'NotificationRule': else None, user=json.get('user', None), text=json.get('text', None), + subject=json.get('subject', None), days=json.get('days', None), ) return notification_rule @@ -299,6 +301,7 @@ def serialize(self) -> Dict[str, Any]: 'createTime': self.create_time, 'reactivate': self.reactivate, 'text': self.text, + 'subject': self.subject, 'startTime': self.start_time.strftime('%H:%M') if self.start_time is not None else None, @@ -354,6 +357,7 @@ def from_document(cls, doc: Dict[str, Any]) -> 'NotificationRule': create_time=doc.get('createTime', None), reactivate=doc.get('reactivate', None), text=doc.get('text', None), + subject=doc.get('subject', None), start_time=( datetime.strptime( f'{doc["startTime"]:.2f}'.replace('.', ':'), '%H:%M' @@ -400,6 +404,7 @@ def from_record(cls, rec) -> 'NotificationRule': create_time=rec.create_time, reactivate=rec.reactivate, text=rec.text, + subject=rec.subject, start_time=rec.start_time, end_time=rec.end_time, days=rec.days, diff --git a/alerta/plugins/notification_rule.py b/alerta/plugins/notification_rule.py index 08b94feaf..2cc5f11a8 100644 --- a/alerta/plugins/notification_rule.py +++ b/alerta/plugins/notification_rule.py @@ -123,22 +123,22 @@ def send_link_mobility_xml(message: str, channel: NotificationChannel, receivers return requests.post(f'{channel.host}', data, headers=headers, verify=channel.verify if channel.verify is None or channel.verify.lower() != 'false' else False) -def send_smtp_mail(message: str, channel: NotificationChannel, receivers: set, fernet: Fernet, **kwargs): +def send_smtp_mail(message: str, channel: NotificationChannel, receivers: set, fernet: Fernet, subject: str, **kwargs): server = smtplib.SMTP_SSL(channel.host) api_sid = fernet.decrypt(channel.api_sid.encode()).decode() api_token = fernet.decrypt(channel.api_token.encode()).decode() server.login(api_sid, api_token) - server.sendmail(channel.sender, list(receivers), f"From: {channel.sender}\nTo: {','.join(receivers)}\nSubject: Alerta\n\n{message}") + server.sendmail(channel.sender, list(receivers), f"From: {channel.sender}\nTo: {','.join(receivers)}\nSubject: {subject}\n\n{message}") server.quit() -def send_email(message: str, channel: NotificationChannel, receivers: set, fernet: Fernet, **kwargs): +def send_email(message: str, channel: NotificationChannel, receivers: set, fernet: Fernet, subject: str, **kwargs): data = { 'personalizations': [ {'to': [{'email': email} for email in receivers]} ], 'from': {'email': channel.sender}, - 'subject': 'Alerta', + 'subject': subject, 'content': [{'type': 'text/html', 'value': message.replace('\n', '
')}], } api_token = fernet.decrypt(channel.api_token.encode()).decode() @@ -193,7 +193,7 @@ def delay_notification(alert: Alert, notification_rule: NotificationRule): }).create() -def handle_channel(message: str, channel: NotificationChannel, notification_rule: NotificationRule, users: 'set[NotificationInfo]', fernet: Fernet, alert: str): +def handle_channel(message: str, channel: NotificationChannel, notification_rule: NotificationRule, users: 'set[NotificationInfo]', fernet: Fernet, alert: str, subject: str): notification_type = channel.type phone_numbers = {*notification_rule.receivers, *[f'{user.country_code}{user.phone_number}' for user in users if user.phone_number is not None]} mails = {*[receiver.lower() for receiver in notification_rule.receivers], *[user.email.lower() for user in users if user.email is not None]} @@ -202,7 +202,7 @@ def handle_channel(message: str, channel: NotificationChannel, notification_rule if len(mails) == 0: return try: - response = send_email(message, channel, mails, fernet) + response = send_email(message, channel, mails, fernet, subject) if response.status_code != 202: data = response.json()['errors'][0] log_notification(False, message, channel, notification_rule.id, alert, mails, f'Got status code {response.status_code}: {data["message"]}') @@ -217,7 +217,7 @@ def handle_channel(message: str, channel: NotificationChannel, notification_rule if len(mails) == 0: return try: - send_smtp_mail(message, channel, mails, fernet) + send_smtp_mail(message, channel, mails, fernet, subject) log_notification(True, message, channel, notification_rule.id, alert, mails) except InvalidToken: log_notification(False, message, channel, notification_rule.id, alert, mails, 'NotificationChannel: Failed to decrypt authentication keys') @@ -278,9 +278,10 @@ def handle_channel(message: str, channel: NotificationChannel, notification_rule def handle_test(channel: NotificationChannel, info: NotificationRule, config): message = info.text if info.text != '' else 'this is a test message for testing a notification_channel in alerta' + subject = info.subject if info.subject != '' else 'Alerta Test Notification' fernet = Fernet(config['NOTIFICATION_KEY']) channel = update_bearer(channel, fernet) - handle_channel(message, channel, info, info.users, fernet, 'Test Notification Channel') + handle_channel(message, channel, info, info.users, fernet, 'Test Notification Channel', subject) def get_notification_trigger_text(rule: NotificationRule, alert: Alert, status: str): @@ -294,19 +295,21 @@ def get_notification_trigger_text(rule: NotificationRule, alert: Alert, status: def handle_notifications(alert: 'Alert', notifications: 'list[tuple[NotificationRule,NotificationChannel, list[set[NotificationInfo | None]]]]', on_users: 'list[set[NotificationInfo | None]]', fernet: Fernet, app_context, status: str = ''): app_context.push() standard_message = '%(environment)s: %(severity)s alert for %(service)s - %(resource)s is %(event)s' + default_subject = 'Alerta Notification' for notification_rule, channel, users in notifications: if channel is None: return if notification_rule.use_oncall: users.update(on_users) - msg_obj = {**alert.serialize, 'status': status} if status != '' else alert.serialize + msg_obj = get_message_obj({**alert.serialize, 'status': status} if status != '' else alert.serialize) text = get_notification_trigger_text(notification_rule, alert, status) message = ( text if text != '' and text is not None else standard_message - ) % get_message_obj(msg_obj) + ) % msg_obj + subject = (notification_rule.subject if notification_rule.subject != '' and notification_rule.subject is not None else default_subject) % msg_obj - handle_channel(message, channel, notification_rule, users, fernet, alert.id) + handle_channel(message, channel, notification_rule, users, fernet, alert.id, subject) def handle_alert(alert: Alert, config, stat: str = ''): diff --git a/alerta/sql/schema.sql b/alerta/sql/schema.sql index 3c6ad9cf8..7d82c301d 100644 --- a/alerta/sql/schema.sql +++ b/alerta/sql/schema.sql @@ -288,6 +288,12 @@ BEGIN END $$; +DO $$ +BEGIN + ALTER TABLE notification_rules ADD COLUMN "subject" text; +EXCEPTION + WHEN duplicate_column THEN RAISE NOTICE 'column "subject" already exists in notification_rules.'; +END$$; DO $$ BEGIN ALTER TABLE notification_rules ADD COLUMN delay_time interval; From aa24c7979c100e879b9d96d302a5c8a4e5d52b18 Mon Sep 17 00:00:00 2001 From: sbgap Date: Fri, 28 Aug 2026 13:28:41 +0200 Subject: [PATCH 2/3] fix: add None check for selecting default subject --- alerta/plugins/notification_rule.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alerta/plugins/notification_rule.py b/alerta/plugins/notification_rule.py index 2cc5f11a8..058a3b0b5 100644 --- a/alerta/plugins/notification_rule.py +++ b/alerta/plugins/notification_rule.py @@ -278,7 +278,7 @@ def handle_channel(message: str, channel: NotificationChannel, notification_rule def handle_test(channel: NotificationChannel, info: NotificationRule, config): message = info.text if info.text != '' else 'this is a test message for testing a notification_channel in alerta' - subject = info.subject if info.subject != '' else 'Alerta Test Notification' + subject = info.subject if info.subject != '' and info.subject is not None else 'Alerta Test Notification' fernet = Fernet(config['NOTIFICATION_KEY']) channel = update_bearer(channel, fernet) handle_channel(message, channel, info, info.users, fernet, 'Test Notification Channel', subject) From 0691e7a0e55e8533876e5374a0d01db9ebe98bdb Mon Sep 17 00:00:00 2001 From: sbgap Date: Fri, 28 Aug 2026 13:29:05 +0200 Subject: [PATCH 3/3] feat: add subject for notification sends --- alerta/views/notification_sends.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alerta/views/notification_sends.py b/alerta/views/notification_sends.py index 096d30e65..06d3bfaf4 100644 --- a/alerta/views/notification_sends.py +++ b/alerta/views/notification_sends.py @@ -62,7 +62,7 @@ def notification_send(notification_channel_id): users = [NotificationSendInfo.find_by_id(notification['id']).email for notification in data['notifications'] if notification['type'] == 'User'] groups = [notification['id'] for notification in data['notifications'] if notification['type'] == 'Group'] try: - notification_rule = NotificationRule.parse({'usersEmails': users, 'groupIds': groups, 'receivers': [], 'text': data['text'], 'channelId': notification_channel_id, 'environment': plugins.config.get('DEFAULT_ENVIRONMENT')}) + notification_rule = NotificationRule.parse({'usersEmails': users, 'groupIds': groups, 'receivers': [], 'text': data['text'], 'subject': data.get('subject'), 'channelId': notification_channel_id, 'environment': plugins.config.get('DEFAULT_ENVIRONMENT')}) except Exception as e: raise ApiError(str(e), 400) try: