Skip to content

Commit

Permalink
v1.8.0
Browse files Browse the repository at this point in the history
  • Loading branch information
joeyorlando authored Jul 5, 2024
2 parents 7cf2a2c + 0261272 commit 46017ac
Show file tree
Hide file tree
Showing 66 changed files with 872 additions and 506 deletions.
9 changes: 5 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@ build: ## rebuild images (e.g. when changing requirements.txt)
cleanup: stop ## this will remove all of the images, containers, volumes, and networks
## associated with your local OnCall developer setup
$(call echo_deprecation_message)
docker system prune --filter label="$(DOCKER_COMPOSE_DEV_LABEL)" --all --volumes
docker system prune --filter label="$(DOCKER_COMPOSE_DEV_LABEL)" --all --volumes --force
docker volume prune --filter label="$(DOCKER_COMPOSE_DEV_LABEL)" --all --force

install-pre-commit:
@if [ ! -x "$$(command -v pre-commit)" ]; then \
Expand Down Expand Up @@ -245,17 +246,17 @@ pip-compile-locked-dependencies: ## compile engine requirements.txt files
define backend_command
export `grep -v '^#' $(DEV_ENV_FILE) | xargs -0` && \
export BROKER_TYPE=$(BROKER_TYPE) && \
. ./venv/bin/activate && \
. $(VENV_DIR)/bin/activate && \
cd engine && \
$(1)
endef

backend-bootstrap:
python3.12 -m venv $(VENV_DIR)
$(VENV_DIR)/bin/pip install -U pip wheel uv
$(VENV_DIR)/bin/uv pip sync $(REQUIREMENTS_TXT) $(REQUIREMENTS_DEV_TXT)
$(VENV_DIR)/bin/uv pip sync --python=$(VENV_DIR)/bin/python $(REQUIREMENTS_TXT) $(REQUIREMENTS_DEV_TXT)
@if [ -f $(REQUIREMENTS_ENTERPRISE_TXT) ]; then \
$(VENV_DIR)/bin/uv pip install -r $(REQUIREMENTS_ENTERPRISE_TXT); \
$(VENV_DIR)/bin/uv pip install --python=$(VENV_DIR)/bin/python -r $(REQUIREMENTS_ENTERPRISE_TXT); \
fi

backend-migrate:
Expand Down
4 changes: 2 additions & 2 deletions docs/sources/configure/escalation-chains-and-routes/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ from an on-call schedule.
* `Notify all users from a team` - send a notification to all users in a team.
* `Resolve incident automatically` - resolve the alert group right now with status
`Resolved automatically`.
* `Notify whole slack channel` - send a notification to the users in the slack channel. These users will be notified
* `Escalate to all Slack channel members` - send a notification to the users in the slack channel. These users will be notified
via the method configured in their user profile.
* `Notify Slack User Group` - send a notification to each member of a slack user group. These users will be notified
via the method configured in their user profile.
Expand All @@ -97,7 +97,7 @@ Useful when you want to get escalation only during working hours
passes some threshold
* `Repeat escalation from beginning (5 times max)` - loop the escalation chain

> **Note:** Both "**Notify whole Slack channel**" and "**Notify Slack User Group**" will filter OnCall registered users
> **Note:** Both "**Escalate to all Slack channel members**" and "**Notify Slack User Group**" will filter OnCall registered users
matching the users in the Slack channel or Slack User Group with their profiles linked to their Slack accounts (ie. users
should have linked their Slack and OnCall users). In both cases, the filtered users satisfying the criteria above are
notified following their respective notification policies. However, to avoid **spamming** the Slack channel/thread,
Expand Down
2 changes: 1 addition & 1 deletion docs/sources/manage/notify/slack/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ and users:
Once your Slack integration is configured you can configure Escalation Chains to notify via Slack messages for alerts
in Grafana OnCall.

There are two Slack notification options that you can configure into escalation chains, notify whole Slack channel and
There are two Slack notification options that you can configure into escalation chains, escalate to all Slack channel members and
notify Slack user group:

