Skip to content
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

Direct paging improvements #3064

Merged
merged 19 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,7 @@ def _get_notification_plan_for_user(self, user_to_notify, future_step=False, imp
# last passed step order + 1
notification_policy_order = last_user_log.notification_policy.order + 1

notification_policies = UserNotificationPolicy.objects.filter(user=user_to_notify, important=important)
notification_policies = user_to_notify.get_or_create_notification_policies(important=important)

for notification_policy in notification_policies:
future_notification = notification_policy.order >= notification_policy_order
Expand Down
36 changes: 16 additions & 20 deletions engine/apps/alerts/models/alert_receive_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,10 +230,25 @@ def get_default_template_attribute(self, render_for, attr_name):
defaults = getattr(self, f"INTEGRATION_TO_DEFAULT_WEB_{attr_name.upper()}_TEMPLATE", {})
return defaults.get(self.integration)

class DuplicateDirectPagingError(Exception):
"""Only one Direct Paging integration is allowed per team."""

DETAIL = "Direct paging integration already exists for this team" # Returned in BadRequest responses

@classmethod
def create(cls, **kwargs):
organization = kwargs["organization"]
team = kwargs["team"] if kwargs["team"] else None
integration = kwargs["integration"]
if (
integration == AlertReceiveChannel.INTEGRATION_DIRECT_PAGING
and AlertReceiveChannel.objects.filter(
organization=organization, team=team, integration=integration
).exists()
iskhakov marked this conversation as resolved.
Show resolved Hide resolved
):
raise cls.DuplicateDirectPagingError
with transaction.atomic():
other_channels = cls.objects_with_deleted.select_for_update().filter(organization=kwargs["organization"])
other_channels = cls.objects_with_deleted.select_for_update().filter(organization=organization)
channel = cls(**kwargs)
smile_code = number_to_smiles_translator(other_channels.count())
verbal_name = (
Expand All @@ -251,25 +266,6 @@ def delete(self):
def hard_delete(self):
super(AlertReceiveChannel, self).delete()

class DuplicateDirectPagingError(Exception):
"""Only one Direct Paging integration is allowed per team."""

DETAIL = "Direct paging integration already exists for this team" # Returned in BadRequest responses

def save(self, *args, **kwargs):
# Don't allow multiple Direct Paging integrations per team
if (
self.integration == AlertReceiveChannel.INTEGRATION_DIRECT_PAGING
and AlertReceiveChannel.objects.filter(
organization=self.organization, team=self.team, integration=self.integration
)
.exclude(pk=self.pk)
.exists()
):
raise self.DuplicateDirectPagingError

return super().save(*args, **kwargs)

def change_team(self, team_id, user):
if team_id == self.team_id:
raise TeamCanNotBeChangedError("Integration is already in this team")
Expand Down
6 changes: 2 additions & 4 deletions engine/apps/alerts/tasks/notify_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,8 @@ def notify_group_task(alert_group_pk, escalation_policy_snapshot_order=None):
if not user.is_notification_allowed:
continue

notification_policies = UserNotificationPolicy.objects.filter(
user=user,
important=escalation_policy_step == EscalationPolicy.STEP_NOTIFY_GROUP_IMPORTANT,
)
important = escalation_policy_step == EscalationPolicy.STEP_NOTIFY_GROUP_IMPORTANT
notification_policies = user.get_or_create_notification_policies(important=important)

if notification_policies:
usergroup_notification_plan += "\n_{} (".format(
Expand Down
2 changes: 1 addition & 1 deletion engine/apps/alerts/tasks/notify_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def notify_user_task(
user_has_notification = UserHasNotification.objects.filter(pk=user_has_notification.pk).select_for_update()[0]

if previous_notification_policy_pk is None:
notification_policy = UserNotificationPolicy.objects.filter(user=user, important=important).first()
notification_policy = user.get_or_create_notification_policies(important=important).first()
if notification_policy is None:
task_logger.info(
f"notify_user_task: Failed to notify. No notification policies. user_id={user_pk} alert_group_id={alert_group_pk} important={important}"
Expand Down
6 changes: 2 additions & 4 deletions engine/apps/api/views/user_notification_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,13 @@ def get_queryset(self):
except ValueError:
raise BadRequest(detail="Invalid user param")
if user_id is None or user_id == self.request.user.public_primary_key:
queryset = self.model.objects.filter(user=self.request.user, important=important)
target_user = self.request.user
else:
try:
target_user = User.objects.get(public_primary_key=user_id)
except User.DoesNotExist:
raise BadRequest(detail="User does not exist")

queryset = self.model.objects.filter(user=target_user, important=important)

queryset = target_user.get_or_create_notification_policies(important=important)
return self.serializer_class.setup_eager_loading(queryset)

def get_object(self):
Expand Down
6 changes: 2 additions & 4 deletions engine/apps/base/models/user_notification_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,9 @@ def create_default_policies_for_user(self, user: User) -> None:
model(
user=user,
step=model.Step.NOTIFY,
notify_by=NotificationChannelOptions.DEFAULT_NOTIFICATION_CHANNEL,
notify_by=settings.EMAIL_BACKED_INTERNAL_ID,
order=0,
),
model(user=user, step=model.Step.WAIT, wait_delay=datetime.timedelta(minutes=15), order=1),
model(user=user, step=model.Step.NOTIFY, notify_by=model.NotificationChannel.PHONE_CALL, order=2),
)

try:
Expand All @@ -97,7 +95,7 @@ def create_important_policies_for_user(self, user: User) -> None:
model(
user=user,
step=model.Step.NOTIFY,
notify_by=model.NotificationChannel.PHONE_CALL,
notify_by=settings.EMAIL_BACKED_INTERNAL_ID,
important=True,
order=0,
),
Expand Down
54 changes: 39 additions & 15 deletions engine/apps/user_management/models/team.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

from django.conf import settings
from django.core.validators import MinLengthValidator
from django.db import models
from django.db import models, transaction

from apps.metrics_exporter.helpers import metrics_bulk_update_team_label_cache
from apps.alerts.models import AlertReceiveChannel, ChannelFilter
from apps.metrics_exporter.helpers import metrics_add_integration_to_cache, metrics_bulk_update_team_label_cache
from apps.metrics_exporter.metrics_cache_manager import MetricsCacheManager
from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length

Expand Down Expand Up @@ -39,19 +40,42 @@ def sync_for_organization(
grafana_teams = {team["id"]: team for team in api_teams}
existing_team_ids: typing.Set[int] = set(organization.teams.all().values_list("team_id", flat=True))

# create missing teams
teams_to_create = tuple(
Team(
organization_id=organization.pk,
team_id=team["id"],
name=team["name"],
email=team["email"],
avatar_url=team["avatarUrl"],
)
for team in grafana_teams.values()
if team["id"] not in existing_team_ids
)
organization.teams.bulk_create(teams_to_create, batch_size=5000)
# create new teams, direct paging integration and default route
teams_to_create = []
direct_paging_integrations_to_create = []
default_channel_filters_to_create = []
for team in grafana_teams.values():
if team["id"] not in existing_team_ids:
team = Team(
organization_id=organization.pk,
team_id=team["id"],
name=team["name"],
email=team["email"],
avatar_url=team["avatarUrl"],
)
teams_to_create.append(team)
alert_receive_channel = AlertReceiveChannel(
organization=organization,
team=team,
integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING,
verbal_name=f"Direct paging ({team.name if team else 'No'} team)",
)
direct_paging_integrations_to_create.append(alert_receive_channel)
channel_filter = ChannelFilter(
alert_receive_channel=alert_receive_channel,
filtering_term=None,
is_default=True,
order=0,
)
default_channel_filters_to_create.append(channel_filter)
with transaction.atomic():
organization.teams.bulk_create(teams_to_create, batch_size=5000)
AlertReceiveChannel.objects.bulk_create(direct_paging_integrations_to_create, batch_size=5000)
ChannelFilter.objects.bulk_create(default_channel_filters_to_create, batch_size=5000)

# Add direct paging integrations to metrics cache
for integration in direct_paging_integrations_to_create:
metrics_add_integration_to_cache(integration)

# delete excess teams
team_ids_to_delete = existing_team_ids - grafana_teams.keys()
Expand Down
64 changes: 47 additions & 17 deletions engine/apps/user_management/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import pytz
from django.conf import settings
from django.core.validators import MinLengthValidator
from django.db import models
from django.db import models, transaction
from django.db.models import Q
from django.db.models.signals import post_save
from django.dispatch import receiver
Expand Down Expand Up @@ -75,25 +75,46 @@ def sync_for_team(team, api_members: list[dict]):

@staticmethod
def sync_for_organization(organization, api_users: list[dict]):
from apps.base.models import UserNotificationPolicy

grafana_users = {user["userId"]: user for user in api_users}
existing_user_ids = set(organization.users.all().values_list("user_id", flat=True))

# create missing users
users_to_create = tuple(
User(
organization_id=organization.pk,
user_id=user["userId"],
email=user["email"],
name=user["name"],
username=user["login"],
role=LegacyAccessControlRole[user["role"].upper()],
avatar_url=user["avatarUrl"],
permissions=user["permissions"],
)
for user in grafana_users.values()
if user["userId"] not in existing_user_ids
)
organization.users.bulk_create(users_to_create, batch_size=5000)
users_to_create = []
policies_to_create = []
for user in grafana_users.values():
if user["userId"] not in existing_user_ids:
user = User(
organization_id=organization.pk,
user_id=user["userId"],
email=user["email"],
name=user["name"],
username=user["login"],
role=LegacyAccessControlRole[user["role"].upper()],
avatar_url=user["avatarUrl"],
permissions=user["permissions"],
)
users_to_create.append(user)
policies_to_create.append(
UserNotificationPolicy(
user=user,
step=UserNotificationPolicy.Step.NOTIFY,
notify_by=settings.EMAIL_BACKEND_INTERNAL_ID,
order=0,
),
)
policies_to_create.append(
UserNotificationPolicy(
user=user,
step=UserNotificationPolicy.Step.NOTIFY,
notify_by=settings.EMAIL_BACKEND_INTERNAL_ID,
order=0,
important=True,
),
)
iskhakov marked this conversation as resolved.
Show resolved Hide resolved
with transaction.atomic():
organization.users.bulk_create(users_to_create, batch_size=5000)
UserNotificationPolicy.objects.bulk_create(policies_to_create, batch_size=5000)

# delete excess users
user_ids_to_delete = existing_user_ids - grafana_users.keys()
Expand Down Expand Up @@ -380,6 +401,15 @@ def build_permissions_query(
return PermissionsRegexQuery(permissions__regex=r".*{0}.*".format(permission.value))
return RoleInQuery(role__lte=permission.fallback_role.value)

def get_or_create_notification_policies(self, important=False):
iskhakov marked this conversation as resolved.
Show resolved Hide resolved
if not self.notification_policies.filter(important=important).exists():
if important:
self.notification_policies.create_important_policies_for_user(self)
else:
self.notification_policies.create_default_policies_for_user(self)
notification_policies = self.notification_policies.filter(important=important)
return notification_policies


# TODO: check whether this signal can be moved to save method of the model
@receiver(post_save, sender=User)
Expand Down
3 changes: 2 additions & 1 deletion engine/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -696,8 +696,9 @@ class BrokerTypes:
EMAIL_FROM_ADDRESS = os.getenv("EMAIL_FROM_ADDRESS")
EMAIL_NOTIFICATIONS_LIMIT = getenv_integer("EMAIL_NOTIFICATIONS_LIMIT", 200)

EMAIL_BACKEND_INTERNAL_ID = 8
if FEATURE_EMAIL_INTEGRATION_ENABLED:
EXTRA_MESSAGING_BACKENDS += [("apps.email.backend.EmailBackend", 8)]
EXTRA_MESSAGING_BACKENDS += [("apps.email.backend.EmailBackend", EMAIL_BACKEND_INTERNAL_ID)]

# Inbound email settings
INBOUND_EMAIL_ESP = os.getenv("INBOUND_EMAIL_ESP")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,11 @@
flex-direction: row;
margin-bottom: 4px;
max-width: 100%;
margin-left: 16px;

&__content {
width: 100%;
padding-top: 12px;
padding-bottom: 12px;
}

&__leftDelimitator {
border-left: var(--border-medium);
border-left-width: 3px;
margin-right: 16px;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ interface IntegrationBlockItemProps {
const IntegrationBlockItem: React.FC<IntegrationBlockItemProps> = (props) => {
return (
<div className={cx('blockItem')} data-testid="integration-block-item">
<div className={cx('blockItem__leftDelimitator')} />
<div className={cx('blockItem__content')}>{props.children}</div>
</div>
);
Expand Down
8 changes: 5 additions & 3 deletions grafana-plugin/src/components/Policy/EscalationPolicy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,11 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
textColor={isDisabled ? getVar('--tag-text-success') : undefined}
backgroundColor={backgroundColor}
>
<WithPermissionControlTooltip disableByPaywall userAction={UserActions.EscalationChainsWrite}>
<DragHandle />
</WithPermissionControlTooltip>
{!isDisabled && (
<WithPermissionControlTooltip disableByPaywall userAction={UserActions.EscalationChainsWrite}>
<DragHandle />
</WithPermissionControlTooltip>
)}
iskhakov marked this conversation as resolved.
Show resolved Hide resolved
{escalationOption &&
reactStringReplace(escalationOption.display_name, /\{\{([^}]+)\}\}/g, this.replacePlaceholder)}
{this._renderNote()}
Expand Down
Loading