Merge branch 'dev' of github.com:grafana/oncall into dev
This commit is contained in:
commit
dedc73dc19
21 changed files with 473 additions and 49 deletions
|
|
@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### 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
|
||||
|
||||
- Add additional shift info in schedule filter_events internal API ([#3110](https://github.com/grafana/oncall/pull/3110))
|
||||
|
||||
## v1.3.41 (2023-10-04)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
canonical: https://grafana.com/docs/oncall/latest/integration-with-alert-sources/
|
||||
canonical: https://grafana.com/docs/oncall/latest/integrations/
|
||||
keywords:
|
||||
- Grafana Cloud
|
||||
- Alerts
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
aliases:
|
||||
- add-alertmanager/
|
||||
canonical: https://grafana.com/docs/oncall/latest/integrations/available-integrations/configure-alertmanager/
|
||||
canonical: https://grafana.com/docs/oncall/latest/integrations/alertmanager/
|
||||
keywords:
|
||||
- Grafana Cloud
|
||||
- Alerts
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
aliases:
|
||||
- add-grafana-alerting/
|
||||
canonical: https://grafana.com/docs/oncall/latest/integrations/available-integrations/configure-grafana-alerting/
|
||||
canonical: https://grafana.com/docs/oncall/latest/integrations/grafana-alerting/
|
||||
keywords:
|
||||
- Grafana Cloud
|
||||
- Alerts
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
aliases:
|
||||
- inbound-email/
|
||||
canonical: https://grafana.com/docs/oncall/latest/integrations/available-integrations/configure-inbound-email/
|
||||
canonical: https://grafana.com/docs/oncall/latest/integrations/inbound-email/
|
||||
keywords:
|
||||
- Grafana Cloud
|
||||
- Alerts
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
aliases:
|
||||
- add-sentry/
|
||||
- /docs/oncall/latest/integrations/available-integrations/configure-Sentry/
|
||||
canonical: https://grafana.com/docs/oncall/latest/integrations/available-integrations/configure-sentry/
|
||||
canonical: https://grafana.com/docs/oncall/latest/integrations/sentry/
|
||||
keywords:
|
||||
- Grafana Cloud
|
||||
- Alerts
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
aliases:
|
||||
- ../add-webhook-integration/
|
||||
canonical: https://grafana.com/docs/oncall/latest/integrations/available-integrations/configure-webhook/
|
||||
canonical: https://grafana.com/docs/oncall/latest/integrations/webhook/
|
||||
keywords:
|
||||
- Grafana Cloud
|
||||
- Alerts
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
aliases:
|
||||
- add-zabbix/
|
||||
canonical: https://grafana.com/docs/oncall/latest/integrations/available-integrations/configure-zabbix/
|
||||
canonical: https://grafana.com/docs/oncall/latest/integrations/zabbix/
|
||||
keywords:
|
||||
- Grafana Cloud
|
||||
- Alerts
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
aliases:
|
||||
- ../../chat-options/configure-teams/
|
||||
canonical: https://grafana.com/docs/oncall/latest/integrations/chatops-integrations/configure-teams/
|
||||
canonical: https://grafana.com/docs/oncall/latest/notify/ms-teams/
|
||||
keywords:
|
||||
- Grafana Cloud
|
||||
- Alerts
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
aliases:
|
||||
- ../../chat-options/configure-slack/
|
||||
canonical: https://grafana.com/docs/oncall/latest/integrations/chatops-integrations/configure-slack/
|
||||
canonical: https://grafana.com/docs/oncall/latest/notify/slack/
|
||||
keywords:
|
||||
- Grafana Cloud
|
||||
- Alerts
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
aliases:
|
||||
- ../../chat-options/configure-telegram/
|
||||
canonical: https://grafana.com/docs/oncall/latest/integrations/chatops-integrations/configure-telegram/
|
||||
canonical: https://grafana.com/docs/oncall/latest/notify/telegram/
|
||||
keywords:
|
||||
- Grafana Cloud
|
||||
- Alerts
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ weight: 700
|
|||
> endpoint remains available and is compatible with previous callers but under the hood it will interact with the
|
||||
> new webhooks objects. It is recommended to use the /webhooks endpoint going forward which has more features.
|
||||
|
||||
For more details about specific fields of a webhook see [outgoing webhooks][outgoing-webhooks] documentation.
|
||||
For more details about specific fields of a webhook see [outgoing webhooks](../../outgoing-webhooks) documentation.
|
||||
|
||||
## List webhooks
|
||||
|
||||
|
|
@ -105,7 +105,7 @@ curl "{{API_URL}}/api/v1/webhooks/" \
|
|||
|
||||
### Trigger Types
|
||||
|
||||
See [here](outgoing-webhooks#event-types) for details
|
||||
For more detail, refer to [Event types](../../outgoing-webhooks#event-types).
|
||||
|
||||
- `escalation`
|
||||
- `alert group created`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')]),
|
||||
),
|
||||
]
|
||||
|
|
@ -16,7 +16,7 @@ from django.dispatch import receiver
|
|||
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
|
||||
|
|
@ -550,7 +550,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
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
|
||||
|
|
@ -564,10 +564,16 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
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
|
||||
|
|
@ -576,7 +582,9 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
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}, "
|
||||
|
|
@ -630,7 +638,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
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
|
||||
|
|
@ -642,7 +650,9 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
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}, "
|
||||
|
|
@ -659,7 +669,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
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
|
||||
|
|
@ -672,12 +682,15 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
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}, "
|
||||
|
|
@ -777,7 +790,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
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:
|
||||
|
|
@ -786,7 +799,9 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
# 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()
|
||||
|
|
@ -807,7 +822,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
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
|
||||
|
||||
|
|
@ -831,6 +846,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
author=user,
|
||||
root_alert_group=root_alert_group,
|
||||
reason="Attach dropdown",
|
||||
action_source=action_source,
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
|
|
@ -850,6 +866,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
author=user,
|
||||
dependent_alert_group=self,
|
||||
reason="Attach dropdown",
|
||||
action_source=action_source,
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
|
|
@ -870,6 +887,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
author=user,
|
||||
root_alert_group=root_alert_group,
|
||||
reason="Failed to attach dropdown",
|
||||
action_source=action_source,
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
|
|
@ -884,7 +902,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
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
|
||||
|
|
@ -898,6 +916,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
author=user,
|
||||
root_alert_group=root_alert_group,
|
||||
reason="Unattach button",
|
||||
action_source=action_source,
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
|
|
@ -917,6 +936,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
author=user,
|
||||
dependent_alert_group=self,
|
||||
reason="Unattach dropdown",
|
||||
action_source=action_source,
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
|
|
@ -957,7 +977,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
)
|
||||
|
||||
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
|
||||
|
||||
|
|
@ -965,11 +985,18 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
|
||||
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()
|
||||
|
|
@ -978,6 +1005,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
author=user,
|
||||
silence_delay=None,
|
||||
reason="Silence button",
|
||||
action_source=action_source,
|
||||
)
|
||||
|
||||
now = timezone.now()
|
||||
|
|
@ -1006,6 +1034,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
author=user,
|
||||
silence_delay=silence_delay_timedelta,
|
||||
reason="Silence button",
|
||||
action_source=action_source,
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
|
|
@ -1022,7 +1051,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
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
|
||||
|
|
@ -1040,6 +1069,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
|||
silence_delay=None,
|
||||
# 2.Look like some time ago there was no TYPE_UN_SILENCE
|
||||
reason="Unsilence button",
|
||||
action_source=action_source,
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from django.dispatch import receiver
|
|||
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
|
||||
|
|
@ -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,
|
||||
|
|
@ -248,7 +252,6 @@ class AlertGroupLogRecord(models.Model):
|
|||
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()
|
||||
|
|
@ -258,13 +261,18 @@ class AlertGroupLogRecord(models.Model):
|
|||
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()
|
||||
|
|
@ -479,7 +487,7 @@ class AlertGroupLogRecord(models.Model):
|
|||
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:
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from unittest.mock import call, patch
|
|||
|
||||
import pytest
|
||||
|
||||
from apps.alerts.constants import ActionSource
|
||||
from apps.alerts.incident_appearance.renderers.phone_call_renderer import AlertGroupPhoneCallRenderer
|
||||
from apps.alerts.models import AlertGroup, AlertGroupLogRecord
|
||||
from apps.alerts.tasks.delete_alert_group import delete_alert_group
|
||||
|
|
@ -403,3 +404,59 @@ def test_bulk_silence_forever(
|
|||
assert alert_group.silenced
|
||||
assert alert_group.raw_escalation_snapshot["next_step_eta"] == raw_next_step_eta
|
||||
assert not mocked_start_unsilence_task.called
|
||||
|
||||
|
||||
@pytest.mark.parametrize("action_source", ActionSource)
|
||||
@pytest.mark.django_db
|
||||
def test_alert_group_log_record_action_source(
|
||||
make_organization_and_user,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
action_source,
|
||||
):
|
||||
"""Test that action source is saved in alert group log record"""
|
||||
organization, user = make_organization_and_user()
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
|
||||
alert_group = make_alert_group(alert_receive_channel)
|
||||
root_alert_group = make_alert_group(alert_receive_channel)
|
||||
|
||||
# Silence alert group
|
||||
alert_group.silence_by_user(user, 42, action_source=action_source)
|
||||
log_record = alert_group.log_records.last()
|
||||
assert (log_record.type, log_record.action_source) == (AlertGroupLogRecord.TYPE_SILENCE, action_source)
|
||||
|
||||
# Unsilence alert group
|
||||
alert_group.un_silence_by_user(user, action_source=action_source)
|
||||
log_record = alert_group.log_records.last()
|
||||
assert (log_record.type, log_record.action_source) == (AlertGroupLogRecord.TYPE_UN_SILENCE, action_source)
|
||||
|
||||
# Acknowledge alert group
|
||||
alert_group.acknowledge_by_user(user, action_source=action_source)
|
||||
log_record = alert_group.log_records.last()
|
||||
assert (log_record.type, log_record.action_source) == (AlertGroupLogRecord.TYPE_ACK, action_source)
|
||||
|
||||
# Unacknowledge alert group
|
||||
alert_group.un_acknowledge_by_user(user, action_source=action_source)
|
||||
log_record = alert_group.log_records.last()
|
||||
assert (log_record.type, log_record.action_source) == (AlertGroupLogRecord.TYPE_UN_ACK, action_source)
|
||||
|
||||
# Resolve alert group
|
||||
alert_group.resolve_by_user(user, action_source=action_source)
|
||||
log_record = alert_group.log_records.last()
|
||||
assert (log_record.type, log_record.action_source) == (AlertGroupLogRecord.TYPE_RESOLVED, action_source)
|
||||
|
||||
# Unresolve alert group
|
||||
alert_group.un_resolve_by_user(user, action_source=action_source)
|
||||
log_record = alert_group.log_records.last()
|
||||
assert (log_record.type, log_record.action_source) == (AlertGroupLogRecord.TYPE_UN_RESOLVED, action_source)
|
||||
|
||||
# Attach alert group
|
||||
alert_group.attach_by_user(user, root_alert_group, action_source=action_source)
|
||||
log_record = alert_group.log_records.last()
|
||||
assert (log_record.type, log_record.action_source) == (AlertGroupLogRecord.TYPE_ATTACHED, action_source)
|
||||
|
||||
# Unattach alert group
|
||||
alert_group.un_attach_by_user(user, action_source=action_source)
|
||||
log_record = alert_group.log_records.last()
|
||||
assert (log_record.type, log_record.action_source) == (AlertGroupLogRecord.TYPE_UNATTACHED, action_source)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from rest_framework import status
|
|||
from rest_framework.response import Response
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from apps.alerts.constants import ActionSource
|
||||
from apps.alerts.models import AlertGroup, AlertGroupLogRecord
|
||||
from apps.api.errors import AlertGroupAPIError
|
||||
from apps.api.permissions import LegacyAccessControlRole
|
||||
|
|
@ -1862,3 +1863,31 @@ def test_alert_group_resolve_resolution_note(
|
|||
|
||||
assert new_alert_group.has_resolution_notes
|
||||
assert mock_signal.called
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_timeline_api_action(
|
||||
make_organization_and_user_with_plugin_token,
|
||||
make_alert_receive_channel,
|
||||
make_channel_filter,
|
||||
make_alert_group,
|
||||
make_alert,
|
||||
make_user_auth_headers,
|
||||
):
|
||||
"""Check that the timeline API returns the correct actions when using AlertSource.WEB vs ActionSource.API"""
|
||||
organization, user, token = make_organization_and_user_with_plugin_token()
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
channel_filter = make_channel_filter(alert_receive_channel, is_default=True)
|
||||
alert_group = make_alert_group(alert_receive_channel, channel_filter=channel_filter)
|
||||
make_alert(alert_group=alert_group, raw_request_data=alert_raw_request_data)
|
||||
|
||||
alert_group.acknowledge_by_user(user, action_source=ActionSource.WEB)
|
||||
alert_group.resolve_by_user(user, action_source=ActionSource.API)
|
||||
|
||||
client = APIClient()
|
||||
url = reverse("api-internal:alertgroup-detail", kwargs={"pk": alert_group.public_primary_key})
|
||||
response = client.get(url, **make_user_auth_headers(user, token))
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["render_after_resolve_report_json"][0]["action"] == "acknowledged by {{author}}"
|
||||
assert response.json()["render_after_resolve_report_json"][1]["action"] == "resolved by API"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from django.urls import reverse
|
|||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from apps.alerts.constants import ActionSource
|
||||
from apps.alerts.models import AlertGroup, AlertReceiveChannel
|
||||
|
||||
|
||||
|
|
@ -291,14 +292,167 @@ def test_pagination(settings, incident_public_api_setup):
|
|||
assert result["next"].startswith("https://test.com/test/prefixed/urls")
|
||||
|
||||
|
||||
# This is test from old django-based tests
|
||||
# TODO: uncomment with date checking in delete mode
|
||||
# def test_delete_incident_invalid_date(self):
|
||||
# not_valid_creation_date = VALID_DATE_FOR_DELETE_INCIDENT - timezone.timedelta(days=1)
|
||||
# self.grafana_second_alert_group.started_at = not_valid_creation_date
|
||||
# self.grafana_second_alert_group.save()
|
||||
#
|
||||
# url = reverse("api-public:alert_groups-detail", kwargs={'pk': self.grafana_second_alert_group.public_primary_key})
|
||||
# data = {"mode": "delete"}
|
||||
# response = self.client.delete(url, data=data, format="json", HTTP_AUTHORIZATION=f"{self.token}")
|
||||
# self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
@pytest.mark.parametrize(
|
||||
"acknowledged,resolved,attached,maintenance,status_code",
|
||||
[
|
||||
(False, False, False, False, status.HTTP_200_OK),
|
||||
(True, False, False, False, status.HTTP_400_BAD_REQUEST),
|
||||
(False, True, False, False, status.HTTP_400_BAD_REQUEST),
|
||||
(False, False, True, False, status.HTTP_400_BAD_REQUEST),
|
||||
(False, False, False, True, status.HTTP_400_BAD_REQUEST),
|
||||
],
|
||||
)
|
||||
@pytest.mark.django_db
|
||||
def test_alert_group_acknowledge(
|
||||
make_organization_and_user_with_token,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
acknowledged,
|
||||
resolved,
|
||||
attached,
|
||||
maintenance,
|
||||
status_code,
|
||||
):
|
||||
organization, _, token = make_organization_and_user_with_token()
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
root_alert_group = make_alert_group(alert_receive_channel)
|
||||
alert_group = make_alert_group(
|
||||
alert_receive_channel,
|
||||
acknowledged=acknowledged,
|
||||
resolved=resolved,
|
||||
root_alert_group=root_alert_group if attached else None,
|
||||
maintenance_uuid="test_maintenance_uuid" if maintenance else None,
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
url = reverse("api-public:alert_groups-acknowledge", kwargs={"pk": alert_group.public_primary_key})
|
||||
response = client.post(url, HTTP_AUTHORIZATION=token)
|
||||
assert response.status_code == status_code
|
||||
|
||||
if status_code == status.HTTP_200_OK:
|
||||
alert_group.refresh_from_db()
|
||||
assert alert_group.acknowledged is True
|
||||
assert alert_group.log_records.last().action_source == ActionSource.API
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"acknowledged,resolved,attached,maintenance,status_code",
|
||||
[
|
||||
(True, False, False, False, status.HTTP_200_OK),
|
||||
(True, True, False, False, status.HTTP_400_BAD_REQUEST),
|
||||
(True, False, True, False, status.HTTP_400_BAD_REQUEST),
|
||||
(True, False, False, True, status.HTTP_400_BAD_REQUEST),
|
||||
(False, False, False, False, status.HTTP_400_BAD_REQUEST),
|
||||
],
|
||||
)
|
||||
@pytest.mark.django_db
|
||||
def test_alert_group_unacknowledge(
|
||||
make_organization_and_user_with_token,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
acknowledged,
|
||||
resolved,
|
||||
attached,
|
||||
maintenance,
|
||||
status_code,
|
||||
):
|
||||
organization, _, token = make_organization_and_user_with_token()
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
root_alert_group = make_alert_group(alert_receive_channel)
|
||||
alert_group = make_alert_group(
|
||||
alert_receive_channel,
|
||||
acknowledged=acknowledged,
|
||||
resolved=resolved,
|
||||
root_alert_group=root_alert_group if attached else None,
|
||||
maintenance_uuid="test_maintenance_uuid" if maintenance else None,
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
url = reverse("api-public:alert_groups-unacknowledge", kwargs={"pk": alert_group.public_primary_key})
|
||||
response = client.post(url, HTTP_AUTHORIZATION=token)
|
||||
assert response.status_code == status_code
|
||||
|
||||
if status_code == status.HTTP_200_OK:
|
||||
alert_group.refresh_from_db()
|
||||
assert alert_group.acknowledged is False
|
||||
assert alert_group.log_records.last().action_source == ActionSource.API
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resolved,attached,maintenance,status_code",
|
||||
[
|
||||
(False, False, False, status.HTTP_200_OK),
|
||||
(False, False, True, status.HTTP_200_OK),
|
||||
(True, False, False, status.HTTP_400_BAD_REQUEST),
|
||||
(False, True, False, status.HTTP_400_BAD_REQUEST),
|
||||
],
|
||||
)
|
||||
@pytest.mark.django_db
|
||||
def test_alert_group_resolve(
|
||||
make_organization_and_user_with_token,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
resolved,
|
||||
attached,
|
||||
maintenance,
|
||||
status_code,
|
||||
):
|
||||
organization, _, token = make_organization_and_user_with_token()
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
root_alert_group = make_alert_group(alert_receive_channel)
|
||||
alert_group = make_alert_group(
|
||||
alert_receive_channel,
|
||||
resolved=resolved,
|
||||
root_alert_group=root_alert_group if attached else None,
|
||||
maintenance_uuid="test_maintenance_uuid" if maintenance else None,
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
url = reverse("api-public:alert_groups-resolve", kwargs={"pk": alert_group.public_primary_key})
|
||||
response = client.post(url, HTTP_AUTHORIZATION=token)
|
||||
assert response.status_code == status_code
|
||||
|
||||
if status_code == status.HTTP_200_OK and not maintenance:
|
||||
alert_group.refresh_from_db()
|
||||
assert alert_group.resolved is True
|
||||
assert alert_group.log_records.last().action_source == ActionSource.API
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resolved,attached,maintenance,status_code",
|
||||
[
|
||||
(True, False, False, status.HTTP_200_OK),
|
||||
(True, True, False, status.HTTP_400_BAD_REQUEST),
|
||||
(True, False, True, status.HTTP_400_BAD_REQUEST),
|
||||
(False, False, False, status.HTTP_400_BAD_REQUEST),
|
||||
],
|
||||
)
|
||||
@pytest.mark.django_db
|
||||
def test_alert_group_unresolve(
|
||||
make_organization_and_user_with_token,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
resolved,
|
||||
attached,
|
||||
maintenance,
|
||||
status_code,
|
||||
):
|
||||
organization, _, token = make_organization_and_user_with_token()
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
root_alert_group = make_alert_group(alert_receive_channel)
|
||||
alert_group = make_alert_group(
|
||||
alert_receive_channel,
|
||||
resolved=resolved,
|
||||
root_alert_group=root_alert_group if attached else None,
|
||||
maintenance_uuid="test_maintenance_uuid" if maintenance else None,
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
url = reverse("api-public:alert_groups-unresolve", kwargs={"pk": alert_group.public_primary_key})
|
||||
response = client.post(url, HTTP_AUTHORIZATION=token)
|
||||
assert response.status_code == status_code
|
||||
|
||||
if status_code == status.HTTP_200_OK:
|
||||
alert_group.refresh_from_db()
|
||||
assert alert_group.resolved is False
|
||||
assert alert_group.log_records.last().action_source == ActionSource.API
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
from django.db.models import Q
|
||||
from django_filters import rest_framework as filters
|
||||
from rest_framework import mixins, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import NotFound
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
from apps.alerts.constants import ActionSource
|
||||
from apps.alerts.models import AlertGroup
|
||||
from apps.alerts.tasks import delete_alert_group, wipe
|
||||
from apps.auth_token.auth import ApiTokenAuthentication
|
||||
|
|
@ -112,3 +114,74 @@ class IncidentView(RateLimitHeadersMixin, mixins.ListModelMixin, mixins.DestroyM
|
|||
wipe.apply_async((instance.pk, request.user.pk))
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@action(methods=["post"], detail=True)
|
||||
def acknowledge(self, request, pk):
|
||||
alert_group = self.get_object()
|
||||
|
||||
if alert_group.acknowledged:
|
||||
raise BadRequest(detail="Can't acknowledge an acknowledged alert group")
|
||||
|
||||
if alert_group.resolved:
|
||||
raise BadRequest(detail="Can't acknowledge a resolved alert group")
|
||||
|
||||
if alert_group.root_alert_group:
|
||||
raise BadRequest(detail="Can't acknowledge an attached alert group")
|
||||
|
||||
if alert_group.is_maintenance_incident:
|
||||
raise BadRequest(detail="Can't acknowledge a maintenance alert group")
|
||||
|
||||
alert_group.acknowledge_by_user(self.request.user, action_source=ActionSource.API)
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
@action(methods=["post"], detail=True)
|
||||
def unacknowledge(self, request, pk):
|
||||
alert_group = self.get_object()
|
||||
|
||||
if not alert_group.acknowledged:
|
||||
raise BadRequest(detail="Can't unacknowledge an unacknowledged alert group")
|
||||
|
||||
if alert_group.resolved:
|
||||
raise BadRequest(detail="Can't unacknowledge a resolved alert group")
|
||||
|
||||
if alert_group.root_alert_group:
|
||||
raise BadRequest(detail="Can't unacknowledge an attached alert group")
|
||||
|
||||
if alert_group.is_maintenance_incident:
|
||||
raise BadRequest(detail="Can't unacknowledge a maintenance alert group")
|
||||
|
||||
alert_group.un_acknowledge_by_user(self.request.user, action_source=ActionSource.API)
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
@action(methods=["post"], detail=True)
|
||||
def resolve(self, request, pk):
|
||||
alert_group = self.get_object()
|
||||
|
||||
if alert_group.resolved:
|
||||
raise BadRequest(detail="Can't resolve a resolved alert group")
|
||||
|
||||
if alert_group.root_alert_group:
|
||||
raise BadRequest(detail="Can't resolve an attached alert group")
|
||||
|
||||
if alert_group.is_maintenance_incident:
|
||||
alert_group.stop_maintenance(self.request.user)
|
||||
else:
|
||||
alert_group.resolve_by_user(self.request.user, action_source=ActionSource.API)
|
||||
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
@action(methods=["post"], detail=True)
|
||||
def unresolve(self, request, pk):
|
||||
alert_group = self.get_object()
|
||||
|
||||
if not alert_group.resolved:
|
||||
raise BadRequest(detail="Can't unresolve an unresolved alert group")
|
||||
|
||||
if alert_group.root_alert_group:
|
||||
raise BadRequest(detail="Can't unresolve an attached alert group")
|
||||
|
||||
if alert_group.is_maintenance_incident:
|
||||
raise BadRequest(detail="Can't unresolve a maintenance alert group")
|
||||
|
||||
alert_group.un_resolve_by_user(self.request.user, action_source=ActionSource.API)
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue