Skip to content

Commit

Permalink
Merge pull request #5128 from grafana/dev
Browse files Browse the repository at this point in the history
v1.10.2
  • Loading branch information
mderynck authored Oct 4, 2024
2 parents a5707f2 + ac7dc97 commit 305801a
Show file tree
Hide file tree
Showing 8 changed files with 832 additions and 42 deletions.
58 changes: 52 additions & 6 deletions engine/apps/alerts/models/alert_group_log_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,14 +227,50 @@ class AlertGroupLogRecord(models.Model):

STEP_SPECIFIC_INFO_KEYS = ["schedule_name", "custom_button_name", "usergroup_handle", "source_integration_name"]

def _make_log_line_link(self, url, title, html=False, for_slack=False, substitute_with_tag=False):
if html and url:
return f"<a href='{url}'>{title}</a>"
elif for_slack and url:
return f"<{url}|{title}>"
elif substitute_with_tag:
return f"{{{{{substitute_with_tag}}}}}"
else:
return title

def render_log_line_json(self):
time = humanize.naturaldelta(self.alert_group.started_at - self.created_at)
created_at = DateTimeField().to_representation(self.created_at)
organization = self.alert_group.channel.organization
author = self.author.short(organization) if self.author is not None else None
escalation_chain = self.alert_group.channel_filter.escalation_chain if self.alert_group.channel_filter else None
step_info = self.get_step_specific_info()
escalation_chain_data = (
{
"pk": escalation_chain.public_primary_key,
"title": escalation_chain.name,
}
if escalation_chain
else None
)
schedule = (
{
"pk": self.escalation_policy.notify_schedule.public_primary_key,
"title": self.escalation_policy.notify_schedule.name,
}
if self.escalation_policy and self.escalation_policy.notify_schedule
else None
)
webhook = (
{
"pk": step_info["webhook_id"],
"title": step_info.get("webhook_name", "webhook"),
}
if step_info and "webhook_id" in step_info
else None
)

sf = SlackFormatter(organization)
action = sf.format(self.rendered_log_line_action(substitute_author_with_tag=True))
action = sf.format(self.rendered_log_line_action(substitute_with_tag=True))
action = clean_markup(action)

result = {
Expand All @@ -244,6 +280,9 @@ def render_log_line_json(self):
"type": self.type,
"created_at": created_at,
"author": author,
"escalation_chain": escalation_chain_data,
"schedule": schedule,
"webhook": webhook,
}
return result

Expand All @@ -258,7 +297,7 @@ def rendered_incident_log_line(self, for_slack=False, html=False):
result += self.rendered_log_line_action(for_slack=for_slack, html=html)
return result

def rendered_log_line_action(self, for_slack=False, html=False, substitute_author_with_tag=False):
def rendered_log_line_action(self, for_slack=False, html=False, substitute_with_tag=False):
from apps.alerts.models import EscalationPolicy