1. In Grafana OnCall, navigate to the **Escalation Chains** tab then select an existing escalation chain or
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ The above command returns JSON structured in the following way:
| ----------- | :------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `user_id` | Yes | User ID |
| `position` | Optional | Personal notification rules execute one after another starting from `position=0`. `Position=-1` will put the escalation policy to the end of the list. A new escalation policy created with a position of an existing escalation policy will move the old one (and all following) down on the list. |
| `type` | Yes | One of: `wait`, `notify_by_slack`, `notify_by_sms`, `notify_by_phone_call`, `notify_by_telegram`, `notify_by_email`. |
| `duration` | Optional | A time in secs when type `wait` is chosen for `type`. |
| `type` | Yes | One of: `wait`, `notify_by_slack`, `notify_by_sms`, `notify_by_phone_call`, `notify_by_telegram`, `notify_by_email`, `notify_by_mobile_app`, `notify_by_mobile_app_critical`. |
| `duration` | Optional | A time in seconds to wait (when `type=wait`). Can be one of 60, 300, 900, 1800, or 3600. |
| `important` | Optional | Boolean value indicates if a rule is "important". Default is `false`. |

**HTTP request**
Expand Down
13 changes: 9 additions & 4 deletions engine/apps/alerts/incident_log_builder/incident_log_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
from django.db.models.manager import RelatedManager

from apps.alerts.models import AlertGroup, AlertGroupLogRecord, ResolutionNote
from apps.base.models import UserNotificationPolicyLogRecord
from apps.base.models import UserNotificationPolicy, UserNotificationPolicyLogRecord
from apps.user_management.models import User


class IncidentLogBuilder:
Expand Down Expand Up @@ -578,7 +579,9 @@ def _render_escalation_step_plan_from_escalation_policy_snapshot(
escalation_plan_dict.setdefault(timedelta, []).append(plan)
return escalation_plan_dict

def _render_user_notification_line(self, user_to_notify, notification_policy, for_slack=False):
def _render_user_notification_line(
self, user_to_notify: "User", notification_policy: "UserNotificationPolicy", for_slack=False
):
"""
Renders user notification plan line
:param user_to_notify:
Expand Down Expand Up @@ -611,7 +614,9 @@ def _render_user_notification_line(self, user_to_notify, notification_policy, fo
result += f"inviting {user_verbal} but notification channel is unspecified"
return result

def _get_notification_plan_for_user(self, user_to_notify, future_step=False, important=False, for_slack=False):
def _get_notification_plan_for_user(
self, user_to_notify: "User", future_step=False, important=False, for_slack=False
):
"""
Renders user notification plan
:param user_to_notify:
Expand Down Expand Up @@ -665,7 +670,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 = user_to_notify.get_or_create_notification_policies(important=important)
notification_policies = user_to_notify.get_notification_policies_or_use_default_fallback(important=important)

for notification_policy in notification_policies:
future_notification = notification_policy.order >= notification_policy_order
Expand Down
5 changes: 4 additions & 1 deletion engine/apps/alerts/models/escalation_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,10 @@ class EscalationPolicy(OrderedModel):
),
STEP_FINAL_RESOLVE: ("Resolve alert group automatically", "Resolve alert group automatically"),
# Slack
STEP_FINAL_NOTIFYALL: ("Notify whole Slack channel", "Notify whole Slack channel"),
STEP_FINAL_NOTIFYALL: (
"Escalate to all Slack channel members (use with caution)",
"Escalate to all Slack channel members (use with caution)",
),
STEP_NOTIFY_GROUP: (
"Start {{importance}} notification for everyone from Slack User Group {{slack_user_group}}",
"Notify Slack User Group",
Expand Down
10 changes: 5 additions & 5 deletions engine/apps/alerts/tasks/notify_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,22 +83,22 @@ def notify_group_task(alert_group_pk, escalation_policy_snapshot_order=None):
continue

important = escalation_policy_step == EscalationPolicy.STEP_NOTIFY_GROUP_IMPORTANT
notification_policies = user.get_or_create_notification_policies(important=important)
notification_policies = user.get_notification_policies_or_use_default_fallback(important=important)

if notification_policies:
usergroup_notification_plan += "\n_{} (".format(
step.get_user_notification_message_for_thread_for_usergroup(user, notification_policies.first())
step.get_user_notification_message_for_thread_for_usergroup(user, notification_policies[0])
)

notification_channels = []
if notification_policies.filter(step=UserNotificationPolicy.Step.NOTIFY).count() == 0:
else:
usergroup_notification_plan += "Empty notifications"

notification_channels = []
for notification_policy in notification_policies:
if notification_policy.step == UserNotificationPolicy.Step.NOTIFY:
notification_channels.append(
UserNotificationPolicy.NotificationChannel(notification_policy.notify_by).label
)

usergroup_notification_plan += "→".join(notification_channels) + ")_"
reason = f"Membership in <!subteam^{usergroup.slack_id}> User Group"

Expand Down
16 changes: 8 additions & 8 deletions engine/apps/alerts/tasks/notify_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,20 +72,20 @@ 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 = user.get_or_create_notification_policies(important=important).first()
if notification_policy is None:
notification_policies = user.get_notification_policies_or_use_default_fallback(important=important)
if not notification_policies:
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}"
)
return

