oncall-engine/engine/apps/slack/representatives/alert_group_representative.py

290 lines
12 KiB
Python
Raw Permalink Normal View History

import logging
import typing
from celery.utils.log import get_task_logger
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from apps.alerts.constants import ActionSource
from apps.alerts.representative import AlertGroupAbstractRepresentative
from apps.slack.scenarios.scenario_step import ScenarioStep
from common.custom_celery_tasks import shared_dedicated_queue_retry_task
if typing.TYPE_CHECKING:
from apps.alerts.models import AlertGroupLogRecord
logger = get_task_logger(__name__)
logger.setLevel(logging.DEBUG)
@shared_dedicated_queue_retry_task(
autoretry_for=(Exception,), retry_backoff=True, max_retries=1 if settings.DEBUG else None
)
def on_create_alert_slack_representative_async(alert_pk):
"""
It's asynced in order to prevent Slack downtime causing issues with SMS and other destinations.
"""
`apps.get_model` -> `import` (#2619) # What this PR does Remove [`apps.get_model`](https://docs.djangoproject.com/en/3.2/ref/applications/#django.apps.apps.get_model) invocations and use inline `import` statements in places where models are imported within functions/methods to avoid circular imports. I believe `import` statements are more appropriate for most use cases as they allow for better static code analysis & formatting, and solve the issue of circular imports without being unnecessarily dynamic as `apps.get_model`. With `import` statements, it's possible to: - Jump to model definitions in most IDEs - Automatically sort inline imports with `isort` - Find import errors faster/easier (most IDEs highlight broken imports) - Have more consistency across regular & inline imports when importing models This PR also adds a flake8 rule to ban imports of `django.apps.apps`, so it's harder to use `apps.get_model` by mistake (it's possible to ignore this rule by using `# noqa: I251`). The rule is not enforced on directories with migration files, because `apps.get_model` is often used to get a historical state of a model, which is useful when writing migrations ([see this SO answer for more details](https://stackoverflow.com/a/37769213)). So `apps.get_model` is considered OK in migrations (even necessary in some cases). ## 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)
2023-07-25 10:43:23 +01:00
from apps.alerts.models import Alert
alert = (
Alert.objects.filter(pk=alert_pk)
.select_related(
"group",
"group__channel",
"group__channel__organization",
"group__channel__organization__slack_team_identity",
)
.get()
)
logger.debug(f"Start on_create_alert_slack_representative for alert {alert_pk} from alert_group {alert.group_id}")
organization = alert.group.channel.organization
if organization.slack_team_identity:
logger.debug(
f"Process on_create_alert_slack_representative for alert {alert_pk} from alert_group {alert.group_id}"
)
IncomingAlertStep = ScenarioStep.get_step("distribute_alerts", "IncomingAlertStep")
step = IncomingAlertStep(organization.slack_team_identity, organization)
step.process_signal(alert)
else:
logger.debug(
f"Drop on_create_alert_slack_representative for alert {alert_pk} from alert_group {alert.group_id}"
)
logger.debug(f"Finish on_create_alert_slack_representative for alert {alert_pk} from alert_group {alert.group_id}")
@shared_dedicated_queue_retry_task(
autoretry_for=(Exception,),
retry_backoff=True,
dont_autoretry_for=(ObjectDoesNotExist,),
max_retries=1 if settings.DEBUG else None,
)
def on_alert_group_action_triggered_async(log_record_id):
`apps.get_model` -> `import` (#2619) # What this PR does Remove [`apps.get_model`](https://docs.djangoproject.com/en/3.2/ref/applications/#django.apps.apps.get_model) invocations and use inline `import` statements in places where models are imported within functions/methods to avoid circular imports. I believe `import` statements are more appropriate for most use cases as they allow for better static code analysis & formatting, and solve the issue of circular imports without being unnecessarily dynamic as `apps.get_model`. With `import` statements, it's possible to: - Jump to model definitions in most IDEs - Automatically sort inline imports with `isort` - Find import errors faster/easier (most IDEs highlight broken imports) - Have more consistency across regular & inline imports when importing models This PR also adds a flake8 rule to ban imports of `django.apps.apps`, so it's harder to use `apps.get_model` by mistake (it's possible to ignore this rule by using `# noqa: I251`). The rule is not enforced on directories with migration files, because `apps.get_model` is often used to get a historical state of a model, which is useful when writing migrations ([see this SO answer for more details](https://stackoverflow.com/a/37769213)). So `apps.get_model` is considered OK in migrations (even necessary in some cases). ## 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)
2023-07-25 10:43:23 +01:00
from apps.alerts.models import AlertGroupLogRecord
try:
log_record = AlertGroupLogRecord.objects.get(pk=log_record_id)
except AlertGroupLogRecord.DoesNotExist as e:
logger.warning(f"SLACK representative: log record {log_record_id} never created or has been deleted")
raise e
alert_group_id = log_record.alert_group_id
logger.debug(f"Start on_alert_group_action_triggered for alert_group {alert_group_id}, log record {log_record_id}")
instance = AlertGroupSlackRepresentative(log_record)
if instance.is_applicable():
logger.debug(f"SLACK representative is applicable for alert_group {alert_group_id}, log record {log_record_id}")
handler = instance.get_handler()
logger.debug(
f"Found handler {handler.__name__} in SLACK representative for alert_group {alert_group_id}, "
f"log record {log_record_id}"
)
handler()
logger.debug(
f"Finish handler {handler.__name__} in SLACK representative for alert_group {alert_group_id}, "
f"log record {log_record_id}"
)
else:
logger.debug(
f"SLACK representative is NOT applicable for alert_group {alert_group_id}, log record {log_record_id}"
)
logger.debug(f"Finish on_alert_group_action_triggered for alert_group {alert_group_id}, log record {log_record_id}")
class AlertGroupSlackRepresentative(AlertGroupAbstractRepresentative):
def __init__(self, log_record: "AlertGroupLogRecord"):
self.log_record = log_record
def is_applicable(self):
slack_message = self.log_record.alert_group.slack_message
slack_team_identity = self.log_record.alert_group.channel.organization.slack_team_identity
return (
slack_message is not None
and slack_team_identity is not None
and slack_message.slack_team_identity == slack_team_identity
)
@classmethod
def on_create_alert(cls, **kwargs):
`apps.get_model` -> `import` (#2619) # What this PR does Remove [`apps.get_model`](https://docs.djangoproject.com/en/3.2/ref/applications/#django.apps.apps.get_model) invocations and use inline `import` statements in places where models are imported within functions/methods to avoid circular imports. I believe `import` statements are more appropriate for most use cases as they allow for better static code analysis & formatting, and solve the issue of circular imports without being unnecessarily dynamic as `apps.get_model`. With `import` statements, it's possible to: - Jump to model definitions in most IDEs - Automatically sort inline imports with `isort` - Find import errors faster/easier (most IDEs highlight broken imports) - Have more consistency across regular & inline imports when importing models This PR also adds a flake8 rule to ban imports of `django.apps.apps`, so it's harder to use `apps.get_model` by mistake (it's possible to ignore this rule by using `# noqa: I251`). The rule is not enforced on directories with migration files, because `apps.get_model` is often used to get a historical state of a model, which is useful when writing migrations ([see this SO answer for more details](https://stackoverflow.com/a/37769213)). So `apps.get_model` is considered OK in migrations (even necessary in some cases). ## 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)
2023-07-25 10:43:23 +01:00
from apps.alerts.models import Alert
alert = kwargs["alert"]
if isinstance(alert, Alert):
alert_id = alert.pk
else:
alert_id = alert
alert = Alert.objects.get(pk=alert_id)
logger.debug(
f"Received alert_create_signal in SLACK representative for alert {alert_id} "
f"from alert_group {alert.group_id}"
)
if alert.group.notify_in_slack_enabled is False:
logger.debug(
f"Skipping alert with id {alert_id} from alert_group {alert.group_id} since notify_in_slack is disabled"
)
return
on_create_alert_slack_representative_async.apply_async((alert_id,))
logger.debug(
f"Async process alert_create_signal in SLACK representative for alert {alert_id} "
f"from alert_group {alert.group_id}"
)
@classmethod
def on_alert_group_action_triggered(cls, **kwargs):
logger.debug("Received alert_group_action_triggered signal in SLACK representative")
log_record = cls.get_log_record_from_kwargs(**kwargs)
if not log_record:
return
force_sync = kwargs.get("force_sync", False)
if log_record.action_source == ActionSource.SLACK or force_sync:
logger.debug(f"SLACK on_alert_group_action_triggered: sync {log_record.id} {force_sync}")
on_alert_group_action_triggered_async(log_record.id)
else:
logger.debug(f"SLACK on_alert_group_action_triggered: async {log_record.id} {force_sync}")
on_alert_group_action_triggered_async.apply_async((log_record.id,))
@classmethod
def on_alert_group_update_resolution_note(cls, **kwargs):
alert_group = kwargs["alert_group"]
resolution_note = kwargs.get("resolution_note")
organization = alert_group.channel.organization
logger.debug(
f"Received alert_group_update_resolution_note signal in SLACK representative for alert_group {alert_group.pk}"
)
if alert_group.slack_message and organization.slack_team_identity:
UpdateResolutionNoteStep = ScenarioStep.get_step("resolution_note", "UpdateResolutionNoteStep")
step = UpdateResolutionNoteStep(organization.slack_team_identity, organization)
step.process_signal(alert_group, resolution_note)
@classmethod
def on_alert_group_post_acknowledge_reminder_message(cls, **kwargs):
log_record = cls.get_log_record_from_kwargs(**kwargs)
if not log_record:
return
slack_team_identity = log_record.alert_group.channel.organization.slack_team_identity
alert_group = log_record.alert_group
logger.debug(
f"Received post_ack_reminder_message_signal signal in SLACK representative for alert_group {alert_group.id}"
)
if not (log_record.alert_group.slack_message and slack_team_identity):
logger.debug(
f"SLACK representative is NOT applicable for alert_group {alert_group.id}, log record {log_record.id}"
)
return
AcknowledgeConfirmationStep = ScenarioStep.get_step("distribute_alerts", "AcknowledgeConfirmationStep")
step = AcknowledgeConfirmationStep(slack_team_identity)
step.process_signal(log_record)
def on_acknowledge(self):
AcknowledgeGroupStep = ScenarioStep.get_step("distribute_alerts", "AcknowledgeGroupStep")
step = AcknowledgeGroupStep(self.log_record.alert_group.channel.organization.slack_team_identity)
step.process_signal(self.log_record)
def on_un_acknowledge(self):
UnAcknowledgeGroupStep = ScenarioStep.get_step("distribute_alerts", "UnAcknowledgeGroupStep")
step = UnAcknowledgeGroupStep(self.log_record.alert_group.channel.organization.slack_team_identity)
step.process_signal(self.log_record)
def on_resolve(self):
ResolveGroupStep = ScenarioStep.get_step("distribute_alerts", "ResolveGroupStep")
step = ResolveGroupStep(self.log_record.alert_group.channel.organization.slack_team_identity)
step.process_signal(self.log_record)
def on_un_resolve(self):
UnResolveGroupStep = ScenarioStep.get_step("distribute_alerts", "UnResolveGroupStep")
step = UnResolveGroupStep(self.log_record.alert_group.channel.organization.slack_team_identity)
step.process_signal(self.log_record)
def on_attach(self):
AttachGroupStep = ScenarioStep.get_step("distribute_alerts", "AttachGroupStep")
step = AttachGroupStep(self.log_record.alert_group.channel.organization.slack_team_identity)
step.process_signal(self.log_record)
def on_fail_attach(self):
AttachGroupStep = ScenarioStep.get_step("distribute_alerts", "AttachGroupStep")
step = AttachGroupStep(self.log_record.alert_group.channel.organization.slack_team_identity)
step.process_signal(self.log_record)
def on_un_attach(self):
UnAttachGroupStep = ScenarioStep.get_step("distribute_alerts", "UnAttachGroupStep")
step = UnAttachGroupStep(self.log_record.alert_group.channel.organization.slack_team_identity)
step.process_signal(self.log_record)
def on_silence(self):
SilenceGroupStep = ScenarioStep.get_step("distribute_alerts", "SilenceGroupStep")
step = SilenceGroupStep(self.log_record.alert_group.channel.organization.slack_team_identity)
step.process_signal(self.log_record)
def on_un_silence(self):
UnSilenceGroupStep = ScenarioStep.get_step("distribute_alerts", "UnSilenceGroupStep")
step = UnSilenceGroupStep(self.log_record.alert_group.channel.organization.slack_team_identity)
step.process_signal(self.log_record)
def on_invite(self):
InviteOtherPersonToIncident = ScenarioStep.get_step("distribute_alerts", "InviteOtherPersonToIncident")
step = InviteOtherPersonToIncident(self.log_record.alert_group.channel.organization.slack_team_identity)
step.process_signal(self.log_record)
def on_re_invite(self):
self.on_invite()
def on_un_invite(self):
StopInvitationProcess = ScenarioStep.get_step("distribute_alerts", "StopInvitationProcess")
step = StopInvitationProcess(self.log_record.alert_group.channel.organization.slack_team_identity)
step.process_signal(self.log_record)
def on_auto_un_acknowledge(self):
self.on_un_acknowledge()
def on_ack_reminder_triggered(self):
# deprecated, remove this handler after release
AcknowledgeConfirmationStep = ScenarioStep.get_step("distribute_alerts", "AcknowledgeConfirmationStep")
step = AcknowledgeConfirmationStep(self.log_record.alert_group.channel.organization.slack_team_identity)
step.process_signal(self.log_record)
def on_wiped(self):
WipeGroupStep = ScenarioStep.get_step("distribute_alerts", "WipeGroupStep")
step = WipeGroupStep(self.log_record.alert_group.channel.organization.slack_team_identity)
step.process_signal(self.log_record)
def on_deleted(self):
DeleteGroupStep = ScenarioStep.get_step("distribute_alerts", "DeleteGroupStep")
step = DeleteGroupStep(self.log_record.alert_group.channel.organization.slack_team_identity)
step.process_signal(self.log_record)
def get_handler(self):
handler_name = self.get_handler_name()
if hasattr(self, handler_name):
handler = getattr(self, handler_name)
else:
handler = self.on_handler_not_found
return handler
def get_handler_name(self):
return self.HANDLER_PREFIX + self.get_handlers_map()[self.log_record.type]
@classmethod
def on_handler_not_found(cls):
pass
@classmethod
def get_log_record_from_kwargs(cls, **kwargs) -> typing.Optional["AlertGroupLogRecord"]:
from apps.alerts.models import AlertGroupLogRecord
log_record = None
log_record_id = kwargs["log_record"]
if not isinstance(log_record_id, AlertGroupLogRecord):
try:
log_record = AlertGroupLogRecord.objects.get(pk=log_record_id)
except AlertGroupLogRecord.DoesNotExist:
logger.warning(f"log record {log_record_id} never created or has been deleted")
else:
log_record = log_record_id
return log_record