SIENTIAPDE-2149: model_ids allow-list filter for receiver groups

New, separate model_id-based filter distinct from the existing
ignore_models_id deny-list (SIENTIAPDE-2012): a group configuring
model_ids only receives notifications from those models. Both axes
apply together (AND) when a group sets both.

Design decisions made while implementing (matching the levels/
ignore_models_id split from SIENTIAPDE-2011/2012, not yet confirmed
with the team beyond what's in the ticket):
- Query-time filter: union of every active group's model_ids, applied
  only when ALL active groups set one -- mirrors levels' exact
  union-or-nothing logic in _compute_query_time_filter. Combines
  cleanly with ignore_models_id's intersection $nin on the same
  model_id field.
- Per-group filter: extended _check_levels_and_ignore_models_id (now
  _check_levels_and_model_filters) in slot_manager.py, and the inline
  condition in formatters.py's filter_notification_reports.
- Empty/missing model_ids means no restriction, same convention as
  levels and ignore_models_id.

Covered by new unit tests (slot_manager, formatters,
_compute_query_time_filter) and two new e2e tests against real
Mongo/Redis/Temporal: per-group allow-list interacting with another
unrestricted group, and the query-time union excluding a model outside
every active group's allow-list.

Not yet verified on the real cluster -- blocked on the dev cluster's
package index not auto-syncing new sientia_do releases (separate issue,
being investigated for SIENTIAPDE-2013).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Eduardo Rios
2026-09-18 15:02:42 -03:00
parent 87ddd02038
commit da8a796197
11 changed files with 473 additions and 8 deletions

View File

@@ -826,6 +826,7 @@ class Formatters(SientiaMonitoring):
- ignore (list, optional): Notification IDs to exclude
- levels (list, optional): Notification levels to include
- ignore_models_id (list, optional): Model IDs to exclude
- model_ids (list, optional): Model IDs to allow (SIENTIAPDE-2149)
Returns:
dict[str, Any]: Filtered receiver groups with their notifications, keyed by group_name.
@@ -851,6 +852,7 @@ class Formatters(SientiaMonitoring):
ignore_list = receiver_group.get('ignore', [])
levels_list = receiver_group.get('levels')
ignore_models_id_list = receiver_group.get('ignore_models_id') or []
model_ids_list = receiver_group.get('model_ids')
for notification in notification_package:
alert_type = 'reports'
@@ -868,6 +870,7 @@ class Formatters(SientiaMonitoring):
not ignore_models_id_list
or notification.get('model_id') not in ignore_models_id_list
)
and (not model_ids_list or notification.get('model_id') in model_ids_list)
):
receiver_groups[group_name]['notifications'].append(notification)
already_added_keys.append(key)

View File

@@ -398,6 +398,7 @@ class SlotManager(SientiaMonitoring):
- ignore (list, optional): Notification IDs to exclude
- levels (list, optional): Notification levels to include
- ignore_models_id (list, optional): Model IDs to exclude
- model_ids (list, optional): Model IDs to allow (SIENTIAPDE-2149)
- notification_ttl (int): Seconds before considering notification persistent
Returns:
@@ -450,6 +451,7 @@ class SlotManager(SientiaMonitoring):
ignore_list = receiver_group.get('ignore', [])
levels_list = receiver_group.get('levels')
ignore_models_id_list = receiver_group.get('ignore_models_id') or []
model_ids_list = receiver_group.get('model_ids')
notifications = []
for notification in notification_package:
@@ -462,8 +464,8 @@ class SlotManager(SientiaMonitoring):
and self._check_keys_and_notif_id(
notification_id, ignore_list, key, already_added_keys
)
and self._check_levels_and_ignore_models_id(
notification, levels_list, ignore_models_id_list
and self._check_levels_and_model_filters(
notification, levels_list, ignore_models_id_list, model_ids_list
)
):
notifications.append(notification)
@@ -493,23 +495,29 @@ class SlotManager(SientiaMonitoring):
return 'do_nothing'
@staticmethod
def _check_levels_and_ignore_models_id(notification, levels_list, ignore_models_id_list):
def _check_levels_and_model_filters(
notification, levels_list, ignore_models_id_list, model_ids_list
):
"""
Check if the notification's level is in the levels_list and its model_id is not in the ignore_models_id_list.
Check the notification's level and model_id against a group's filters.
Args:
notification (dict): The notification to check.
levels_list (list, optional): List of levels to include. If None, all levels are included.
ignore_models_id_list (list, optional): List of model_ids to exclude. If None, no models are excluded.
model_ids_list (list, optional): List of model_ids to allow (SIENTIAPDE-2149).
If None/empty, all models are allowed. Independent of ignore_models_id_list --
both apply together when both are configured.
Returns:
bool: True if the notification passes the checks, False otherwise.
"""
level_check = not levels_list or notification.get('level') in levels_list
model_id_check = (
ignore_models_id_check = (
not ignore_models_id_list or notification.get('model_id') not in ignore_models_id_list
)
return level_check and model_id_check
model_ids_check = not model_ids_list or notification.get('model_id') in model_ids_list
return level_check and ignore_models_id_check and model_ids_check
@staticmethod
def _check_keys_and_notif_id(

View File

@@ -137,6 +137,12 @@ class LoadNotificationPackage:
is only safe to drop from the fetch if EVERY active group ignores it.
A group with no `ignore_models_id` is treated as "ignores nothing" (empty
set), which naturally zeroes out the intersection via set math.
- model_ids (SIENTIAPDE-2149): union of every group's `model_ids` allow-list,
mirroring `levels`' own union-or-nothing logic. If ANY active group has no
restriction (missing/empty `model_ids`), that group wants every model, so
the query can't restrict to an allow-list at all. `model_ids` and
`ignore_models_id` are independent axes and can both narrow the same
`model_id` field at once (Mongo supports multiple operators on one field).
"""
if not sending_configs:
return default_filter
@@ -160,7 +166,21 @@ class LoadNotificationPackage:
]
models_intersection = set.intersection(*sending_configs_ignore_models_id)
sending_configs_model_ids = [group.get('model_ids') for group in sending_configs]
model_id_filter: dict[str, Any] = {}
if all(sending_configs_model_ids):
model_ids_union: set[str] = set()
for model_ids in sending_configs_model_ids:
if model_ids:
model_ids_union.update(model_ids)
model_id_filter['$in'] = sorted(model_ids_union)
if models_intersection:
query_filter['model_id'] = {'$nin': sorted(models_intersection)}
model_id_filter['$nin'] = sorted(models_intersection)
if model_id_filter:
query_filter['model_id'] = model_id_filter
return query_filter