# Here we collect a brief overview of notification steps configured for user to send it to thread.
collected_steps_ids = []
next_notification_policy = notification_policy.next()
while next_notification_policy is not None:
if next_notification_policy.step == UserNotificationPolicy.Step.NOTIFY:
if next_notification_policy.notify_by not in collected_steps_ids:
collected_steps_ids.append(next_notification_policy.notify_by)
next_notification_policy = next_notification_policy.next()
for notification_policy in notification_policies:
if notification_policy.step == UserNotificationPolicy.Step.NOTIFY:
if notification_policy.notify_by not in collected_steps_ids:
collected_steps_ids.append(notification_policy.notify_by)

collected_steps = ", ".join(
UserNotificationPolicy.NotificationChannel(step_id).label for step_id in collected_steps_ids
)
Expand Down
5 changes: 5 additions & 0 deletions engine/apps/api/views/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
FailedToStartVerification,
NumberAlreadyVerified,
NumberNotVerified,
PhoneNumberBanned,
ProviderNotSupports,
)
from apps.phone_notifications.phone_backend import PhoneBackend
Expand Down Expand Up @@ -478,6 +479,8 @@ def get_verification_code(self, request, pk) -> Response:
phone_backend.send_verification_sms(user)
except NumberAlreadyVerified:
return Response("Phone number already verified", status=status.HTTP_400_BAD_REQUEST)
except PhoneNumberBanned:
return Response("Phone number has been banned", status=status.HTTP_403_FORBIDDEN)
except FailedToStartVerification as e:
return handle_phone_notificator_failed(e)
except ProviderNotSupports:
Expand Down Expand Up @@ -505,6 +508,8 @@ def get_verification_call(self, request, pk) -> Response:
phone_backend.make_verification_call(user)
except NumberAlreadyVerified:
return Response("Phone number already verified", status=status.HTTP_400_BAD_REQUEST)
except PhoneNumberBanned:
return Response("Phone number has been banned", status=status.HTTP_403_FORBIDDEN)
except FailedToStartVerification as e:
return handle_phone_notificator_failed(e)
except ProviderNotSupports:
Expand Down
8 changes: 2 additions & 6 deletions engine/apps/api/views/user_notification_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
from apps.user_management.models import User
from common.api_helpers.exceptions import BadRequest
from common.api_helpers.mixins import UpdateSerializerMixin
from common.exceptions import UserNotificationPolicyCouldNotBeDeleted
from common.insight_log import EntityEvent, write_resource_insight_log
from common.ordered_model.viewset import OrderedModelViewSet

Expand Down Expand Up @@ -73,7 +72,7 @@ def get_queryset(self):
target_user = User.objects.get(public_primary_key=user_id)
except User.DoesNotExist:
raise BadRequest(detail="User does not exist")
queryset = target_user.get_or_create_notification_policies(important=important)
queryset = UserNotificationPolicy.objects.filter(user=target_user, important=important)
return self.serializer_class.setup_eager_loading(queryset)

