Skip to content

Commit

Permalink
Public API: Acknowledge & Resolve actions (#3108)
Browse files Browse the repository at this point in the history
# What this PR does

Makes it possible to acknowledge/unacknowledge and resolve/unresolve
alert groups via public API, and makes sure these actions are reflected
properly in the alert group timeline.

## Demo

```bash
curl --request POST \
     --header "Authorization: TOKEN" \
     http://localhost:8080/api/v1/alert_groups/IQMHLV8INB24N/resolve
```

<img width="651" alt="Screenshot 2023-10-04 at 16 05 27"
src="https://github.com/grafana/oncall/assets/20116910/d4e66868-0132-4b6b-95c7-8424fced7c0b">

## Which issue(s) this PR fixes

#3051

## Checklist

- [x] Unit, integration, and e2e (if applicable) tests updated
- [x] Documentation added (or `pr:no public docs` PR label added if not
required)
- [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not
required)
  • Loading branch information
vstpme authored Oct 5, 2023
1 parent 6dcd443 commit a727450
Show file tree
Hide file tree
Showing 10 changed files with 461 additions and 37 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

### Added

- Make it possible to acknowledge/unacknowledge and resolve/unresolve alert groups via API by @vadimkerr ([#3108](https://github.com/grafana/oncall/pull/3108))

## v1.3.42 (2023-10-04)

### Added
Expand Down
48 changes: 48 additions & 0 deletions docs/sources/oncall-api-reference/alertgroups.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,54 @@ These available filter parameters should be provided as `GET` arguments:

`GET {{API_URL}}/api/v1/alert_groups/`

# Acknowledge alert groups

```shell
curl "{{API_URL}}/api/v1/alert_groups/I68T24C13IFW1/acknowledge" \
--request POST \
--header "Authorization: meowmeowmeow"
```

**HTTP request**

`POST {{API_URL}}/api/v1/alert_groups/<ALERT_GROUP_ID>/acknowledge`

# Unacknowledge alert groups

```shell
curl "{{API_URL}}/api/v1/alert_groups/I68T24C13IFW1/unacknowledge" \
--request POST \
--header "Authorization: meowmeowmeow"
```

**HTTP request**

`POST {{API_URL}}/api/v1/alert_groups/<ALERT_GROUP_ID>/unacknowledge`

# Resolve alert groups

```shell
curl "{{API_URL}}/api/v1/alert_groups/I68T24C13IFW1/resolve" \
--request POST \
--header "Authorization: meowmeowmeow"
```

**HTTP request**

`POST {{API_URL}}/api/v1/alert_groups/<ALERT_GROUP_ID>/resolve`

# Unresolve alert groups

```shell
curl "{{API_URL}}/api/v1/alert_groups/I68T24C13IFW1/unresolve" \
--request POST \
--header "Authorization: meowmeowmeow"
```

**HTTP request**

`POST {{API_URL}}/api/v1/alert_groups/<ALERT_GROUP_ID>/unresolve`

# Delete alert groups

```shell
Expand Down
15 changes: 8 additions & 7 deletions engine/apps/alerts/constants.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
from enum import Enum

from django.db.models import IntegerChoices

class ActionSource:
(
SLACK,
WEB,
PHONE,
TELEGRAM,
) = range(4)

class ActionSource(IntegerChoices):
SLACK = 0, "Slack"
WEB = 1, "Web"
PHONE = 2, "Phone"
TELEGRAM = 3, "Telegram"
API = 4, "API"


TASK_DELAY_SECONDS = 1
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.20 on 2023-10-04 10:11

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('alerts', '0032_remove_alertgroup_slack_message_state'),
]

operations = [
migrations.AddField(
model_name='alertgrouplogrecord',
name='action_source',
field=models.SmallIntegerField(default=None, null=True, verbose_name=[(0, 'Slack'), (1, 'Web'), (2, 'Phone'), (3, 'Telegram'), (4, 'API')]),
),
]
62 changes: 46 additions & 16 deletions engine/apps/alerts/models/alert_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from django.utils import timezone
from django.utils.functional import cached_property

from apps.alerts.constants import AlertGroupState
from apps.alerts.constants import ActionSource, AlertGroupState
from apps.alerts.escalation_snapshot import EscalationSnapshotMixin
from apps.alerts.escalation_snapshot.escalation_snapshot_mixin import START_ESCALATION_DELAY
from apps.alerts.incident_appearance.renderers.constants import DEFAULT_BACKUP_TITLE
Expand Down Expand Up @@ -550,7 +550,7 @@ def _update_metrics(self, organization_id, previous_state, state):
started_at=self.started_at,
)

def acknowledge_by_user(self, user: User, action_source: typing.Optional[str] = None) -> None:
def acknowledge_by_user(self, user: User, action_source: typing.Optional[ActionSource] = None) -> None:
from apps.alerts.models import AlertGroupLogRecord

initial_state = self.state
Expand All @@ -564,10 +564,16 @@ def acknowledge_by_user(self, user: User, action_source: typing.Optional[str] =
author=user,
silence_delay=None,
reason="Acknowledge button",
action_source=action_source,
)
if self.resolved:
self.unresolve()
self.log_records.create(type=AlertGroupLogRecord.TYPE_UN_RESOLVED, author=user, reason="Acknowledge button")
self.log_records.create(
type=AlertGroupLogRecord.TYPE_UN_RESOLVED,
author=user,
reason="Acknowledge button",
action_source=action_source,
)

self.acknowledge(acknowledged_by_user=user, acknowledged_by=AlertGroup.USER)
# Update alert group state and response time metrics cache
Expand All @@ -576,7 +582,9 @@ def acknowledge_by_user(self, user: User, action_source: typing.Optional[str] =
self.stop_escalation()
self.start_ack_reminder_if_needed()

log_record = self.log_records.create(type=AlertGroupLogRecord.TYPE_ACK, author=user)
log_record = self.log_records.create(
type=AlertGroupLogRecord.TYPE_ACK, author=user, action_source=action_source
)

logger.debug(
f"send alert_group_action_triggered_signal for alert_group {self.pk}, "
Expand Down Expand Up @@ -630,7 +638,7 @@ def acknowledge_by_source(self):
for dependent_alert_group in self.dependent_alert_groups.all():
dependent_alert_group.acknowledge_by_source()

def un_acknowledge_by_user(self, user: User, action_source: typing.Optional[str] = None) -> None:
def un_acknowledge_by_user(self, user: User, action_source: typing.Optional[ActionSource] = None) -> None:
from apps.alerts.models import AlertGroupLogRecord

initial_state = self.state
Expand All @@ -642,7 +650,9 @@ def un_acknowledge_by_user(self, user: User, action_source: typing.Optional[str]
if self.is_root_alert_group:
self.start_escalation_if_needed()

log_record = self.log_records.create(type=AlertGroupLogRecord.TYPE_UN_ACK, author=user)
log_record = self.log_records.create(
type=AlertGroupLogRecord.TYPE_UN_ACK, author=user, action_source=action_source
)

logger.debug(
f"send alert_group_action_triggered_signal for alert_group {self.pk}, "
Expand All @@ -659,7 +669,7 @@ def un_acknowledge_by_user(self, user: User, action_source: typing.Optional[str]
dependent_alert_group.un_acknowledge_by_user(user, action_source=action_source)
logger.debug(f"Finished un_acknowledge_by_user for alert_group {self.pk}")

def resolve_by_user(self, user: User, action_source: typing.Optional[str] = None) -> None:
def resolve_by_user(self, user: User, action_source: typing.Optional[ActionSource] = None) -> None:
from apps.alerts.models import AlertGroupLogRecord

initial_state = self.state
Expand All @@ -672,12 +682,15 @@ def resolve_by_user(self, user: User, action_source: typing.Optional[str] = None
author=user,
silence_delay=None,
reason="Resolve button",
action_source=action_source,
)
self.resolve(resolved_by=AlertGroup.USER, resolved_by_user=user)
# Update alert group state and response time metrics cache
self._update_metrics(organization_id=user.organization_id, previous_state=initial_state, state=self.state)
self.stop_escalation()
log_record = self.log_records.create(type=AlertGroupLogRecord.TYPE_RESOLVED, author=user)
log_record = self.log_records.create(
type=AlertGroupLogRecord.TYPE_RESOLVED, author=user, action_source=action_source
)

logger.debug(
f"send alert_group_action_triggered_signal for alert_group {self.pk}, "
Expand Down Expand Up @@ -777,7 +790,7 @@ def resolve_by_disable_maintenance(self):
for dependent_alert_group in self.dependent_alert_groups.all():
dependent_alert_group.resolve_by_disable_maintenance()

def un_resolve_by_user(self, user: User, action_source: typing.Optional[str] = None) -> None:
def un_resolve_by_user(self, user: User, action_source: typing.Optional[ActionSource] = None) -> None:
from apps.alerts.models import AlertGroupLogRecord

if self.wiped_at is None:
Expand All @@ -786,7 +799,9 @@ def un_resolve_by_user(self, user: User, action_source: typing.Optional[str] = N
# Update alert group state metric cache
self._update_metrics(organization_id=user.organization_id, previous_state=initial_state, state=self.state)

log_record = self.log_records.create(type=AlertGroupLogRecord.TYPE_UN_RESOLVED, author=user)
log_record = self.log_records.create(
type=AlertGroupLogRecord.TYPE_UN_RESOLVED, author=user, action_source=action_source
)

if self.is_root_alert_group:
self.start_escalation_if_needed()
Expand All @@ -807,7 +822,7 @@ def un_resolve_by_user(self, user: User, action_source: typing.Optional[str] = N
dependent_alert_group.un_resolve_by_user(user, action_source=action_source)

def attach_by_user(
self, user: User, root_alert_group: "AlertGroup", action_source: typing.Optional[str] = None
self, user: User, root_alert_group: "AlertGroup", action_source: typing.Optional[ActionSource] = None
) -> None:
from apps.alerts.models import AlertGroupLogRecord

Expand All @@ -831,6 +846,7 @@ def attach_by_user(
author=user,
root_alert_group=root_alert_group,
reason="Attach dropdown",
action_source=action_source,
)

logger.debug(
Expand All @@ -850,6 +866,7 @@ def attach_by_user(
author=user,
dependent_alert_group=self,
reason="Attach dropdown",
action_source=action_source,
)

logger.debug(
Expand All @@ -870,6 +887,7 @@ def attach_by_user(
author=user,
root_alert_group=root_alert_group,
reason="Failed to attach dropdown",
action_source=action_source,
)

logger.debug(
Expand All @@ -884,7 +902,7 @@ def attach_by_user(
action_source=action_source,
)

def un_attach_by_user(self, user: User, action_source: typing.Optional[str] = None) -> None:
def un_attach_by_user(self, user: User, action_source: typing.Optional[ActionSource] = None) -> None:
from apps.alerts.models import AlertGroupLogRecord

root_alert_group: AlertGroup = self.root_alert_group
Expand All @@ -898,6 +916,7 @@ def un_attach_by_user(self, user: User, action_source: typing.Optional[str] = No
author=user,
root_alert_group=root_alert_group,
reason="Unattach button",
action_source=action_source,
)

logger.debug(
Expand All @@ -917,6 +936,7 @@ def un_attach_by_user(self, user: User, action_source: typing.Optional[str] = No
author=user,
dependent_alert_group=self,
reason="Unattach dropdown",
action_source=action_source,
)

logger.debug(
Expand Down Expand Up @@ -957,19 +977,26 @@ def un_attach_by_delete(self):
)

def silence_by_user(
self, user: User, silence_delay: typing.Optional[int], action_source: typing.Optional[str] = None
self, user: User, silence_delay: typing.Optional[int], action_source: typing.Optional[ActionSource] = None
) -> None:
from apps.alerts.models import AlertGroupLogRecord

initial_state = self.state

if self.resolved:
self.unresolve()
self.log_records.create(type=AlertGroupLogRecord.TYPE_UN_RESOLVED, author=user, reason="Silence button")
self.log_records.create(
type=AlertGroupLogRecord.TYPE_UN_RESOLVED,
author=user,
reason="Silence button",
action_source=action_source,
)

if self.acknowledged:
self.unacknowledge()
self.log_records.create(type=AlertGroupLogRecord.TYPE_UN_ACK, author=user, reason="Silence button")
self.log_records.create(
type=AlertGroupLogRecord.TYPE_UN_ACK, author=user, reason="Silence button", action_source=action_source
)

if self.silenced:
self.un_silence()
Expand All @@ -978,6 +1005,7 @@ def silence_by_user(
author=user,
silence_delay=None,
reason="Silence button",
action_source=action_source,
)

now = timezone.now()
Expand Down Expand Up @@ -1006,6 +1034,7 @@ def silence_by_user(
author=user,
silence_delay=silence_delay_timedelta,
reason="Silence button",
action_source=action_source,
)

logger.debug(
Expand All @@ -1022,7 +1051,7 @@ def silence_by_user(
for dependent_alert_group in self.dependent_alert_groups.all():
dependent_alert_group.silence_by_user(user, silence_delay, action_source)

def un_silence_by_user(self, user: User, action_source: typing.Optional[str] = None) -> None:
def un_silence_by_user(self, user: User, action_source: typing.Optional[ActionSource] = None) -> None:
from apps.alerts.models import AlertGroupLogRecord

initial_state = self.state
Expand All @@ -1040,6 +1069,7 @@ def un_silence_by_user(self, user: User, action_source: typing.Optional[str] = N
silence_delay=None,
# 2.Look like some time ago there was no TYPE_UN_SILENCE
reason="Unsilence button",
action_source=action_source,
)

logger.debug(
Expand Down
14 changes: 11 additions & 3 deletions engine/apps/alerts/models/alert_group_log_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from rest_framework.fields import DateTimeField

from apps.alerts import tasks
from apps.alerts.constants import ActionSource
from apps.alerts.utils import render_relative_timeline
from apps.slack.slack_formatter import SlackFormatter
from common.utils import clean_markup
Expand Down Expand Up @@ -155,6 +156,9 @@ class AlertGroupLogRecord(models.Model):

type = models.IntegerField(choices=TYPE_CHOICES)

# Where the action was performed (e.g. web UI, Slack, API, etc.)
action_source = models.SmallIntegerField(ActionSource.choices, null=True, default=None)

author = models.ForeignKey(
"user_management.User",
on_delete=models.SET_NULL,
Expand Down Expand Up @@ -248,7 +252,6 @@ def rendered_log_line_action(self, for_slack=False, html=False, substitute_autho
from apps.alerts.models import EscalationPolicy

result = ""
author_name = None
invitee_name = None
escalation_policy_step = None
step_specific_info = self.get_step_specific_info()
Expand All @@ -258,13 +261,18 @@ def rendered_log_line_action(self, for_slack=False, html=False, substitute_autho
elif self.escalation_policy is not None:
escalation_policy_step = self.escalation_policy.step

if self.author is not None:
if self.action_source == ActionSource.API:
author_name = "API"
elif self.author:
if substitute_author_with_tag:
author_name = "{{author}}"
elif for_slack:
author_name = self.author.get_username_with_slack_verbal()
else:
author_name = self.author.username
else:
author_name = None

if self.invitation is not None:
if for_slack:
invitee_name = self.invitation.invitee.get_username_with_slack_verbal()
Expand Down Expand Up @@ -479,7 +487,7 @@ def rendered_log_line_action(self, for_slack=False, html=False, substitute_autho
f"because it is already attached or resolved."
)
elif self.type == AlertGroupLogRecord.TYPE_RESOLVED:
result += f"alert group resolved {f'by {author_name}'if author_name else ''}"
result += f"resolved {f'by {author_name}'if author_name else ''}"
elif self.type == AlertGroupLogRecord.TYPE_UN_RESOLVED:
result += f"unresolved by {author_name}"
elif self.type == AlertGroupLogRecord.TYPE_WIPED:
Expand Down
Loading

0 comments on commit a727450

Please sign in to comment.