result = ""
Expand All @@ -276,7 +315,7 @@ def rendered_log_line_action(self, for_slack=False, html=False, substitute_autho
elif self.action_source == ActionSource.BACKSYNC:
author_name = "source integration " + step_specific_info.get("source_integration_name", "")
elif self.author:
if substitute_author_with_tag:
if substitute_with_tag:
author_name = "{{author}}"
elif for_slack:
author_name = self.author.get_username_with_slack_verbal()
Expand All @@ -303,7 +342,9 @@ def rendered_log_line_action(self, for_slack=False, html=False, substitute_autho
result += f'alert group assigned to route "{channel_filter.str_for_clients}"'

if escalation_chain is not None:
result += f' with escalation chain "{escalation_chain.name}"'
tag = "escalation_chain" if substitute_with_tag else False
escalation_chain_text = self._make_log_line_link(None, escalation_chain.name, html, for_slack, tag)
result += f' with escalation chain "{escalation_chain_text}"'
else:
result += " with no escalation chain, skipping escalation"
else:
Expand Down Expand Up @@ -379,7 +420,9 @@ def rendered_log_line_action(self, for_slack=False, html=False, substitute_autho
important_text = ""
if escalation_policy_step == EscalationPolicy.STEP_NOTIFY_SCHEDULE_IMPORTANT:
important_text = " (Important)"
result += f'triggered step "Notify on-call from Schedule {schedule_name}{important_text}"'
tag = "schedule" if substitute_with_tag else False
schedule_text = self._make_log_line_link(None, schedule_name, html, for_slack, tag)
result += f'triggered step "Notify on-call from Schedule {schedule_text}{important_text}"'
elif escalation_policy_step == EscalationPolicy.STEP_REPEAT_ESCALATION_N_TIMES:
result += "escalation started from the beginning"
else:
Expand Down Expand Up @@ -485,7 +528,10 @@ def rendered_log_line_action(self, for_slack=False, html=False, substitute_autho
trigger = f"{author_name}"
else:
trigger = trigger or "escalation chain"
result += f"outgoing webhook `{webhook_name}` triggered by {trigger}"
tag = "webhook" if substitute_with_tag else False
webhook_text = self._make_log_line_link(None, webhook_name, html, for_slack, tag)
result += f"outgoing webhook `{webhook_text}` triggered by {trigger}"

elif self.type == AlertGroupLogRecord.TYPE_FAILED_ATTACHMENT:
if self.alert_group.slack_message is not None:
result += (
Expand Down
138 changes: 137 additions & 1 deletion engine/apps/alerts/tests/test_alert_group_log_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import pytest

from apps.alerts.models import AlertGroupLogRecord
from apps.alerts.models import AlertGroupLogRecord, EscalationPolicy
from apps.schedules.models import OnCallScheduleWeb


@pytest.mark.django_db
Expand Down Expand Up @@ -37,3 +38,138 @@ def test_trigger_update_signal(
with patch("apps.alerts.tasks.send_update_log_report_signal") as mock_update_log_signal:
alert_group.log_records.create(type=log_type)
mock_update_log_signal.apply_async.assert_called_once()


@pytest.mark.django_db
@pytest.mark.parametrize(
"for_slack, html, substitute_with_tag, expected",
[
(True, False, False, 'with escalation chain "Escalation name"'),
(False, True, False, 'with escalation chain "Escalation name"'),
(False, False, True, 'with escalation chain "{{escalation_chain}}'),
],
)
def test_log_record_escalation_chain_link(
make_organization_with_slack_team_identity,
make_alert_receive_channel,
make_escalation_chain,
make_channel_filter,
make_alert_group,
for_slack,
html,
substitute_with_tag,
expected,
):
organization, _ = make_organization_with_slack_team_identity()
alert_receive_channel = make_alert_receive_channel(organization)
escalation_chain = make_escalation_chain(organization, name="Escalation name")
channel_filter = make_channel_filter(alert_receive_channel, escalation_chain=escalation_chain)
alert_group = make_alert_group(alert_receive_channel, channel_filter=channel_filter)
alert_group.raw_escalation_snapshot = alert_group.build_raw_escalation_snapshot()

log = alert_group.log_records.create(
type=AlertGroupLogRecord.TYPE_ROUTE_ASSIGNED,
)

log_line = log.rendered_log_line_action(for_slack=for_slack, html=html, substitute_with_tag=substitute_with_tag)
assert expected in log_line

log_data = log.render_log_line_json()
escalation_chain_data = log_data.get("escalation_chain")
assert escalation_chain_data == {"pk": escalation_chain.public_primary_key, "title": escalation_chain.name}


@pytest.mark.django_db
@pytest.mark.parametrize(
"for_slack, html, substitute_with_tag, expected",
[
(True, False, False, "Notify on-call from Schedule 'Schedule name'"),
(False, True, False, "Notify on-call from Schedule 'Schedule name'"),
(False, False, True, "Notify on-call from Schedule {{schedule}}"),
],
)
def test_log_record_schedule_link(
make_organization_with_slack_team_identity,
make_alert_receive_channel,
make_channel_filter,
make_alert_group,
make_schedule,
make_escalation_chain,
make_escalation_policy,
for_slack,
html,
substitute_with_tag,
expected,
):
organization, _ = make_organization_with_slack_team_identity()
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel)
schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb, name="Schedule name")
escalation_chain = make_escalation_chain(organization, name="Escalation name")
channel_filter = make_channel_filter(alert_receive_channel, escalation_chain=escalation_chain)
escalation_policy = make_escalation_policy(
escalation_chain=channel_filter.escalation_chain,
escalation_policy_step=EscalationPolicy.STEP_NOTIFY_SCHEDULE,
notify_schedule=schedule,
)

log = alert_group.log_records.create(
type=AlertGroupLogRecord.TYPE_ESCALATION_TRIGGERED,
step_specific_info={"schedule_name": schedule.name},
escalation_policy=escalation_policy,
)

log_line = log.rendered_log_line_action(for_slack=for_slack, html=html, substitute_with_tag=substitute_with_tag)
assert expected in log_line

log_data = log.render_log_line_json()
schedule_data = log_data.get("schedule")
assert schedule_data == {"pk": schedule.public_primary_key, "title": schedule.name}


@pytest.mark.django_db
@pytest.mark.parametrize(
"for_slack, html, substitute_with_tag, expected",
[
(True, False, False, "outgoing webhook `Webhook name`"),
(False, True, False, "outgoing webhook `Webhook name`"),
(False, False, True, "outgoing webhook `{{webhook}}`"),
],
)
def test_log_record_webhook_link(
make_organization_with_slack_team_identity,
make_alert_receive_channel,
make_channel_filter,
make_alert_group,
make_custom_webhook,
make_escalation_chain,
make_escalation_policy,
for_slack,
html,
substitute_with_tag,
expected,
):
organization, _ = make_organization_with_slack_team_identity()
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel)
webhook = make_custom_webhook(organization, name="Webhook name")
escalation_chain = make_escalation_chain(organization, name="Escalation name")
channel_filter = make_channel_filter(alert_receive_channel, escalation_chain=escalation_chain)
escalation_policy = make_escalation_policy(
escalation_chain=channel_filter.escalation_chain,
escalation_policy_step=EscalationPolicy.STEP_TRIGGER_CUSTOM_WEBHOOK,
custom_webhook=webhook,
)

log = alert_group.log_records.create(
type=AlertGroupLogRecord.TYPE_CUSTOM_WEBHOOK_TRIGGERED,
step_specific_info={"webhook_id": webhook.public_primary_key, "webhook_name": webhook.name},
escalation_policy=escalation_policy,
)

log_line = log.rendered_log_line_action(for_slack=for_slack, html=html, substitute_with_tag=substitute_with_tag)
assert expected in log_line

log_data = log.render_log_line_json()
webhook_data = log_data.get("webhook")
assert webhook_data == {"pk": webhook.public_primary_key, "title": webhook.name}
48 changes: 47 additions & 1 deletion engine/apps/public_api/tests/test_resolution_notes.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,14 @@ def test_get_resolution_note(
assert response.data == result


@patch("apps.alerts.tasks.send_update_resolution_note_signal.send_update_resolution_note_signal.apply_async")
@pytest.mark.django_db
def test_create_resolution_note(make_organization_and_user_with_token, make_alert_receive_channel, make_alert_group):
def test_create_resolution_note(
mock_send_update_resolution_note_signal,
make_organization_and_user_with_token,
make_alert_receive_channel,
make_alert_group,
):
organization, user, token = make_organization_and_user_with_token()
client = APIClient()

Expand Down Expand Up @@ -137,6 +143,8 @@ def test_create_resolution_note(make_organization_and_user_with_token, make_aler
assert response.status_code == status.HTTP_201_CREATED
assert response.data == result

mock_send_update_resolution_note_signal.assert_called_once()


@pytest.mark.django_db
def test_create_resolution_note_invalid_text(
Expand All @@ -163,8 +171,10 @@ def test_create_resolution_note_invalid_text(
assert response.data["text"][0] == "This field may not be blank."


@patch("apps.alerts.tasks.send_update_resolution_note_signal.send_update_resolution_note_signal.apply_async")
@pytest.mark.django_db
def test_update_resolution_note(
mock_send_update_resolution_note_signal,
make_organization_and_user_with_token,
make_alert_receive_channel,
make_alert_group,
Expand Down Expand Up @@ -206,6 +216,39 @@ def test_update_resolution_note(
assert resolution_note.text == result["text"]
assert response.data == result

mock_send_update_resolution_note_signal.assert_called_once()


@patch("apps.alerts.tasks.send_update_resolution_note_signal.send_update_resolution_note_signal.apply_async")
@pytest.mark.django_db
def test_update_resolution_note_same_text(
mock_send_update_resolution_note_signal,
make_organization_and_user_with_token,
make_alert_receive_channel,
make_alert_group,
make_resolution_note,
):
organization, user, token = make_organization_and_user_with_token()
client = APIClient()

alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel)

resolution_note = make_resolution_note(
alert_group=alert_group,
source=ResolutionNote.Source.WEB,
author=user,
)

url = reverse("api-public:resolution_notes-detail", kwargs={"pk": resolution_note.public_primary_key})
response = client.put(
url, data={"text": resolution_note.message_text}, format="json", HTTP_AUTHORIZATION=f"{token}"
)
assert response.status_code == status.HTTP_200_OK

# update signal shouldn't be sent when text doesn't change
mock_send_update_resolution_note_signal.assert_not_called()


@pytest.mark.django_db
def test_update_resolution_note_invalid_source(
Expand Down Expand Up @@ -242,8 +285,10 @@ def test_update_resolution_note_invalid_source(
assert response.data["detail"] == "Cannot update message with this source type"


@patch("apps.alerts.tasks.send_update_resolution_note_signal.send_update_resolution_note_signal.apply_async")
@pytest.mark.django_db
def test_delete_resolution_note(
mock_send_update_resolution_note_signal,
make_organization_and_user_with_token,
make_alert_receive_channel,
make_alert_group,
Expand Down Expand Up @@ -272,6 +317,7 @@ def test_delete_resolution_note(
resolution_note.refresh_from_db()

assert resolution_note.deleted_at is not None
mock_send_update_resolution_note_signal.assert_called_once()

response = client.get(url, format="json", HTTP_AUTHORIZATION=f"{token}")

Expand Down
28 changes: 12 additions & 16 deletions engine/apps/public_api/views/resolution_notes.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,20 +56,16 @@ def get_object(self):
except ResolutionNote.DoesNotExist:
raise NotFound

def dispatch(self, request, *args, **kwargs):
result = super().dispatch(request, *args, **kwargs)
def perform_create(self, serializer):
super().perform_create(serializer)
send_update_resolution_note_signal.apply_async((serializer.instance.alert_group.pk, serializer.instance.pk))

# send signal to update alert group and resolution_note
method = request.method.lower()
if method in ["post", "put", "patch", "delete"]:
instance_id = self.kwargs.get("pk") or result.data.get("id")
if instance_id:
instance = ResolutionNote.objects_with_deleted.filter(public_primary_key=instance_id).first()
if instance is not None:
send_update_resolution_note_signal.apply_async(
kwargs={
"alert_group_pk": instance.alert_group.pk,
"resolution_note_pk": instance.pk,
}
)
return result
def perform_update(self, serializer):
is_text_updated = serializer.instance.message_text != serializer.validated_data["message_text"]
super().perform_update(serializer)
if is_text_updated:
send_update_resolution_note_signal.apply_async((serializer.instance.alert_group.pk, serializer.instance.pk))

def perform_destroy(self, instance):
super().perform_destroy(instance)
send_update_resolution_note_signal.apply_async((instance.alert_group.pk, instance.pk))
Loading

0 comments on commit 305801a

Please sign in to comment.