def get_object(self):
Expand Down Expand Up @@ -119,10 +118,7 @@ def perform_update(self, serializer):
def perform_destroy(self, instance):
user = instance.user
prev_state = user.insight_logs_serialized
try:
instance.delete()
except UserNotificationPolicyCouldNotBeDeleted:
raise BadRequest(detail="Can't delete last user notification policy")
instance.delete()
new_state = user.insight_logs_serialized
write_resource_insight_log(
instance=user,
Expand Down
15 changes: 15 additions & 0 deletions engine/apps/api_for_grafana_incident/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from apps.alerts.incident_appearance.renderers.web_renderer import AlertGroupWebRenderer
from apps.alerts.models import Alert, AlertGroup
from apps.api.serializers.alert_group import AlertGroupFieldsCacheSerializerMixin
from apps.labels.models import AlertGroupAssociatedLabel

logger = logging.getLogger(__name__)

Expand All @@ -21,12 +22,25 @@ class Meta:
]


class LabelsSerializer(serializers.ModelSerializer):
key = serializers.CharField(read_only=True, source="key_name")
value = serializers.CharField(read_only=True, source="value_name")

class Meta:
model = AlertGroupAssociatedLabel
fields = [
"key",
"value",
]


class AlertGroupSerializer(serializers.ModelSerializer):
id = serializers.CharField(read_only=True, source="public_primary_key")
status = serializers.SerializerMethodField(source="get_status")
link = serializers.CharField(read_only=True, source="web_link")
title = serializers.CharField(read_only=True, source="long_verbose_name_without_formatting")
alerts = AlertSerializer(many=True, read_only=True)
labels = LabelsSerializer(many=True, read_only=True)

render_for_web = serializers.SerializerMethodField()

Expand All @@ -53,4 +67,5 @@ class Meta:
"alerts",
"title",
"render_for_web",
"labels",
]
42 changes: 42 additions & 0 deletions engine/apps/api_for_grafana_incident/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@
from django.urls import reverse
from rest_framework.test import APIClient

from apps.metrics_exporter.constants import SERVICE_LABEL
from apps.metrics_exporter.tests.conftest import METRICS_TEST_SERVICE_NAME


@pytest.mark.django_db
def test_alert_group_details(
make_organization,
make_alert_receive_channel,
make_alert_group,
make_alert,
make_alert_group_label_association,
settings,
):
settings.GRAFANA_INCIDENT_STATIC_API_KEY = "test-key"
Expand Down Expand Up @@ -40,6 +44,44 @@ def test_alert_group_details(
"payload": alert_payload,
}
],
"labels": [],
"render_for_web": {
"title": "title: bar",
"message": "<p>Something foo + baz</p>",
"image_url": "http://foo",
"source_link": None,
},
}
assert response.json() == expected
# enable labels feature flag
settings.FEATURE_LABELS_ENABLED_FOR_ALL = True
alert_group_with_labels = make_alert_group(alert_receive_channel)
alert_with_labels = make_alert(alert_group_with_labels, alert_payload)
_ = make_alert_group_label_association(
organization, alert_group_with_labels, key_name=SERVICE_LABEL, value_name=METRICS_TEST_SERVICE_NAME
)

url = reverse(
"api-gi:alert-groups-detail", kwargs={"public_primary_key": alert_group_with_labels.public_primary_key}
)
response = client.get(url, format="json", **headers)
expected = {
"id": alert_group_with_labels.public_primary_key,
"link": alert_group_with_labels.web_link,
"status": "new",
"title": alert_group_with_labels.long_verbose_name_without_formatting,
"alerts": [
{
"id_oncall": alert_with_labels.public_primary_key,
"payload": alert_payload,
}
],
"labels": [
{
"key": "service_name",
"value": "test_service",
}
],
"render_for_web": {
"title": "title: bar",
"message": "<p>Something foo + baz</p>",
Expand Down
Loading

0 comments on commit 46017ac

Please sign in to comment.