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

v1.8.0 #4616

Merged
merged 20 commits into from
Jul 5, 2024
Merged

v1.8.0 #4616

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
bc62727
Post stack slug to chatops proxy (#4559)
Konstantinov-Innokentii Jun 21, 2024
615081a
Inject chatops-proxy metadata into direct paging user dropdown (#4556)
vstpme Jun 21, 2024
16e98da
Tweaks to overrides (added draggable bounds, take timezone into consi…
teodosii Jun 21, 2024
5cf921b
Add spacing between words in notifications. (#4574)
seizethedave Jun 21, 2024
48b7eca
Peridic chatops proxy sync (#4565)
Konstantinov-Innokentii Jun 24, 2024
c39dd8b
make links clickable in resolution notes (#4572)
brojd Jun 24, 2024
5ca0d13
Rename notify whole Slack channel (#4577)
brojd Jun 25, 2024
f6715e0
Update Makefile dev bootstrap; fix test (#4582)
matiasb Jun 25, 2024
d583727
add labels in grafana-incident alertgroup endpoint (#4448)
aron-bordin Jun 25, 2024
63663b2
Fix PhoneNumberBanned producing status code 500 (#4570)
mderynck Jun 25, 2024
acf8e39
Add START_SYNC_ORG_WITH_CHATOPS_PROXY_ENABLED (#4596)
iskhakov Jun 27, 2024
efd701e
FARO npe fixes (#4585)
teodosii Jun 27, 2024
c3c5433
Updated filters look (#4583)
teodosii Jun 27, 2024
9ed70ba
Changed polling to 10s for alert groups (#4586)
teodosii Jun 27, 2024
f27eb23
Shift length limit fix for monthly views (#4588)
teodosii Jun 27, 2024
7980382
make `make cleanup` prune volumes (#4600)
vstpme Jun 27, 2024
2942152
Minor plugin description update (#4599)
matiasb Jun 27, 2024
6e0beba
Make Slack URLs work without trailing slashes (#4607)
vstpme Jul 3, 2024
abedea7
don't force create default user notification policies (#4608)
joeyorlando Jul 5, 2024
0261272
Merge branch 'main' into dev
joeyorlando Jul 5, 2024
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
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
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
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
Loading