Alert list view & caching rework (#216)
* remove cache usage in AlertGroupView * remove CustomSearchFilter * remove caching for alerts * remove readonly db setup * render templates on alert creation * serialize only necessary fields on alert groups list * optimize AlertGroupListSerializer * return on-demand templating for alerts * return on-demand templating for alert groups * use CursorPaginator * remove templating on alert create * pass alert to AlertGroupWebRenderer * alert_count -> alerts_count * make sql joins after pagination * add migration * bring alert.save() back * fix tests * fix tests * fix tests * add perpage query param * add cursor pagination to incidents page * remove cached_render_for_web usage * post merge fix * keep cursor * lint * remove get_alert_groups_and_days_for_previous_same_period * fix pagination on navigate * refine search_fields on AlertGroupView Co-authored-by: Maxim <hello.makson@gmail.com> Co-authored-by: Maxim <maxim.mordasov@grafana.com>
This commit is contained in:
parent
78598fb95b
commit
16bbfbbe73
33 changed files with 438 additions and 653 deletions
|
|
@ -18,9 +18,12 @@ class AlertBaseRenderer(ABC):
|
||||||
|
|
||||||
|
|
||||||
class AlertGroupBaseRenderer(ABC):
|
class AlertGroupBaseRenderer(ABC):
|
||||||
def __init__(self, alert_group):
|
def __init__(self, alert_group, alert=None):
|
||||||
|
if alert is None:
|
||||||
|
alert = alert_group.alerts.first()
|
||||||
|
|
||||||
self.alert_group = alert_group
|
self.alert_group = alert_group
|
||||||
self.alert_renderer = self.alert_renderer_class(self.alert_group.alerts.first())
|
self.alert_renderer = self.alert_renderer_class(alert)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,11 @@ class AlertWebRenderer(AlertBaseRenderer):
|
||||||
|
|
||||||
|
|
||||||
class AlertGroupWebRenderer(AlertGroupBaseRenderer):
|
class AlertGroupWebRenderer(AlertGroupBaseRenderer):
|
||||||
def __init__(self, alert_group):
|
def __init__(self, alert_group, alert=None):
|
||||||
super().__init__(alert_group)
|
if alert is None:
|
||||||
|
alert = alert_group.alerts.last()
|
||||||
|
|
||||||
# use the last alert to render content
|
super().__init__(alert_group, alert)
|
||||||
self.alert_renderer = self.alert_renderer_class(self.alert_group.alerts.last())
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def alert_renderer_class(self):
|
def alert_renderer_class(self):
|
||||||
|
|
|
||||||
21
engine/apps/alerts/migrations/0004_auto_20220711_1106.py
Normal file
21
engine/apps/alerts/migrations/0004_auto_20220711_1106.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Generated by Django 3.2.13 on 2022-07-11 11:06
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('alerts', '0003_grafanaalertingcontactpoint_datasource_uid'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='alertgroup',
|
||||||
|
name='active_cache_for_web_calculation_id',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='alertgroup',
|
||||||
|
name='cached_render_for_web',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -5,7 +5,7 @@ from uuid import uuid4
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.validators import MinLengthValidator
|
from django.core.validators import MinLengthValidator
|
||||||
from django.db import models, transaction
|
from django.db import models
|
||||||
from django.db.models import JSONField
|
from django.db.models import JSONField
|
||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
|
|
||||||
|
|
@ -261,9 +261,6 @@ def listen_for_alert_model_save(sender, instance, created, *args, **kwargs):
|
||||||
else:
|
else:
|
||||||
distribute_alert.apply_async((instance.pk,), countdown=TASK_DELAY_SECONDS)
|
distribute_alert.apply_async((instance.pk,), countdown=TASK_DELAY_SECONDS)
|
||||||
|
|
||||||
logger.info(f"Recalculate AG cache. Reason: save alert model {instance.pk}")
|
|
||||||
transaction.on_commit(instance.group.schedule_cache_for_web)
|
|
||||||
|
|
||||||
|
|
||||||
# Connect signal to base Alert class
|
# Connect signal to base Alert class
|
||||||
post_save.connect(listen_for_alert_model_save, Alert)
|
post_save.connect(listen_for_alert_model_save, Alert)
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,9 @@ import pytz
|
||||||
from celery import uuid as celery_uuid
|
from celery import uuid as celery_uuid
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
|
||||||
from django.core.validators import MinLengthValidator
|
from django.core.validators import MinLengthValidator
|
||||||
from django.db import IntegrityError, models, transaction
|
from django.db import IntegrityError, models
|
||||||
from django.db.models import JSONField, Q, QuerySet
|
from django.db.models import JSONField, Q, QuerySet
|
||||||
from django.db.models.signals import post_save
|
|
||||||
from django.dispatch import receiver
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
|
|
@ -22,16 +19,9 @@ from apps.alerts.incident_appearance.renderers.constants import DEFAULT_BACKUP_T
|
||||||
from apps.alerts.incident_appearance.renderers.slack_renderer import AlertGroupSlackRenderer
|
from apps.alerts.incident_appearance.renderers.slack_renderer import AlertGroupSlackRenderer
|
||||||
from apps.alerts.incident_log_builder import IncidentLogBuilder
|
from apps.alerts.incident_log_builder import IncidentLogBuilder
|
||||||
from apps.alerts.signals import alert_group_action_triggered_signal
|
from apps.alerts.signals import alert_group_action_triggered_signal
|
||||||
from apps.alerts.tasks import (
|
from apps.alerts.tasks import acknowledge_reminder_task, call_ack_url, send_alert_group_signal, unsilence_task
|
||||||
acknowledge_reminder_task,
|
|
||||||
call_ack_url,
|
|
||||||
schedule_cache_for_alert_group,
|
|
||||||
send_alert_group_signal,
|
|
||||||
unsilence_task,
|
|
||||||
)
|
|
||||||
from apps.slack.slack_formatter import SlackFormatter
|
from apps.slack.slack_formatter import SlackFormatter
|
||||||
from apps.user_management.models import User
|
from apps.user_management.models import User
|
||||||
from common.mixins.use_random_readonly_db_manager_mixin import UseRandomReadonlyDbManagerMixin
|
|
||||||
from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length
|
from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length
|
||||||
from common.utils import clean_markup, str_or_backup
|
from common.utils import clean_markup, str_or_backup
|
||||||
|
|
||||||
|
|
@ -108,10 +98,6 @@ class UnarchivedAlertGroupQuerySet(models.QuerySet):
|
||||||
return super().filter(*args, **kwargs, is_archived=False)
|
return super().filter(*args, **kwargs, is_archived=False)
|
||||||
|
|
||||||
|
|
||||||
class AlertGroupManager(UseRandomReadonlyDbManagerMixin, models.Manager):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AlertGroupSlackRenderingMixin:
|
class AlertGroupSlackRenderingMixin:
|
||||||
"""
|
"""
|
||||||
Ideally this mixin should not exist. Instead of this instance of AlertGroupSlackRenderer should be created and used
|
Ideally this mixin should not exist. Instead of this instance of AlertGroupSlackRenderer should be created and used
|
||||||
|
|
@ -134,8 +120,8 @@ class AlertGroupSlackRenderingMixin:
|
||||||
|
|
||||||
|
|
||||||
class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.Model):
|
class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.Model):
|
||||||
all_objects = AlertGroupManager.from_queryset(AlertGroupQuerySet)()
|
all_objects = AlertGroupQuerySet.as_manager()
|
||||||
unarchived_objects = AlertGroupManager.from_queryset(UnarchivedAlertGroupQuerySet)()
|
unarchived_objects = UnarchivedAlertGroupQuerySet.as_manager()
|
||||||
|
|
||||||
(
|
(
|
||||||
NEW,
|
NEW,
|
||||||
|
|
@ -242,8 +228,6 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
||||||
|
|
||||||
active_escalation_id = models.CharField(max_length=100, null=True, default=None) # ID generated by celery
|
active_escalation_id = models.CharField(max_length=100, null=True, default=None) # ID generated by celery
|
||||||
active_resolve_calculation_id = models.CharField(max_length=100, null=True, default=None) # ID generated by celery
|
active_resolve_calculation_id = models.CharField(max_length=100, null=True, default=None) # ID generated by celery
|
||||||
# ID generated by celery
|
|
||||||
active_cache_for_web_calculation_id = models.CharField(max_length=100, null=True, default=None)
|
|
||||||
|
|
||||||
SILENCE_DELAY_OPTIONS = (
|
SILENCE_DELAY_OPTIONS = (
|
||||||
(1800, "30 minutes"),
|
(1800, "30 minutes"),
|
||||||
|
|
@ -315,8 +299,6 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
||||||
related_name="dependent_alert_groups",
|
related_name="dependent_alert_groups",
|
||||||
)
|
)
|
||||||
|
|
||||||
cached_render_for_web = JSONField(default=dict)
|
|
||||||
|
|
||||||
last_unique_unacknowledge_process_id = models.CharField(max_length=100, null=True, default=None)
|
last_unique_unacknowledge_process_id = models.CharField(max_length=100, null=True, default=None)
|
||||||
is_archived = models.BooleanField(default=False)
|
is_archived = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
|
@ -404,18 +386,6 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
||||||
def is_alert_a_resolve_signal(self, alert):
|
def is_alert_a_resolve_signal(self, alert):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def cache_for_web(self, organization):
|
|
||||||
from apps.api.serializers.alert_group import AlertGroupSerializer
|
|
||||||
|
|
||||||
# Re-take object to switch connection from readonly db to master.
|
|
||||||
_self = AlertGroup.all_objects.get(pk=self.pk)
|
|
||||||
_self.cached_render_for_web = AlertGroupSerializer(self, context={"organization": organization}).data
|
|
||||||
self.cached_render_for_web = _self.cached_render_for_web
|
|
||||||
_self.save(update_fields=["cached_render_for_web"])
|
|
||||||
|
|
||||||
def schedule_cache_for_web(self):
|
|
||||||
schedule_cache_for_alert_group.apply_async((self.pk,))
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def permalink(self):
|
def permalink(self):
|
||||||
if self.slack_message is not None:
|
if self.slack_message is not None:
|
||||||
|
|
@ -425,10 +395,6 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
||||||
def web_link(self):
|
def web_link(self):
|
||||||
return urljoin(self.channel.organization.web_link, f"?page=incident&id={self.public_primary_key}")
|
return urljoin(self.channel.organization.web_link, f"?page=incident&id={self.public_primary_key}")
|
||||||
|
|
||||||
@property
|
|
||||||
def alerts_count(self):
|
|
||||||
return self.alerts.count()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def happened_while_maintenance(self):
|
def happened_while_maintenance(self):
|
||||||
return self.root_alert_group is not None and self.root_alert_group.maintenance_uuid is not None
|
return self.root_alert_group is not None and self.root_alert_group.maintenance_uuid is not None
|
||||||
|
|
@ -449,10 +415,6 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
||||||
self.unresolve()
|
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")
|
||||||
|
|
||||||
# clear resolve report cache
|
|
||||||
cache_key = "render_after_resolve_report_json_{}".format(self.pk)
|
|
||||||
cache.delete(cache_key)
|
|
||||||
|
|
||||||
self.acknowledge(acknowledged_by_user=user, acknowledged_by=AlertGroup.USER)
|
self.acknowledge(acknowledged_by_user=user, acknowledged_by=AlertGroup.USER)
|
||||||
self.stop_escalation()
|
self.stop_escalation()
|
||||||
if self.is_root_alert_group:
|
if self.is_root_alert_group:
|
||||||
|
|
@ -673,9 +635,6 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
||||||
self.unresolve()
|
self.unresolve()
|
||||||
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)
|
||||||
|
|
||||||
# clear resolve report cache
|
|
||||||
self.drop_cached_after_resolve_report_json()
|
|
||||||
|
|
||||||
if self.is_root_alert_group:
|
if self.is_root_alert_group:
|
||||||
self.start_escalation_if_needed()
|
self.start_escalation_if_needed()
|
||||||
|
|
||||||
|
|
@ -848,10 +807,6 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
||||||
self.unresolve()
|
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")
|
||||||
|
|
||||||
# clear resolve report cache
|
|
||||||
cache_key = "render_after_resolve_report_json_{}".format(self.pk)
|
|
||||||
cache.delete(cache_key)
|
|
||||||
|
|
||||||
if self.acknowledged:
|
if self.acknowledged:
|
||||||
self.unacknowledge()
|
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")
|
||||||
|
|
@ -1060,8 +1015,6 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
||||||
author=user,
|
author=user,
|
||||||
reason="Bulk action acknowledge",
|
reason="Bulk action acknowledge",
|
||||||
)
|
)
|
||||||
# clear resolve report cache
|
|
||||||
alert_group.drop_cached_after_resolve_report_json()
|
|
||||||
|
|
||||||
for alert_group in alert_groups_to_unsilence_before_acknowledge_list:
|
for alert_group in alert_groups_to_unsilence_before_acknowledge_list:
|
||||||
alert_group.log_records.create(
|
alert_group.log_records.create(
|
||||||
|
|
@ -1194,8 +1147,6 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
||||||
reason="Bulk action restart",
|
reason="Bulk action restart",
|
||||||
)
|
)
|
||||||
|
|
||||||
alert_group.drop_cached_after_resolve_report_json()
|
|
||||||
|
|
||||||
if alert_group.is_root_alert_group:
|
if alert_group.is_root_alert_group:
|
||||||
alert_group.start_escalation_if_needed()
|
alert_group.start_escalation_if_needed()
|
||||||
|
|
||||||
|
|
@ -1293,7 +1244,6 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
||||||
author=user,
|
author=user,
|
||||||
reason="Bulk action silence",
|
reason="Bulk action silence",
|
||||||
)
|
)
|
||||||
alert_group.drop_cached_after_resolve_report_json()
|
|
||||||
|
|
||||||
for alert_group in alert_groups_to_unsilence_before_silence_list:
|
for alert_group in alert_groups_to_unsilence_before_silence_list:
|
||||||
alert_group.log_records.create(
|
alert_group.log_records.create(
|
||||||
|
|
@ -1483,7 +1433,7 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
||||||
else:
|
else:
|
||||||
return "Acknowledged"
|
return "Acknowledged"
|
||||||
|
|
||||||
def non_cached_after_resolve_report_json(self):
|
def render_after_resolve_report_json(self):
|
||||||
AlertGroupLogRecord = apps.get_model("alerts", "AlertGroupLogRecord")
|
AlertGroupLogRecord = apps.get_model("alerts", "AlertGroupLogRecord")
|
||||||
UserNotificationPolicyLogRecord = apps.get_model("base", "UserNotificationPolicyLogRecord")
|
UserNotificationPolicyLogRecord = apps.get_model("base", "UserNotificationPolicyLogRecord")
|
||||||
ResolutionNote = apps.get_model("alerts", "ResolutionNote")
|
ResolutionNote = apps.get_model("alerts", "ResolutionNote")
|
||||||
|
|
@ -1501,21 +1451,6 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
||||||
result_log_report.append(log_record.render_log_line_json())
|
result_log_report.append(log_record.render_log_line_json())
|
||||||
return result_log_report
|
return result_log_report
|
||||||
|
|
||||||
def render_after_resolve_report_json(self):
|
|
||||||
cache_key = "render_after_resolve_report_json_{}".format(self.pk)
|
|
||||||
|
|
||||||
# cache.get_or_set in some cases returns None, so use get and set cache methods separately
|
|
||||||
log_report = cache.get(cache_key)
|
|
||||||
if log_report is None:
|
|
||||||
log_report = self.non_cached_after_resolve_report_json()
|
|
||||||
cache.set(cache_key, log_report)
|
|
||||||
return log_report
|
|
||||||
|
|
||||||
def drop_cached_after_resolve_report_json(self):
|
|
||||||
cache_key = "render_after_resolve_report_json_{}".format(self.pk)
|
|
||||||
if cache_key in cache:
|
|
||||||
cache.delete(cache_key)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_resolution_notes(self):
|
def has_resolution_notes(self):
|
||||||
return self.resolution_notes.exists()
|
return self.resolution_notes.exists()
|
||||||
|
|
@ -1595,14 +1530,3 @@ class AlertGroup(AlertGroupSlackRenderingMixin, EscalationSnapshotMixin, models.
|
||||||
)
|
)
|
||||||
|
|
||||||
return stop_escalation_log
|
return stop_escalation_log
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=AlertGroup)
|
|
||||||
def listen_for_alert_group_model_save(sender, instance, created, *args, **kwargs):
|
|
||||||
if (
|
|
||||||
kwargs is not None
|
|
||||||
and "update_fields" in kwargs
|
|
||||||
and kwargs["update_fields"] is dict
|
|
||||||
and "cached_render_for_web" not in kwargs["update_fields"]
|
|
||||||
):
|
|
||||||
transaction.on_commit(instance.schedule_cache_for_alert_group)
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import logging
|
||||||
|
|
||||||
import humanize
|
import humanize
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.db import models, transaction
|
from django.db import models
|
||||||
from django.db.models import JSONField
|
from django.db.models import JSONField
|
||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
@ -546,7 +546,6 @@ class AlertGroupLogRecord(models.Model):
|
||||||
|
|
||||||
@receiver(post_save, sender=AlertGroupLogRecord)
|
@receiver(post_save, sender=AlertGroupLogRecord)
|
||||||
def listen_for_alertgrouplogrecord(sender, instance, created, *args, **kwargs):
|
def listen_for_alertgrouplogrecord(sender, instance, created, *args, **kwargs):
|
||||||
instance.alert_group.drop_cached_after_resolve_report_json()
|
|
||||||
if instance.type != AlertGroupLogRecord.TYPE_DELETED:
|
if instance.type != AlertGroupLogRecord.TYPE_DELETED:
|
||||||
if not instance.alert_group.is_maintenance_incident:
|
if not instance.alert_group.is_maintenance_incident:
|
||||||
alert_group_pk = instance.alert_group.pk
|
alert_group_pk = instance.alert_group.pk
|
||||||
|
|
@ -555,6 +554,3 @@ def listen_for_alertgrouplogrecord(sender, instance, created, *args, **kwargs):
|
||||||
f"alert group event: {instance.get_type_display()}"
|
f"alert group event: {instance.get_type_display()}"
|
||||||
)
|
)
|
||||||
send_update_log_report_signal.apply_async(kwargs={"alert_group_pk": alert_group_pk}, countdown=8)
|
send_update_log_report_signal.apply_async(kwargs={"alert_group_pk": alert_group_pk}, countdown=8)
|
||||||
|
|
||||||
logger.info(f"Recalculate AG cache. Reason: save alert_group_log_record model {instance.pk}")
|
|
||||||
transaction.on_commit(instance.alert_group.schedule_cache_for_web)
|
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,7 @@ from jinja2 import Template
|
||||||
from apps.alerts.grafana_alerting_sync_manager.grafana_alerting_sync import GrafanaAlertingSyncManager
|
from apps.alerts.grafana_alerting_sync_manager.grafana_alerting_sync import GrafanaAlertingSyncManager
|
||||||
from apps.alerts.integration_options_mixin import IntegrationOptionsMixin
|
from apps.alerts.integration_options_mixin import IntegrationOptionsMixin
|
||||||
from apps.alerts.models.maintainable_object import MaintainableObject
|
from apps.alerts.models.maintainable_object import MaintainableObject
|
||||||
from apps.alerts.tasks import (
|
from apps.alerts.tasks import disable_maintenance, sync_grafana_alerting_contact_points
|
||||||
disable_maintenance,
|
|
||||||
invalidate_web_cache_for_alert_group,
|
|
||||||
sync_grafana_alerting_contact_points,
|
|
||||||
)
|
|
||||||
from apps.base.messaging import get_messaging_backend_from_id
|
from apps.base.messaging import get_messaging_backend_from_id
|
||||||
from apps.base.utils import live_settings
|
from apps.base.utils import live_settings
|
||||||
from apps.integrations.metadata import heartbeat
|
from apps.integrations.metadata import heartbeat
|
||||||
|
|
@ -693,16 +689,6 @@ def listen_for_alertreceivechannel_model_save(sender, instance, created, *args,
|
||||||
create_organization_log(
|
create_organization_log(
|
||||||
instance.organization, None, OrganizationLogType.TYPE_HEARTBEAT_CREATED, description
|
instance.organization, None, OrganizationLogType.TYPE_HEARTBEAT_CREATED, description
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
logger.info(f"Drop AG cache. Reason: save alert_receive_channel {instance.pk}")
|
|
||||||
if kwargs is not None:
|
|
||||||
if "update_fields" in kwargs:
|
|
||||||
if kwargs["update_fields"] is not None:
|
|
||||||
# Hack to not to invalidate web cache on AlertReceiveChannel.start_send_rate_limit_message_task
|
|
||||||
if "rate_limit_message_task_id" in kwargs["update_fields"]:
|
|
||||||
return
|
|
||||||
|
|
||||||
invalidate_web_cache_for_alert_group.apply_async(kwargs={"channel_pk": instance.pk})
|
|
||||||
|
|
||||||
if instance.integration == AlertReceiveChannel.INTEGRATION_GRAFANA_ALERTING:
|
if instance.integration == AlertReceiveChannel.INTEGRATION_GRAFANA_ALERTING:
|
||||||
if created:
|
if created:
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ from .custom_button_result import custom_button_result # noqa: F401
|
||||||
from .delete_alert_group import delete_alert_group # noqa: F401
|
from .delete_alert_group import delete_alert_group # noqa: F401
|
||||||
from .distribute_alert import distribute_alert # noqa: F401
|
from .distribute_alert import distribute_alert # noqa: F401
|
||||||
from .escalate_alert_group import escalate_alert_group # noqa: F401
|
from .escalate_alert_group import escalate_alert_group # noqa: F401
|
||||||
from .invalidate_web_cache_for_alert_group import invalidate_web_cache_for_alert_group # noqa: F401
|
from .invalidate_web_cache_for_alert_group import invalidate_web_cache_for_alert_group # noqa: F401, todo: remove
|
||||||
from .invite_user_to_join_incident import invite_user_to_join_incident # noqa: F401
|
from .invite_user_to_join_incident import invite_user_to_join_incident # noqa: F401
|
||||||
from .maintenance import disable_maintenance # noqa: F401
|
from .maintenance import disable_maintenance # noqa: F401
|
||||||
from .notify_all import notify_all_task # noqa: F401
|
from .notify_all import notify_all_task # noqa: F401
|
||||||
|
|
|
||||||
|
|
@ -1,54 +1,19 @@
|
||||||
from celery.utils.log import get_task_logger
|
|
||||||
from django.apps import apps
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
|
||||||
|
|
||||||
from common.custom_celery_tasks import shared_dedicated_queue_retry_task
|
from common.custom_celery_tasks import shared_dedicated_queue_retry_task
|
||||||
|
|
||||||
logger = get_task_logger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def get_cache_key_caching_alert_group_for_web(alert_group_pk):
|
|
||||||
CACHE_KEY_PREFIX = "cache_alert_group_for_web"
|
|
||||||
return f"{CACHE_KEY_PREFIX}_{alert_group_pk}"
|
|
||||||
|
|
||||||
|
|
||||||
@shared_dedicated_queue_retry_task(
|
@shared_dedicated_queue_retry_task(
|
||||||
autoretry_for=(Exception,), retry_backoff=True, max_retries=0 if settings.DEBUG else None
|
autoretry_for=(Exception,), retry_backoff=True, max_retries=0 if settings.DEBUG else None
|
||||||
)
|
)
|
||||||
def schedule_cache_for_alert_group(alert_group_pk):
|
def schedule_cache_for_alert_group(alert_group_pk):
|
||||||
CACHE_FOR_ALERT_GROUP_LIFETIME = 60
|
# todo: remove
|
||||||
START_CACHE_DELAY = 5 # we introduce delay to avoid recaching after each alert.
|
pass
|
||||||
|
|
||||||
task = cache_alert_group_for_web.apply_async(args=[alert_group_pk], countdown=START_CACHE_DELAY)
|
|
||||||
cache_key = get_cache_key_caching_alert_group_for_web(alert_group_pk)
|
|
||||||
cache.set(cache_key, task.id, timeout=CACHE_FOR_ALERT_GROUP_LIFETIME)
|
|
||||||
|
|
||||||
|
|
||||||
@shared_dedicated_queue_retry_task(
|
@shared_dedicated_queue_retry_task(
|
||||||
autoretry_for=(Exception,), retry_backoff=True, max_retries=0 if settings.DEBUG else None
|
autoretry_for=(Exception,), retry_backoff=True, max_retries=0 if settings.DEBUG else None
|
||||||
)
|
)
|
||||||
def cache_alert_group_for_web(alert_group_pk):
|
def cache_alert_group_for_web(alert_group_pk):
|
||||||
"""
|
# todo: remove
|
||||||
Async task to re-cache alert_group for web.
|
pass
|
||||||
"""
|
|
||||||
cache_key = get_cache_key_caching_alert_group_for_web(alert_group_pk)
|
|
||||||
cached_task_id = cache.get(cache_key)
|
|
||||||
current_task_id = cache_alert_group_for_web.request.id
|
|
||||||
|
|
||||||
if cached_task_id is None:
|
|
||||||
return (
|
|
||||||
f"cache_alert_group_for_web skipped, because of current task_id ({current_task_id})"
|
|
||||||
f" for alert_group {alert_group_pk} doesn't exist in cache, which means this task is not"
|
|
||||||
f" relevant: cache was dropped by engine restart ot CACHE_FOR_ALERT_GROUP_LIFETIME"
|
|
||||||
)
|
|
||||||
if not current_task_id == cached_task_id or cached_task_id is None:
|
|
||||||
return (
|
|
||||||
f"cache_alert_group_for_web skipped, because of current task_id ({current_task_id})"
|
|
||||||
f" doesn't equal to cached task_id ({cached_task_id}) for alert_group {alert_group_pk},"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
AlertGroup = apps.get_model("alerts", "AlertGroup")
|
|
||||||
alert_group = AlertGroup.all_objects.using_readonly_db.get(pk=alert_group_pk)
|
|
||||||
alert_group.cache_for_web(alert_group.channel.organization)
|
|
||||||
logger.info(f"cache_alert_group_for_web: cache refreshed for alert_group {alert_group_pk}")
|
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,11 @@
|
||||||
from django.apps import apps
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from common.custom_celery_tasks import shared_dedicated_queue_retry_task
|
from common.custom_celery_tasks import shared_dedicated_queue_retry_task
|
||||||
|
|
||||||
from .task_logger import task_logger
|
|
||||||
|
|
||||||
|
|
||||||
@shared_dedicated_queue_retry_task(
|
@shared_dedicated_queue_retry_task(
|
||||||
autoretry_for=(Exception,), retry_backoff=True, max_retries=1 if settings.DEBUG else None
|
autoretry_for=(Exception,), retry_backoff=True, max_retries=1 if settings.DEBUG else None
|
||||||
)
|
)
|
||||||
def invalidate_web_cache_for_alert_group(org_pk=None, channel_pk=None, alert_group_pk=None, alert_group_pks=None):
|
def invalidate_web_cache_for_alert_group(org_pk=None, channel_pk=None, alert_group_pk=None, alert_group_pks=None):
|
||||||
AlertGroup = apps.get_model("alerts", "AlertGroup")
|
# todo: remove
|
||||||
DynamicSetting = apps.get_model("base", "DynamicSetting")
|
pass
|
||||||
|
|
||||||
if channel_pk:
|
|
||||||
task_logger.debug(f"invalidate_web_cache_for_alert_group: Reason - alert_receive_channel {channel_pk}")
|
|
||||||
q = AlertGroup.all_objects.filter(channel__pk=channel_pk)
|
|
||||||
elif org_pk:
|
|
||||||
task_logger.debug(f"invalidate_web_cache_for_alert_group: Reason - organization {org_pk}")
|
|
||||||
q = AlertGroup.all_objects.filter(channel__organization__pk=org_pk)
|
|
||||||
elif alert_group_pk:
|
|
||||||
task_logger.debug(f"invalidate_web_cache_for_alert_group: Reason - alert_group {alert_group_pk}")
|
|
||||||
q = AlertGroup.all_objects.filter(pk=alert_group_pk)
|
|
||||||
elif alert_group_pks:
|
|
||||||
task_logger.debug(f"invalidate_web_cache_for_alert_group: Reason - alert_groups {alert_group_pks}")
|
|
||||||
q = AlertGroup.all_objects.filter(pk__in=alert_group_pks)
|
|
||||||
|
|
||||||
skip_task = DynamicSetting.objects.get_or_create(name="skip_invalidate_web_cache_for_alert_group")[0]
|
|
||||||
if skip_task.boolean_value:
|
|
||||||
return "Task has been skipped because of skip_invalidate_web_cache_for_alert_group DynamicSetting"
|
|
||||||
q.update(cached_render_for_web={})
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
import humanize
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from apps.alerts.incident_appearance.renderers.web_renderer import AlertGroupWebRenderer
|
from apps.alerts.incident_appearance.renderers.web_renderer import AlertGroupWebRenderer
|
||||||
|
|
@ -28,50 +26,30 @@ class ShortAlertGroupSerializer(serializers.ModelSerializer):
|
||||||
return AlertGroupWebRenderer(obj).render()
|
return AlertGroupWebRenderer(obj).render()
|
||||||
|
|
||||||
|
|
||||||
class AlertGroupSerializer(EagerLoadingMixin, serializers.ModelSerializer):
|
class AlertGroupListSerializer(EagerLoadingMixin, serializers.ModelSerializer):
|
||||||
"""
|
|
||||||
Attention: It's heavily cached. Make sure to invalidate alertgroup's web cache if you update the format!
|
|
||||||
"""
|
|
||||||
|
|
||||||
pk = serializers.CharField(read_only=True, source="public_primary_key")
|
pk = serializers.CharField(read_only=True, source="public_primary_key")
|
||||||
alert_receive_channel = FastAlertReceiveChannelSerializer(source="channel")
|
alert_receive_channel = FastAlertReceiveChannelSerializer(source="channel")
|
||||||
alerts = serializers.SerializerMethodField("get_limited_alerts")
|
status = serializers.ReadOnlyField()
|
||||||
resolved_by_verbose = serializers.CharField(source="get_resolved_by_display")
|
|
||||||
resolved_by_user = FastUserSerializer(required=False)
|
resolved_by_user = FastUserSerializer(required=False)
|
||||||
acknowledged_by_user = FastUserSerializer(required=False)
|
acknowledged_by_user = FastUserSerializer(required=False)
|
||||||
silenced_by_user = FastUserSerializer(required=False)
|
silenced_by_user = FastUserSerializer(required=False)
|
||||||
related_users = serializers.SerializerMethodField()
|
related_users = serializers.SerializerMethodField()
|
||||||
|
|
||||||
last_alert_at = serializers.SerializerMethodField()
|
|
||||||
|
|
||||||
started_at_verbose = serializers.SerializerMethodField()
|
|
||||||
acknowledged_at_verbose = serializers.SerializerMethodField()
|
|
||||||
resolved_at_verbose = serializers.SerializerMethodField()
|
|
||||||
silenced_at_verbose = serializers.SerializerMethodField()
|
|
||||||
|
|
||||||
dependent_alert_groups = ShortAlertGroupSerializer(many=True)
|
dependent_alert_groups = ShortAlertGroupSerializer(many=True)
|
||||||
root_alert_group = ShortAlertGroupSerializer()
|
root_alert_group = ShortAlertGroupSerializer()
|
||||||
|
|
||||||
alerts_count = serializers.ReadOnlyField()
|
alerts_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
status = serializers.ReadOnlyField()
|
|
||||||
render_for_web = serializers.SerializerMethodField()
|
render_for_web = serializers.SerializerMethodField()
|
||||||
|
|
||||||
PREFETCH_RELATED = [
|
PREFETCH_RELATED = [
|
||||||
"alerts",
|
|
||||||
"dependent_alert_groups",
|
"dependent_alert_groups",
|
||||||
"log_records",
|
|
||||||
"log_records__author",
|
"log_records__author",
|
||||||
"log_records__escalation_policy",
|
|
||||||
"log_records__invitation__invitee",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
SELECT_RELATED = [
|
SELECT_RELATED = [
|
||||||
"slack_message",
|
|
||||||
"channel__organization",
|
"channel__organization",
|
||||||
"slack_message___slack_team_identity",
|
"root_alert_group",
|
||||||
"acknowledged_by_user",
|
|
||||||
"resolved_by_user",
|
"resolved_by_user",
|
||||||
|
"acknowledged_by_user",
|
||||||
"silenced_by_user",
|
"silenced_by_user",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -85,7 +63,6 @@ class AlertGroupSerializer(EagerLoadingMixin, serializers.ModelSerializer):
|
||||||
"alert_receive_channel",
|
"alert_receive_channel",
|
||||||
"resolved",
|
"resolved",
|
||||||
"resolved_by",
|
"resolved_by",
|
||||||
"resolved_by_verbose",
|
|
||||||
"resolved_by_user",
|
"resolved_by_user",
|
||||||
"resolved_at",
|
"resolved_at",
|
||||||
"acknowledged_at",
|
"acknowledged_at",
|
||||||
|
|
@ -96,44 +73,18 @@ class AlertGroupSerializer(EagerLoadingMixin, serializers.ModelSerializer):
|
||||||
"silenced",
|
"silenced",
|
||||||
"silenced_by_user",
|
"silenced_by_user",
|
||||||
"silenced_at",
|
"silenced_at",
|
||||||
"silenced_at_verbose",
|
|
||||||
"silenced_until",
|
"silenced_until",
|
||||||
"started_at",
|
"started_at",
|
||||||
"last_alert_at",
|
|
||||||
"silenced_until",
|
"silenced_until",
|
||||||
"permalink",
|
|
||||||
"alerts",
|
|
||||||
"related_users",
|
"related_users",
|
||||||
"started_at_verbose",
|
|
||||||
"acknowledged_at_verbose",
|
|
||||||
"resolved_at_verbose",
|
|
||||||
"render_for_web",
|
"render_for_web",
|
||||||
"render_after_resolve_report_json",
|
|
||||||
"dependent_alert_groups",
|
"dependent_alert_groups",
|
||||||
"root_alert_group",
|
"root_alert_group",
|
||||||
"status",
|
"status",
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_last_alert_at(self, obj):
|
def get_render_for_web(self, obj):
|
||||||
last_alert = obj.alerts.last()
|
return AlertGroupWebRenderer(obj, obj.last_alert).render()
|
||||||
# TODO: This is a Hotfix for 0.0.27
|
|
||||||
if last_alert is None:
|
|
||||||
logger.warning(f"obj {obj} doesn't have last_alert!")
|
|
||||||
return ""
|
|
||||||
return str(last_alert.created_at)
|
|
||||||
|
|
||||||
def get_limited_alerts(self, obj):
|
|
||||||
"""
|
|
||||||
Overriding default alerts because there are alert_groups with thousands of them.
|
|
||||||
It's just too slow, we need to cut here.
|
|
||||||
"""
|
|
||||||
alerts = obj.alerts.all()[:100]
|
|
||||||
|
|
||||||
if len(alerts) > 90:
|
|
||||||
for alert in alerts:
|
|
||||||
alert.title = str(alert.title) + " Only last 100 alerts are shown. Use Amixr API to fetch all of them."
|
|
||||||
|
|
||||||
return AlertSerializer(alerts, many=True).data
|
|
||||||
|
|
||||||
def get_related_users(self, obj):
|
def get_related_users(self, obj):
|
||||||
users_ids = set()
|
users_ids = set()
|
||||||
|
|
@ -159,37 +110,39 @@ class AlertGroupSerializer(EagerLoadingMixin, serializers.ModelSerializer):
|
||||||
users_ids.add(log_record.author.public_primary_key)
|
users_ids.add(log_record.author.public_primary_key)
|
||||||
return users
|
return users
|
||||||
|
|
||||||
def get_started_at_verbose(self, obj):
|
|
||||||
started_at_verbose = None
|
|
||||||
if obj.started_at is not None:
|
|
||||||
started_at_verbose = humanize.naturaltime(
|
|
||||||
datetime.now().replace(tzinfo=None) - obj.started_at.replace(tzinfo=None)
|
|
||||||
)
|
|
||||||
return started_at_verbose
|
|
||||||
|
|
||||||
def get_acknowledged_at_verbose(self, obj):
|
class AlertGroupSerializer(AlertGroupListSerializer):
|
||||||
acknowledged_at_verbose = None
|
alerts = serializers.SerializerMethodField("get_limited_alerts")
|
||||||
if obj.acknowledged_at is not None:
|
last_alert_at = serializers.SerializerMethodField()
|
||||||
acknowledged_at_verbose = humanize.naturaltime(
|
|
||||||
datetime.now().replace(tzinfo=None) - obj.acknowledged_at.replace(tzinfo=None)
|
|
||||||
) # TODO: Deal with timezones
|
|
||||||
return acknowledged_at_verbose
|
|
||||||
|
|
||||||
def get_resolved_at_verbose(self, obj):
|
class Meta(AlertGroupListSerializer.Meta):
|
||||||
resolved_at_verbose = None
|
fields = AlertGroupListSerializer.Meta.fields + [
|
||||||
if obj.resolved_at is not None:
|
"alerts",
|
||||||
resolved_at_verbose = humanize.naturaltime(
|
"render_after_resolve_report_json",
|
||||||
datetime.now().replace(tzinfo=None) - obj.resolved_at.replace(tzinfo=None)
|
"permalink",
|
||||||
) # TODO: Deal with timezones
|
"last_alert_at",
|
||||||
return resolved_at_verbose
|
]
|
||||||
|
|
||||||
def get_silenced_at_verbose(self, obj):
|
|
||||||
silenced_at_verbose = None
|
|
||||||
if obj.silenced_at is not None:
|
|
||||||
silenced_at_verbose = humanize.naturaltime(
|
|
||||||
datetime.now().replace(tzinfo=None) - obj.silenced_at.replace(tzinfo=None)
|
|
||||||
) # TODO: Deal with timezones
|
|
||||||
return silenced_at_verbose
|
|
||||||
|
|
||||||
def get_render_for_web(self, obj):
|
def get_render_for_web(self, obj):
|
||||||
return AlertGroupWebRenderer(obj).render()
|
return AlertGroupWebRenderer(obj).render()
|
||||||
|
|
||||||
|
def get_last_alert_at(self, obj):
|
||||||
|
last_alert = obj.alerts.last()
|
||||||
|
|
||||||
|
if not last_alert:
|
||||||
|
return obj.started_at
|
||||||
|
|
||||||
|
return last_alert.created_at
|
||||||
|
|
||||||
|
def get_limited_alerts(self, obj):
|
||||||
|
"""
|
||||||
|
Overriding default alerts because there are alert_groups with thousands of them.
|
||||||
|
It's just too slow, we need to cut here.
|
||||||
|
"""
|
||||||
|
alerts = obj.alerts.all()[:100]
|
||||||
|
|
||||||
|
if len(alerts) > 90:
|
||||||
|
for alert in alerts:
|
||||||
|
alert.title = str(alert.title) + " Only last 100 alerts are shown. Use OnCall API to fetch all of them."
|
||||||
|
|
||||||
|
return AlertSerializer(alerts, many=True).data
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from apps.alerts.models import AlertGroup, ResolutionNote
|
from apps.alerts.models import AlertGroup, ResolutionNote
|
||||||
from apps.alerts.tasks import invalidate_web_cache_for_alert_group
|
|
||||||
from apps.api.serializers.user import FastUserSerializer
|
from apps.api.serializers.user import FastUserSerializer
|
||||||
from common.api_helpers.custom_fields import OrganizationFilteredPrimaryKeyRelatedField
|
from common.api_helpers.custom_fields import OrganizationFilteredPrimaryKeyRelatedField
|
||||||
from common.api_helpers.exceptions import BadRequest
|
from common.api_helpers.exceptions import BadRequest
|
||||||
|
|
@ -39,9 +38,6 @@ class ResolutionNoteSerializer(EagerLoadingMixin, serializers.ModelSerializer):
|
||||||
validated_data["author"] = self.context["request"].user
|
validated_data["author"] = self.context["request"].user
|
||||||
validated_data["source"] = ResolutionNote.Source.WEB
|
validated_data["source"] = ResolutionNote.Source.WEB
|
||||||
created_instance = super().create(validated_data)
|
created_instance = super().create(validated_data)
|
||||||
# Invalidate alert group cache because resolution notes shown in alert group's timeline
|
|
||||||
created_instance.alert_group.drop_cached_after_resolve_report_json()
|
|
||||||
invalidate_web_cache_for_alert_group(alert_group_pk=created_instance.alert_group.pk)
|
|
||||||
return created_instance
|
return created_instance
|
||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
|
|
@ -57,8 +53,5 @@ class ResolutionNoteUpdateSerializer(ResolutionNoteSerializer):
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
if instance.source != ResolutionNote.Source.WEB:
|
if instance.source != ResolutionNote.Source.WEB:
|
||||||
raise BadRequest(detail="Cannot update message with this source type")
|
raise BadRequest(detail="Cannot update message with this source type")
|
||||||
updated_instance = super().update(instance, validated_data)
|
|
||||||
# Invalidate alert group cache because resolution notes shown in alert group's timeline
|
return super().update(instance, validated_data)
|
||||||
updated_instance.alert_group.drop_cached_after_resolve_report_json()
|
|
||||||
invalidate_web_cache_for_alert_group(alert_group_pk=updated_instance.alert_group.pk)
|
|
||||||
return updated_instance
|
|
||||||
|
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
from celery.utils.log import get_task_logger
|
|
||||||
from django.apps import apps
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.cache import cache
|
|
||||||
|
|
||||||
from common.custom_celery_tasks import shared_dedicated_queue_retry_task
|
|
||||||
|
|
||||||
logger = get_task_logger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def get_cache_key_caching_alert_group_for_web(alert_group_pk):
|
|
||||||
CACHE_KEY_PREFIX = "cache_alert_group_for_web"
|
|
||||||
return f"{CACHE_KEY_PREFIX}_{alert_group_pk}"
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: remove this tasks after all of them will be processed in prod
|
|
||||||
@shared_dedicated_queue_retry_task(
|
|
||||||
autoretry_for=(Exception,), retry_backoff=True, max_retries=0 if settings.DEBUG else None
|
|
||||||
)
|
|
||||||
def schedule_cache_for_alert_group(alert_group_pk):
|
|
||||||
CACHE_FOR_ALERT_GROUP_LIFETIME = 60
|
|
||||||
START_CACHE_DELAY = 5 # we introduce delay to avoid recaching after each alert.
|
|
||||||
|
|
||||||
task = cache_alert_group_for_web.apply_async(args=[alert_group_pk], countdown=START_CACHE_DELAY)
|
|
||||||
cache_key = get_cache_key_caching_alert_group_for_web(alert_group_pk)
|
|
||||||
cache.set(cache_key, task.id, timeout=CACHE_FOR_ALERT_GROUP_LIFETIME)
|
|
||||||
|
|
||||||
|
|
||||||
@shared_dedicated_queue_retry_task(
|
|
||||||
autoretry_for=(Exception,), retry_backoff=True, max_retries=0 if settings.DEBUG else None
|
|
||||||
)
|
|
||||||
def cache_alert_group_for_web(alert_group_pk):
|
|
||||||
"""
|
|
||||||
Async task to re-cache alert_group for web.
|
|
||||||
"""
|
|
||||||
cache_key = get_cache_key_caching_alert_group_for_web(alert_group_pk)
|
|
||||||
cached_task_id = cache.get(cache_key)
|
|
||||||
current_task_id = cache_alert_group_for_web.request.id
|
|
||||||
|
|
||||||
if cached_task_id is None:
|
|
||||||
return (
|
|
||||||
f"cache_alert_group_for_web skipped, because of current task_id ({current_task_id})"
|
|
||||||
f" for alert_group {alert_group_pk} doesn't exist in cache, which means this task is not"
|
|
||||||
f" relevant: cache was dropped by engine restart ot CACHE_FOR_ALERT_GROUP_LIFETIME"
|
|
||||||
)
|
|
||||||
if not current_task_id == cached_task_id or cached_task_id is None:
|
|
||||||
return (
|
|
||||||
f"cache_alert_group_for_web skipped, because of current task_id ({current_task_id})"
|
|
||||||
f" doesn't equal to cached task_id ({cached_task_id}) for alert_group {alert_group_pk},"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
AlertGroup = apps.get_model("alerts", "AlertGroup")
|
|
||||||
alert_group = AlertGroup.all_objects.using_readonly_db.get(pk=alert_group_pk)
|
|
||||||
alert_group.cache_for_web(alert_group.channel.organization)
|
|
||||||
logger.info(f"cache_alert_group_for_web: cache refreshed for alert_group {alert_group_pk}")
|
|
||||||
|
|
@ -63,7 +63,7 @@ def test_get_filter_started_at(alert_group_internal_api_setup, make_user_auth_he
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
assert response.data["count"] == 4
|
assert len(response.data["results"]) == 4
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|
@ -78,7 +78,7 @@ def test_get_filter_resolved_at_alertgroup_empty_result(alert_group_internal_api
|
||||||
**make_user_auth_headers(user, token),
|
**make_user_auth_headers(user, token),
|
||||||
)
|
)
|
||||||
assert response.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
assert response.data["count"] == 0
|
assert len(response.data["results"]) == 0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|
@ -105,7 +105,7 @@ def test_get_filter_resolved_at(alert_group_internal_api_setup, make_user_auth_h
|
||||||
**make_user_auth_headers(user, token),
|
**make_user_auth_headers(user, token),
|
||||||
)
|
)
|
||||||
assert response.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
assert response.data["count"] == 1
|
assert len(response.data["results"]) == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|
@ -117,7 +117,7 @@ def test_status_new(alert_group_internal_api_setup, make_user_auth_headers):
|
||||||
url = reverse("api-internal:alertgroup-list")
|
url = reverse("api-internal:alertgroup-list")
|
||||||
response = client.get(url + "?status=0", format="json", **make_user_auth_headers(user, token))
|
response = client.get(url + "?status=0", format="json", **make_user_auth_headers(user, token))
|
||||||
assert response.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
assert response.data["count"] == 1
|
assert len(response.data["results"]) == 1
|
||||||
assert response.data["results"][0]["pk"] == new_alert_group.public_primary_key
|
assert response.data["results"][0]["pk"] == new_alert_group.public_primary_key
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -130,7 +130,7 @@ def test_status_ack(alert_group_internal_api_setup, make_user_auth_headers):
|
||||||
url = reverse("api-internal:alertgroup-list")
|
url = reverse("api-internal:alertgroup-list")
|
||||||
response = client.get(url + "?status=1", format="json", **make_user_auth_headers(user, token))
|
response = client.get(url + "?status=1", format="json", **make_user_auth_headers(user, token))
|
||||||
assert response.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
assert response.data["count"] == 1
|
assert len(response.data["results"]) == 1
|
||||||
assert response.data["results"][0]["pk"] == ack_alert_group.public_primary_key
|
assert response.data["results"][0]["pk"] == ack_alert_group.public_primary_key
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -143,7 +143,7 @@ def test_status_resolved(alert_group_internal_api_setup, make_user_auth_headers)
|
||||||
url = reverse("api-internal:alertgroup-list")
|
url = reverse("api-internal:alertgroup-list")
|
||||||
response = client.get(url + "?status=2", format="json", **make_user_auth_headers(user, token))
|
response = client.get(url + "?status=2", format="json", **make_user_auth_headers(user, token))
|
||||||
assert response.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
assert response.data["count"] == 1
|
assert len(response.data["results"]) == 1
|
||||||
assert response.data["results"][0]["pk"] == resolved_alert_group.public_primary_key
|
assert response.data["results"][0]["pk"] == resolved_alert_group.public_primary_key
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -156,7 +156,7 @@ def test_status_silenced(alert_group_internal_api_setup, make_user_auth_headers)
|
||||||
url = reverse("api-internal:alertgroup-list")
|
url = reverse("api-internal:alertgroup-list")
|
||||||
response = client.get(url + "?status=3", format="json", **make_user_auth_headers(user, token))
|
response = client.get(url + "?status=3", format="json", **make_user_auth_headers(user, token))
|
||||||
assert response.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
assert response.data["count"] == 1
|
assert len(response.data["results"]) == 1
|
||||||
assert response.data["results"][0]["pk"] == silenced_alert_group.public_primary_key
|
assert response.data["results"][0]["pk"] == silenced_alert_group.public_primary_key
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -171,7 +171,7 @@ def test_all_statuses(alert_group_internal_api_setup, make_user_auth_headers):
|
||||||
url + "?status=0&status=1&&status=2&status=3", format="json", **make_user_auth_headers(user, token)
|
url + "?status=0&status=1&&status=2&status=3", format="json", **make_user_auth_headers(user, token)
|
||||||
)
|
)
|
||||||
assert response.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
assert response.data["count"] == 4
|
assert len(response.data["results"]) == 4
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|
@ -213,7 +213,7 @@ def test_get_filter_resolved_by(
|
||||||
**make_user_auth_headers(first_user, token),
|
**make_user_auth_headers(first_user, token),
|
||||||
)
|
)
|
||||||
assert first_response.status_code == status.HTTP_200_OK
|
assert first_response.status_code == status.HTTP_200_OK
|
||||||
assert first_response.data["count"] == 1
|
assert len(first_response.data["results"]) == 1
|
||||||
|
|
||||||
second_response = client.get(
|
second_response = client.get(
|
||||||
url + f"?resolved_by={second_user.public_primary_key}",
|
url + f"?resolved_by={second_user.public_primary_key}",
|
||||||
|
|
@ -221,7 +221,7 @@ def test_get_filter_resolved_by(
|
||||||
**make_user_auth_headers(first_user, token),
|
**make_user_auth_headers(first_user, token),
|
||||||
)
|
)
|
||||||
assert second_response.status_code == status.HTTP_200_OK
|
assert second_response.status_code == status.HTTP_200_OK
|
||||||
assert second_response.data["count"] == 0
|
assert len(second_response.data["results"]) == 0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|
@ -269,7 +269,7 @@ def test_get_filter_resolved_by_multiple_values(
|
||||||
**make_user_auth_headers(first_user, token),
|
**make_user_auth_headers(first_user, token),
|
||||||
)
|
)
|
||||||
assert first_response.status_code == status.HTTP_200_OK
|
assert first_response.status_code == status.HTTP_200_OK
|
||||||
assert first_response.data["count"] == 2
|
assert len(first_response.data["results"]) == 2
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|
@ -309,7 +309,7 @@ def test_get_filter_acknowledged_by(
|
||||||
**make_user_auth_headers(first_user, token),
|
**make_user_auth_headers(first_user, token),
|
||||||
)
|
)
|
||||||
assert first_response.status_code == status.HTTP_200_OK
|
assert first_response.status_code == status.HTTP_200_OK
|
||||||
assert first_response.data["count"] == 1
|
assert len(first_response.data["results"]) == 1
|
||||||
|
|
||||||
second_response = client.get(
|
second_response = client.get(
|
||||||
url + f"?acknowledged_by={second_user.public_primary_key}",
|
url + f"?acknowledged_by={second_user.public_primary_key}",
|
||||||
|
|
@ -317,7 +317,7 @@ def test_get_filter_acknowledged_by(
|
||||||
**make_user_auth_headers(first_user, token),
|
**make_user_auth_headers(first_user, token),
|
||||||
)
|
)
|
||||||
assert second_response.status_code == status.HTTP_200_OK
|
assert second_response.status_code == status.HTTP_200_OK
|
||||||
assert second_response.data["count"] == 0
|
assert len(second_response.data["results"]) == 0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|
@ -363,7 +363,7 @@ def test_get_filter_acknowledged_by_multiple_values(
|
||||||
**make_user_auth_headers(first_user, token),
|
**make_user_auth_headers(first_user, token),
|
||||||
)
|
)
|
||||||
assert first_response.status_code == status.HTTP_200_OK
|
assert first_response.status_code == status.HTTP_200_OK
|
||||||
assert first_response.data["count"] == 2
|
assert len(first_response.data["results"]) == 2
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|
@ -402,7 +402,7 @@ def test_get_filter_silenced_by(
|
||||||
**make_user_auth_headers(first_user, token),
|
**make_user_auth_headers(first_user, token),
|
||||||
)
|
)
|
||||||
assert first_response.status_code == status.HTTP_200_OK
|
assert first_response.status_code == status.HTTP_200_OK
|
||||||
assert first_response.data["count"] == 1
|
assert len(first_response.data["results"]) == 1
|
||||||
|
|
||||||
second_response = client.get(
|
second_response = client.get(
|
||||||
url + f"?silenced_by={second_user.public_primary_key}",
|
url + f"?silenced_by={second_user.public_primary_key}",
|
||||||
|
|
@ -410,7 +410,7 @@ def test_get_filter_silenced_by(
|
||||||
**make_user_auth_headers(first_user, token),
|
**make_user_auth_headers(first_user, token),
|
||||||
)
|
)
|
||||||
assert second_response.status_code == status.HTTP_200_OK
|
assert second_response.status_code == status.HTTP_200_OK
|
||||||
assert second_response.data["count"] == 0
|
assert len(second_response.data["results"]) == 0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|
@ -455,7 +455,7 @@ def test_get_filter_silenced_by_multiple_values(
|
||||||
**make_user_auth_headers(first_user, token),
|
**make_user_auth_headers(first_user, token),
|
||||||
)
|
)
|
||||||
assert first_response.status_code == status.HTTP_200_OK
|
assert first_response.status_code == status.HTTP_200_OK
|
||||||
assert first_response.data["count"] == 2
|
assert len(first_response.data["results"]) == 2
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|
@ -494,7 +494,7 @@ def test_get_filter_invitees_are(
|
||||||
**make_user_auth_headers(first_user, token),
|
**make_user_auth_headers(first_user, token),
|
||||||
)
|
)
|
||||||
assert first_response.status_code == status.HTTP_200_OK
|
assert first_response.status_code == status.HTTP_200_OK
|
||||||
assert first_response.data["count"] == 1
|
assert len(first_response.data["results"]) == 1
|
||||||
|
|
||||||
second_response = client.get(
|
second_response = client.get(
|
||||||
url + f"?invitees_are={second_user.public_primary_key}",
|
url + f"?invitees_are={second_user.public_primary_key}",
|
||||||
|
|
@ -502,7 +502,7 @@ def test_get_filter_invitees_are(
|
||||||
**make_user_auth_headers(first_user, token),
|
**make_user_auth_headers(first_user, token),
|
||||||
)
|
)
|
||||||
assert second_response.status_code == status.HTTP_200_OK
|
assert second_response.status_code == status.HTTP_200_OK
|
||||||
assert second_response.data["count"] == 0
|
assert len(second_response.data["results"]) == 0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|
@ -548,7 +548,7 @@ def test_get_filter_invitees_are_multiple_values(
|
||||||
**make_user_auth_headers(first_user, token),
|
**make_user_auth_headers(first_user, token),
|
||||||
)
|
)
|
||||||
assert first_response.status_code == status.HTTP_200_OK
|
assert first_response.status_code == status.HTTP_200_OK
|
||||||
assert first_response.data["count"] == 2
|
assert len(first_response.data["results"]) == 2
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|
@ -593,7 +593,7 @@ def test_get_filter_invitees_are_ag_with_multiple_logs(
|
||||||
**make_user_auth_headers(first_user, token),
|
**make_user_auth_headers(first_user, token),
|
||||||
)
|
)
|
||||||
assert first_response.status_code == status.HTTP_200_OK
|
assert first_response.status_code == status.HTTP_200_OK
|
||||||
assert first_response.data["count"] == 1
|
assert len(first_response.data["results"]) == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|
@ -611,11 +611,11 @@ def test_get_filter_with_resolution_note(
|
||||||
# there are no alert groups with resolution_notes
|
# there are no alert groups with resolution_notes
|
||||||
response = client.get(url + "?with_resolution_note=true", format="json", **make_user_auth_headers(user, token))
|
response = client.get(url + "?with_resolution_note=true", format="json", **make_user_auth_headers(user, token))
|
||||||
assert response.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
assert response.data["count"] == 0
|
assert len(response.data["results"]) == 0
|
||||||
|
|
||||||
response = client.get(url + "?with_resolution_note=false", format="json", **make_user_auth_headers(user, token))
|
response = client.get(url + "?with_resolution_note=false", format="json", **make_user_auth_headers(user, token))
|
||||||
assert response.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
assert response.data["count"] == 4
|
assert len(response.data["results"]) == 4
|
||||||
|
|
||||||
# add resolution_notes to two of four alert groups
|
# add resolution_notes to two of four alert groups
|
||||||
make_resolution_note(res_alert_group)
|
make_resolution_note(res_alert_group)
|
||||||
|
|
@ -623,11 +623,11 @@ def test_get_filter_with_resolution_note(
|
||||||
|
|
||||||
response = client.get(url + "?with_resolution_note=true", format="json", **make_user_auth_headers(user, token))
|
response = client.get(url + "?with_resolution_note=true", format="json", **make_user_auth_headers(user, token))
|
||||||
assert response.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
assert response.data["count"] == 2
|
assert len(response.data["results"]) == 2
|
||||||
|
|
||||||
response = client.get(url + "?with_resolution_note=false", format="json", **make_user_auth_headers(user, token))
|
response = client.get(url + "?with_resolution_note=false", format="json", **make_user_auth_headers(user, token))
|
||||||
assert response.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
assert response.data["count"] == 2
|
assert len(response.data["results"]) == 2
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|
@ -653,7 +653,7 @@ def test_get_filter_with_resolution_note_after_delete_resolution_note(
|
||||||
|
|
||||||
response = client.get(url + "?with_resolution_note=true", format="json", **make_user_auth_headers(user, token))
|
response = client.get(url + "?with_resolution_note=true", format="json", **make_user_auth_headers(user, token))
|
||||||
assert response.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
assert response.data["count"] == 1
|
assert len(response.data["results"]) == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
from datetime import datetime, timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from django import forms
|
from django.db.models import Count, Max, Q
|
||||||
from django.db import models
|
|
||||||
from django.db.models import CharField, Q
|
|
||||||
from django.db.models.constants import LOOKUP_SEP
|
|
||||||
from django.db.models.functions import Cast
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django_filters import rest_framework as filters
|
from django_filters import rest_framework as filters
|
||||||
from django_filters.widgets import RangeWidget
|
from django_filters.widgets import RangeWidget
|
||||||
|
|
@ -15,16 +11,15 @@ from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from apps.alerts.constants import ActionSource
|
from apps.alerts.constants import ActionSource
|
||||||
from apps.alerts.models import AlertGroup, AlertReceiveChannel
|
from apps.alerts.models import Alert, AlertGroup, AlertReceiveChannel
|
||||||
from apps.alerts.tasks import invalidate_web_cache_for_alert_group
|
|
||||||
from apps.api.permissions import MODIFY_ACTIONS, READ_ACTIONS, ActionPermission, AnyRole, IsAdminOrEditor
|
from apps.api.permissions import MODIFY_ACTIONS, READ_ACTIONS, ActionPermission, AnyRole, IsAdminOrEditor
|
||||||
from apps.api.serializers.alert_group import AlertGroupSerializer
|
from apps.api.serializers.alert_group import AlertGroupListSerializer, AlertGroupSerializer
|
||||||
from apps.auth_token.auth import MobileAppAuthTokenAuthentication, PluginAuthentication
|
from apps.auth_token.auth import MobileAppAuthTokenAuthentication, PluginAuthentication
|
||||||
from apps.user_management.models import User
|
from apps.user_management.models import User
|
||||||
from common.api_helpers.exceptions import BadRequest
|
from common.api_helpers.exceptions import BadRequest
|
||||||
from common.api_helpers.filters import DateRangeFilterMixin, ModelFieldFilterMixin
|
from common.api_helpers.filters import DateRangeFilterMixin, ModelFieldFilterMixin
|
||||||
from common.api_helpers.mixins import PreviewTemplateMixin, PublicPrimaryKeyMixin
|
from common.api_helpers.mixins import PreviewTemplateMixin, PublicPrimaryKeyMixin
|
||||||
from common.api_helpers.paginators import FiftyPageSizePaginator
|
from common.api_helpers.paginators import TwentyFiveCursorPaginator
|
||||||
|
|
||||||
|
|
||||||
def get_integration_queryset(request):
|
def get_integration_queryset(request):
|
||||||
|
|
@ -148,34 +143,6 @@ class AlertGroupFilter(DateRangeFilterMixin, ModelFieldFilterMixin, filters.Filt
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class CustomSearchFilter(SearchFilter):
|
|
||||||
def must_call_distinct(self, queryset, search_fields):
|
|
||||||
"""
|
|
||||||
Return True if 'distinct()' should be used to query the given lookups.
|
|
||||||
"""
|
|
||||||
for search_field in search_fields:
|
|
||||||
opts = queryset.model._meta
|
|
||||||
if search_field[0] in self.lookup_prefixes:
|
|
||||||
search_field = search_field[1:]
|
|
||||||
|
|
||||||
# From https://github.com/encode/django-rest-framework/pull/6240/files#diff-01f357e474dd8fd702e4951b9227bffcR88
|
|
||||||
# Annotated fields do not need to be distinct
|
|
||||||
if isinstance(queryset, models.QuerySet) and search_field in queryset.query.annotations:
|
|
||||||
continue
|
|
||||||
|
|
||||||
parts = search_field.split(LOOKUP_SEP)
|
|
||||||
for part in parts:
|
|
||||||
field = opts.get_field(part)
|
|
||||||
if hasattr(field, "get_path_info"):
|
|
||||||
# This field is a relation, update opts to follow the relation
|
|
||||||
path_info = field.get_path_info()
|
|
||||||
opts = path_info[-1].to_opts
|
|
||||||
if any(path.m2m for path in path_info):
|
|
||||||
# This field is a m2m relation so we know we need to call distinct
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class AlertGroupView(
|
class AlertGroupView(
|
||||||
PreviewTemplateMixin,
|
PreviewTemplateMixin,
|
||||||
PublicPrimaryKeyMixin,
|
PublicPrimaryKeyMixin,
|
||||||
|
|
@ -216,90 +183,85 @@ class AlertGroupView(
|
||||||
|
|
||||||
serializer_class = AlertGroupSerializer
|
serializer_class = AlertGroupSerializer
|
||||||
|
|
||||||
pagination_class = FiftyPageSizePaginator
|
pagination_class = TwentyFiveCursorPaginator
|
||||||
|
|
||||||
filter_backends = [CustomSearchFilter, filters.DjangoFilterBackend]
|
filter_backends = [SearchFilter, filters.DjangoFilterBackend]
|
||||||
search_fields = ["cached_render_for_web_str"]
|
# todo: add ability to search by templated title
|
||||||
|
search_fields = ["public_primary_key", "inside_organization_number"]
|
||||||
|
|
||||||
filterset_class = AlertGroupFilter
|
filterset_class = AlertGroupFilter
|
||||||
|
|
||||||
def list(self, request, *args, **kwargs):
|
def get_serializer_class(self):
|
||||||
"""
|
if self.action == "list":
|
||||||
It's compute-heavy so we rely on cache here.
|
return AlertGroupListSerializer
|
||||||
Attention: Make sure to invalidate cache if you update the format!
|
|
||||||
"""
|
|
||||||
queryset = self.filter_queryset(self.get_queryset(eager=False, readonly=True))
|
|
||||||
|
|
||||||
page = self.paginate_queryset(queryset)
|
return super().get_serializer_class()
|
||||||
skip_slow_rendering = request.query_params.get("skip_slow_rendering") == "true"
|
|
||||||
data = []
|
|
||||||
|
|
||||||
for alert_group in page:
|
def get_queryset(self):
|
||||||
if alert_group.cached_render_for_web == {}:
|
# make a separate query to fetch all the integrations for current organization and team (it's faster)
|
||||||
# We cannot give empty data to web. So caching synchronously here.
|
alert_receive_channel_pks = AlertReceiveChannel.objects_with_deleted.filter(
|
||||||
if skip_slow_rendering:
|
organization=self.request.auth.organization, team=self.request.user.current_team
|
||||||
# We just return dummy data.
|
).values_list("pk", flat=True)
|
||||||
# Cache is not launched because after skip_slow_rendering request should come usual one
|
alert_receive_channel_pks = list(alert_receive_channel_pks)
|
||||||
# which will start caching
|
|
||||||
data.append({"pk": alert_group.pk, "short": True})
|
|
||||||
else:
|
|
||||||
# Synchronously cache and return. It could be slow.
|
|
||||||
alert_group.cache_for_web(alert_group.channel.organization)
|
|
||||||
data.append(alert_group.cached_render_for_web)
|
|
||||||
else:
|
|
||||||
data.append(alert_group.cached_render_for_web)
|
|
||||||
if not skip_slow_rendering:
|
|
||||||
# Cache is not launched because after skip_slow_rendering request should come usual one
|
|
||||||
# which will start caching
|
|
||||||
alert_group.schedule_cache_for_web()
|
|
||||||
|
|
||||||
return self.get_paginated_response(data)
|
# no select_related or prefetch_related is used at this point, it will be done on paginate_queryset.
|
||||||
|
queryset = AlertGroup.unarchived_objects.filter(channel_id__in=alert_receive_channel_pks)
|
||||||
|
|
||||||
def get_queryset(self, eager=True, readonly=False, order=True):
|
|
||||||
if readonly:
|
|
||||||
queryset = AlertGroup.unarchived_objects.using_readonly_db
|
|
||||||
else:
|
|
||||||
queryset = AlertGroup.unarchived_objects
|
|
||||||
|
|
||||||
queryset = queryset.filter(
|
|
||||||
channel__organization=self.request.auth.organization,
|
|
||||||
channel__team=self.request.user.current_team,
|
|
||||||
)
|
|
||||||
|
|
||||||
if order:
|
|
||||||
queryset = queryset.order_by("-started_at")
|
|
||||||
|
|
||||||
queryset = queryset.annotate(cached_render_for_web_str=Cast("cached_render_for_web", output_field=CharField()))
|
|
||||||
|
|
||||||
if eager:
|
|
||||||
queryset = self.serializer_class.setup_eager_loading(queryset)
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def get_alert_groups_and_days_for_previous_same_period(self):
|
def paginate_queryset(self, queryset):
|
||||||
prev_alert_groups = AlertGroup.unarchived_objects.none()
|
"""
|
||||||
delta_days = None
|
All SQL joins (select_related and prefetch_related) will be performed AFTER pagination, so it only joins tables
|
||||||
|
for 25 alert groups, not the whole table.
|
||||||
|
"""
|
||||||
|
alert_groups = super().paginate_queryset(queryset)
|
||||||
|
alert_groups = self.enrich(alert_groups)
|
||||||
|
return alert_groups
|
||||||
|
|
||||||
started_at = self.request.query_params.get("started_at", None)
|
def get_object(self):
|
||||||
if started_at is not None:
|
obj = super().get_object()
|
||||||
started_at_gte, started_at_lte = AlertGroupFilter.parse_custom_datetime_range(started_at)
|
obj = self.enrich([obj])[0]
|
||||||
delta_days = None
|
return obj
|
||||||
if started_at_lte is not None:
|
|
||||||
started_at_lte = forms.DateTimeField().to_python(started_at_lte)
|
|
||||||
else:
|
|
||||||
started_at_lte = datetime.now()
|
|
||||||
|
|
||||||
if started_at_gte is not None:
|
def enrich(self, alert_groups):
|
||||||
started_at_gte = forms.DateTimeField().to_python(value=started_at_gte)
|
"""
|
||||||
delta = started_at_lte.replace(tzinfo=None) - started_at_gte.replace(tzinfo=None)
|
This method performs select_related and prefetch_related (using setup_eager_loading) as well as in-memory joins
|
||||||
prev_alert_groups = self.get_queryset().filter(
|
to add additional info like alert_count and last_alert for every alert group efficiently.
|
||||||
started_at__range=[started_at_gte - delta, started_at_gte]
|
We need the last_alert because it's used by AlertGroupWebRenderer.
|
||||||
)
|
"""
|
||||||
delta_days = delta.days
|
|
||||||
return prev_alert_groups, delta_days
|
# enrich alert groups with select_related and prefetch_related
|
||||||
|
alert_group_pks = [alert_group.pk for alert_group in alert_groups]
|
||||||
|
queryset = AlertGroup.all_objects.filter(pk__in=alert_group_pks).order_by("-pk")
|
||||||
|
queryset = self.get_serializer_class().setup_eager_loading(queryset)
|
||||||
|
alert_groups = list(queryset)
|
||||||
|
|
||||||
|
# get info on alerts count and last alert ID for every alert group
|
||||||
|
alerts_info = (
|
||||||
|
Alert.objects.values("group_id")
|
||||||
|
.filter(group_id__in=alert_group_pks)
|
||||||
|
.annotate(alerts_count=Count("group_id"), last_alert_id=Max("id"))
|
||||||
|
)
|
||||||
|
alerts_info_map = {info["group_id"]: info for info in alerts_info}
|
||||||
|
|
||||||
|
# fetch last alerts for every alert group
|
||||||
|
last_alert_ids = [info["last_alert_id"] for info in alerts_info_map.values()]
|
||||||
|
last_alerts = Alert.objects.filter(pk__in=last_alert_ids)
|
||||||
|
for alert in last_alerts:
|
||||||
|
# link group back to alert
|
||||||
|
alert.group = [alert_group for alert_group in alert_groups if alert_group.pk == alert.group_id][0]
|
||||||
|
alerts_info_map[alert.group_id].update({"last_alert": alert})
|
||||||
|
|
||||||
|
# add additional "alerts_count" and "last_alert" fields to every alert group
|
||||||
|
for alert_group in alert_groups:
|
||||||
|
alert_group.last_alert = alerts_info_map[alert_group.pk]["last_alert"]
|
||||||
|
alert_group.alerts_count = alerts_info_map[alert_group.pk]["alerts_count"]
|
||||||
|
|
||||||
|
return alert_groups
|
||||||
|
|
||||||
@action(detail=False)
|
@action(detail=False)
|
||||||
def stats(self, *args, **kwargs):
|
def stats(self, *args, **kwargs):
|
||||||
alert_groups = self.filter_queryset(self.get_queryset(eager=False))
|
alert_groups = self.filter_queryset(self.get_queryset())
|
||||||
# Only count field is used, other fields left just in case for the backward compatibility
|
# Only count field is used, other fields left just in case for the backward compatibility
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
|
|
@ -324,7 +286,6 @@ class AlertGroupView(
|
||||||
if alert_group.root_alert_group is not None:
|
if alert_group.root_alert_group is not None:
|
||||||
raise BadRequest(detail="Can't acknowledge an attached alert group")
|
raise BadRequest(detail="Can't acknowledge an attached alert group")
|
||||||
alert_group.acknowledge_by_user(self.request.user, action_source=ActionSource.WEB)
|
alert_group.acknowledge_by_user(self.request.user, action_source=ActionSource.WEB)
|
||||||
invalidate_web_cache_for_alert_group(alert_group_pk=alert_group.pk)
|
|
||||||
|
|
||||||
return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data)
|
return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data)
|
||||||
|
|
||||||
|
|
@ -344,7 +305,6 @@ class AlertGroupView(
|
||||||
raise BadRequest(detail="Can't unacknowledge a resolved alert group")
|
raise BadRequest(detail="Can't unacknowledge a resolved alert group")
|
||||||
|
|
||||||
alert_group.un_acknowledge_by_user(self.request.user, action_source=ActionSource.WEB)
|
alert_group.un_acknowledge_by_user(self.request.user, action_source=ActionSource.WEB)
|
||||||
invalidate_web_cache_for_alert_group(alert_group_pk=alert_group.pk)
|
|
||||||
|
|
||||||
return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data)
|
return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data)
|
||||||
|
|
||||||
|
|
@ -365,7 +325,6 @@ class AlertGroupView(
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
alert_group.resolve_by_user(self.request.user, action_source=ActionSource.WEB)
|
alert_group.resolve_by_user(self.request.user, action_source=ActionSource.WEB)
|
||||||
invalidate_web_cache_for_alert_group(alert_group_pk=alert_group.pk)
|
|
||||||
return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data)
|
return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data)
|
||||||
|
|
||||||
@action(methods=["post"], detail=True)
|
@action(methods=["post"], detail=True)
|
||||||
|
|
@ -381,7 +340,6 @@ class AlertGroupView(
|
||||||
raise BadRequest(detail="The alert group is not resolved")
|
raise BadRequest(detail="The alert group is not resolved")
|
||||||
|
|
||||||
alert_group.un_resolve_by_user(self.request.user, action_source=ActionSource.WEB)
|
alert_group.un_resolve_by_user(self.request.user, action_source=ActionSource.WEB)
|
||||||
invalidate_web_cache_for_alert_group(alert_group_pk=alert_group.pk)
|
|
||||||
return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data)
|
return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data)
|
||||||
|
|
||||||
@action(methods=["post"], detail=True)
|
@action(methods=["post"], detail=True)
|
||||||
|
|
@ -404,8 +362,6 @@ class AlertGroupView(
|
||||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
alert_group.attach_by_user(self.request.user, root_alert_group, action_source=ActionSource.WEB)
|
alert_group.attach_by_user(self.request.user, root_alert_group, action_source=ActionSource.WEB)
|
||||||
invalidate_web_cache_for_alert_group(alert_group_pk=alert_group.pk)
|
|
||||||
invalidate_web_cache_for_alert_group(alert_group_pk=root_alert_group.pk)
|
|
||||||
return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data)
|
return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data)
|
||||||
|
|
||||||
@action(methods=["post"], detail=True)
|
@action(methods=["post"], detail=True)
|
||||||
|
|
@ -415,10 +371,8 @@ class AlertGroupView(
|
||||||
raise BadRequest(detail="Can't unattach maintenance alert group")
|
raise BadRequest(detail="Can't unattach maintenance alert group")
|
||||||
if alert_group.is_root_alert_group:
|
if alert_group.is_root_alert_group:
|
||||||
raise BadRequest(detail="Can't unattach an alert group because it is not attached")
|
raise BadRequest(detail="Can't unattach an alert group because it is not attached")
|
||||||
root_alert_group_pk = alert_group.root_alert_group_id
|
|
||||||
alert_group.un_attach_by_user(self.request.user, action_source=ActionSource.WEB)
|
alert_group.un_attach_by_user(self.request.user, action_source=ActionSource.WEB)
|
||||||
invalidate_web_cache_for_alert_group(alert_group_pk=alert_group.pk)
|
|
||||||
invalidate_web_cache_for_alert_group(alert_group_pk=root_alert_group_pk)
|
|
||||||
return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data)
|
return Response(AlertGroupSerializer(alert_group, context={"request": self.request}).data)
|
||||||
|
|
||||||
@action(methods=["post"], detail=True)
|
@action(methods=["post"], detail=True)
|
||||||
|
|
@ -433,7 +387,6 @@ class AlertGroupView(
|
||||||
raise BadRequest(detail="Can't silence an attached alert group")
|
raise BadRequest(detail="Can't silence an attached alert group")
|
||||||
|
|
||||||
alert_group.silence_by_user(request.user, silence_delay=delay, action_source=ActionSource.WEB)
|
alert_group.silence_by_user(request.user, silence_delay=delay, action_source=ActionSource.WEB)
|
||||||
invalidate_web_cache_for_alert_group(alert_group_pk=alert_group.pk)
|
|
||||||
return Response(AlertGroupSerializer(alert_group, context={"request": request}).data)
|
return Response(AlertGroupSerializer(alert_group, context={"request": request}).data)
|
||||||
|
|
||||||
@action(methods=["get"], detail=False)
|
@action(methods=["get"], detail=False)
|
||||||
|
|
@ -548,9 +501,9 @@ class AlertGroupView(
|
||||||
raise BadRequest(detail="Please specify a delay for silence")
|
raise BadRequest(detail="Please specify a delay for silence")
|
||||||
kwargs["silence_delay"] = delay
|
kwargs["silence_delay"] = delay
|
||||||
|
|
||||||
alert_groups = self.get_queryset(eager=False).filter(public_primary_key__in=alert_group_public_pks)
|
alert_groups = AlertGroup.unarchived_objects.filter(
|
||||||
alert_group_pks = list(alert_groups.values_list("id", flat=True))
|
channel__organization=self.request.auth.organization, public_primary_key__in=alert_group_public_pks
|
||||||
invalidate_web_cache_for_alert_group(alert_group_pks=alert_group_pks)
|
)
|
||||||
|
|
||||||
kwargs["user"] = self.request.user
|
kwargs["user"] = self.request.user
|
||||||
kwargs["alert_groups"] = alert_groups
|
kwargs["alert_groups"] = alert_groups
|
||||||
|
|
|
||||||
|
|
@ -43,10 +43,7 @@ class RouteRegexDebuggerView(APIView):
|
||||||
if len(incidents_matching_regex) < MAX_INCIDENTS_TO_SHOW:
|
if len(incidents_matching_regex) < MAX_INCIDENTS_TO_SHOW:
|
||||||
first_alert = ag.alerts.all()[0]
|
first_alert = ag.alerts.all()[0]
|
||||||
if re.search(regex, json.dumps(first_alert.raw_request_data)):
|
if re.search(regex, json.dumps(first_alert.raw_request_data)):
|
||||||
if ag.cached_render_for_web:
|
title = AlertWebRenderer(first_alert).render()["title"]
|
||||||
title = ag.cached_render_for_web["render_for_web"]["title"]
|
|
||||||
else:
|
|
||||||
title = AlertWebRenderer(first_alert).render()["title"]
|
|
||||||
incidents_matching_regex.append(
|
incidents_matching_regex.append(
|
||||||
{
|
{
|
||||||
"title": title,
|
"title": title,
|
||||||
|
|
|
||||||
|
|
@ -315,7 +315,6 @@ class UserNotificationPolicyLogRecord(models.Model):
|
||||||
|
|
||||||
@receiver(post_save, sender=UserNotificationPolicyLogRecord)
|
@receiver(post_save, sender=UserNotificationPolicyLogRecord)
|
||||||
def listen_for_usernotificationpolicylogrecord_model_save(sender, instance, created, *args, **kwargs):
|
def listen_for_usernotificationpolicylogrecord_model_save(sender, instance, created, *args, **kwargs):
|
||||||
instance.alert_group.drop_cached_after_resolve_report_json()
|
|
||||||
alert_group_pk = instance.alert_group.pk
|
alert_group_pk = instance.alert_group.pk
|
||||||
if instance.type != UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FINISHED:
|
if instance.type != UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FINISHED:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ def construct_expected_response_from_incidents(incidents):
|
||||||
"id": incident.public_primary_key,
|
"id": incident.public_primary_key,
|
||||||
"integration_id": incident.channel.public_primary_key,
|
"integration_id": incident.channel.public_primary_key,
|
||||||
"route_id": incident.channel_filter.public_primary_key,
|
"route_id": incident.channel_filter.public_primary_key,
|
||||||
"alerts_count": incident.alerts_count,
|
"alerts_count": incident.alerts.count(),
|
||||||
"state": incident.state,
|
"state": incident.state,
|
||||||
"created_at": created_at,
|
"created_at": created_at,
|
||||||
"resolved_at": resolved_at,
|
"resolved_at": resolved_at,
|
||||||
|
|
|
||||||
|
|
@ -247,10 +247,6 @@ class UpdateAppearanceStep(scenario_step.ScenarioStep):
|
||||||
if new_value is None and old_value is not None:
|
if new_value is None and old_value is not None:
|
||||||
setattr(alert_receive_channel, attr_name, None)
|
setattr(alert_receive_channel, attr_name, None)
|
||||||
alert_receive_channel.save()
|
alert_receive_channel.save()
|
||||||
# Drop caches for current alert group
|
|
||||||
if notification_channel == "web":
|
|
||||||
setattr(alert_group, f"cached_render_for_web_{templatizable_attr}", None)
|
|
||||||
alert_group.save()
|
|
||||||
elif new_value is not None:
|
elif new_value is not None:
|
||||||
default_values = getattr(
|
default_values = getattr(
|
||||||
AlertReceiveChannel,
|
AlertReceiveChannel,
|
||||||
|
|
@ -265,18 +261,10 @@ class UpdateAppearanceStep(scenario_step.ScenarioStep):
|
||||||
jinja_template_env.from_string(new_value)
|
jinja_template_env.from_string(new_value)
|
||||||
setattr(alert_receive_channel, attr_name, new_value)
|
setattr(alert_receive_channel, attr_name, new_value)
|
||||||
alert_receive_channel.save()
|
alert_receive_channel.save()
|
||||||
# Drop caches for current alert group
|
|
||||||
if notification_channel == "web":
|
|
||||||
setattr(alert_group, f"cached_render_for_web_{templatizable_attr}", None)
|
|
||||||
alert_group.save()
|
|
||||||
elif default_value is not None and new_value.strip() == default_value.strip():
|
elif default_value is not None and new_value.strip() == default_value.strip():
|
||||||
new_value = None
|
new_value = None
|
||||||
setattr(alert_receive_channel, attr_name, new_value)
|
setattr(alert_receive_channel, attr_name, new_value)
|
||||||
alert_receive_channel.save()
|
alert_receive_channel.save()
|
||||||
# Drop caches for current alert group
|
|
||||||
if notification_channel == "web":
|
|
||||||
setattr(alert_group, f"cached_render_for_web_{templatizable_attr}", None)
|
|
||||||
alert_group.save()
|
|
||||||
except TemplateSyntaxError:
|
except TemplateSyntaxError:
|
||||||
return Response(
|
return Response(
|
||||||
{"response_action": "errors", "errors": {attr_name: "Template has incorrect format"}},
|
{"response_action": "errors", "errors": {attr_name: "Template has incorrect format"}},
|
||||||
|
|
|
||||||
|
|
@ -674,7 +674,6 @@ class AddRemoveThreadMessageStep(UpdateResolutionNoteStep, scenario_step.Scenari
|
||||||
add_to_resolution_note = True if value["msg_value"].startswith("add") else False
|
add_to_resolution_note = True if value["msg_value"].startswith("add") else False
|
||||||
slack_thread_message = None
|
slack_thread_message = None
|
||||||
resolution_note = None
|
resolution_note = None
|
||||||
drop_ag_cache = False
|
|
||||||
|
|
||||||
alert_group = AlertGroup.all_objects.get(pk=alert_group_pk)
|
alert_group = AlertGroup.all_objects.get(pk=alert_group_pk)
|
||||||
|
|
||||||
|
|
@ -695,7 +694,6 @@ class AddRemoveThreadMessageStep(UpdateResolutionNoteStep, scenario_step.Scenari
|
||||||
else:
|
else:
|
||||||
resolution_note.recreate()
|
resolution_note.recreate()
|
||||||
self.add_resolution_note_reaction(slack_thread_message)
|
self.add_resolution_note_reaction(slack_thread_message)
|
||||||
drop_ag_cache = True
|
|
||||||
elif not add_to_resolution_note:
|
elif not add_to_resolution_note:
|
||||||
# Check if resolution_note can be removed
|
# Check if resolution_note can be removed
|
||||||
if (
|
if (
|
||||||
|
|
@ -720,13 +718,9 @@ class AddRemoveThreadMessageStep(UpdateResolutionNoteStep, scenario_step.Scenari
|
||||||
slack_thread_message.added_to_resolution_note = False
|
slack_thread_message.added_to_resolution_note = False
|
||||||
slack_thread_message.save(update_fields=["added_to_resolution_note"])
|
slack_thread_message.save(update_fields=["added_to_resolution_note"])
|
||||||
self.remove_resolution_note_reaction(slack_thread_message)
|
self.remove_resolution_note_reaction(slack_thread_message)
|
||||||
drop_ag_cache = True
|
|
||||||
self.update_alert_group_resolution_note_button(
|
self.update_alert_group_resolution_note_button(
|
||||||
alert_group,
|
alert_group,
|
||||||
)
|
)
|
||||||
if drop_ag_cache:
|
|
||||||
alert_group.drop_cached_after_resolve_report_json()
|
|
||||||
alert_group.schedule_cache_for_web()
|
|
||||||
resolution_note_data = json.loads(payload["actions"][0]["value"])
|
resolution_note_data = json.loads(payload["actions"][0]["value"])
|
||||||
resolution_note_data["resolution_note_window_action"] = "edit_update"
|
resolution_note_data["resolution_note_window_action"] = "edit_update"
|
||||||
ResolutionNoteModalStep(slack_team_identity, self.organization, self.user).process_scenario(
|
ResolutionNoteModalStep(slack_team_identity, self.organization, self.user).process_scenario(
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ from django.db.models.signals import post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from emoji import demojize
|
from emoji import demojize
|
||||||
|
|
||||||
from apps.alerts.tasks import invalidate_web_cache_for_alert_group
|
|
||||||
from apps.schedules.tasks import drop_cached_ical_for_custom_events_for_organization
|
from apps.schedules.tasks import drop_cached_ical_for_custom_events_for_organization
|
||||||
from common.constants.role import Role
|
from common.constants.role import Role
|
||||||
from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length
|
from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length
|
||||||
|
|
@ -264,5 +263,3 @@ def listen_for_user_model_save(sender, instance, created, *args, **kwargs):
|
||||||
drop_cached_ical_for_custom_events_for_organization.apply_async(
|
drop_cached_ical_for_custom_events_for_organization.apply_async(
|
||||||
(instance.organization_id,),
|
(instance.organization_id,),
|
||||||
)
|
)
|
||||||
logger.info(f"Drop AG cache. Reason: save user {instance.pk}")
|
|
||||||
invalidate_web_cache_for_alert_group.apply_async(kwargs={"org_pk": instance.organization_id})
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from rest_framework.pagination import PageNumberPagination
|
from rest_framework.pagination import CursorPagination, PageNumberPagination
|
||||||
|
|
||||||
|
|
||||||
class HundredPageSizePaginator(PageNumberPagination):
|
class HundredPageSizePaginator(PageNumberPagination):
|
||||||
|
|
@ -11,3 +11,10 @@ class FiftyPageSizePaginator(PageNumberPagination):
|
||||||
|
|
||||||
class TwentyFivePageSizePaginator(PageNumberPagination):
|
class TwentyFivePageSizePaginator(PageNumberPagination):
|
||||||
page_size = 25
|
page_size = 25
|
||||||
|
|
||||||
|
|
||||||
|
class TwentyFiveCursorPaginator(CursorPagination):
|
||||||
|
page_size = 25
|
||||||
|
max_page_size = 100
|
||||||
|
page_size_query_param = "perpage"
|
||||||
|
ordering = "-pk"
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
import random
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
|
|
||||||
class UseRandomReadonlyDbManagerMixin:
|
|
||||||
"""
|
|
||||||
Use this Mixin in ModelManagers, when you want to use the random readonly replica
|
|
||||||
"""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def using_readonly_db(self):
|
|
||||||
"""Select one of the readonly databases this QuerySet should execute against."""
|
|
||||||
if hasattr(settings, "READONLY_DATABASES") and len(settings.READONLY_DATABASES) > 0:
|
|
||||||
using_db = random.choice(list(settings.READONLY_DATABASES.keys()))
|
|
||||||
return self.using(using_db)
|
|
||||||
else:
|
|
||||||
# Use "default" database
|
|
||||||
# Django uses the database with the alias of default when no other database has been selected.
|
|
||||||
# https://docs.djangoproject.com/en/3.2/topics/db/multi-db/#defining-your-databases
|
|
||||||
return self.using("default")
|
|
||||||
|
|
@ -32,10 +32,6 @@ DATABASES = {
|
||||||
|
|
||||||
TESTING = "pytest" in sys.modules or "unittest" in sys.modules
|
TESTING = "pytest" in sys.modules or "unittest" in sys.modules
|
||||||
|
|
||||||
READONLY_DATABASES = {}
|
|
||||||
|
|
||||||
# Dictionaries concatenation, introduced in python3.9
|
|
||||||
DATABASES = DATABASES | READONLY_DATABASES
|
|
||||||
|
|
||||||
CACHES = {
|
CACHES = {
|
||||||
"default": {
|
"default": {
|
||||||
|
|
|
||||||
|
|
@ -84,12 +84,11 @@ CELERY_TASK_ROUTES = {
|
||||||
"apps.alerts.tasks.create_contact_points_for_datasource.create_contact_points_for_datasource": {"queue": "default"},
|
"apps.alerts.tasks.create_contact_points_for_datasource.create_contact_points_for_datasource": {"queue": "default"},
|
||||||
"apps.alerts.tasks.sync_grafana_alerting_contact_points.sync_grafana_alerting_contact_points": {"queue": "default"},
|
"apps.alerts.tasks.sync_grafana_alerting_contact_points.sync_grafana_alerting_contact_points": {"queue": "default"},
|
||||||
"apps.alerts.tasks.delete_alert_group.delete_alert_group": {"queue": "default"},
|
"apps.alerts.tasks.delete_alert_group.delete_alert_group": {"queue": "default"},
|
||||||
"apps.alerts.tasks.invalidate_web_cache_for_alert_group.invalidate_web_cache_for_alert_group": {"queue": "default"},
|
"apps.alerts.tasks.invalidate_web_cache_for_alert_group.invalidate_web_cache_for_alert_group": {
|
||||||
|
"queue": "default"
|
||||||
|
}, # todo: remove
|
||||||
"apps.alerts.tasks.send_alert_group_signal.send_alert_group_signal": {"queue": "default"},
|
"apps.alerts.tasks.send_alert_group_signal.send_alert_group_signal": {"queue": "default"},
|
||||||
"apps.alerts.tasks.wipe.wipe": {"queue": "default"},
|
"apps.alerts.tasks.wipe.wipe": {"queue": "default"},
|
||||||
# TODO: remove cache_alert_group_for_web and schedule_cache_for_alert_group once existing task will be processed
|
|
||||||
"apps.api.tasks.cache_alert_group_for_web": {"queue": "default"},
|
|
||||||
"apps.api.tasks.schedule_cache_for_alert_group": {"queue": "default"},
|
|
||||||
"apps.heartbeat.tasks.heartbeat_checkup": {"queue": "default"},
|
"apps.heartbeat.tasks.heartbeat_checkup": {"queue": "default"},
|
||||||
"apps.heartbeat.tasks.integration_heartbeat_checkup": {"queue": "default"},
|
"apps.heartbeat.tasks.integration_heartbeat_checkup": {"queue": "default"},
|
||||||
"apps.heartbeat.tasks.process_heartbeat_task": {"queue": "default"},
|
"apps.heartbeat.tasks.process_heartbeat_task": {"queue": "default"},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
.root {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { SelectableValue } from '@grafana/data';
|
||||||
|
import { Button, HorizontalGroup, Icon, Select } from '@grafana/ui';
|
||||||
|
import cn from 'classnames/bind';
|
||||||
|
|
||||||
|
import Text from 'components/Text/Text';
|
||||||
|
|
||||||
|
import styles from './CursorPagination.module.css';
|
||||||
|
|
||||||
|
interface CursorPaginationProps {
|
||||||
|
current: string;
|
||||||
|
onChange: (cursor: string, direction: 'prev' | 'next') => void;
|
||||||
|
itemsPerPageOptions: Array<SelectableValue<number>>;
|
||||||
|
itemsPerPage: number;
|
||||||
|
onChangeItemsPerPage: (value: number) => void;
|
||||||
|
prev: string;
|
||||||
|
next: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cx = cn.bind(styles);
|
||||||
|
|
||||||
|
const CursorPagination: FC<CursorPaginationProps> = (props) => {
|
||||||
|
const { current, onChange, prev, next, itemsPerPage, itemsPerPageOptions, onChangeItemsPerPage } = props;
|
||||||
|
|
||||||
|
const [disabled, setDisabled] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDisabled(false);
|
||||||
|
}, [prev, next]);
|
||||||
|
|
||||||
|
const onChangeItemsPerPageCallback = useCallback((option) => {
|
||||||
|
setDisabled(true);
|
||||||
|
onChangeItemsPerPage(option.value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HorizontalGroup spacing="md" justify="flex-end">
|
||||||
|
<HorizontalGroup>
|
||||||
|
<Text type="secondary">Items per list</Text>
|
||||||
|
<Select
|
||||||
|
isSearchable={false}
|
||||||
|
options={itemsPerPageOptions}
|
||||||
|
value={itemsPerPage}
|
||||||
|
onChange={onChangeItemsPerPageCallback}
|
||||||
|
/>
|
||||||
|
</HorizontalGroup>
|
||||||
|
<HorizontalGroup>
|
||||||
|
<Button
|
||||||
|
aria-label="previous"
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setDisabled(true);
|
||||||
|
onChange(prev, 'prev');
|
||||||
|
}}
|
||||||
|
disabled={disabled || !prev}
|
||||||
|
>
|
||||||
|
<Icon name="angle-left" />
|
||||||
|
</Button>
|
||||||
|
<Text type="secondary">{current}</Text>
|
||||||
|
<Button
|
||||||
|
aria-label="previous"
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setDisabled(true);
|
||||||
|
onChange(next, 'next');
|
||||||
|
}}
|
||||||
|
disabled={disabled || !next}
|
||||||
|
>
|
||||||
|
<Icon name="angle-right" />
|
||||||
|
</Button>
|
||||||
|
</HorizontalGroup>
|
||||||
|
</HorizontalGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CursorPagination;
|
||||||
|
|
@ -37,7 +37,7 @@ const cx = cn.bind(styles);
|
||||||
|
|
||||||
interface IncidentsFiltersProps extends WithStoreProps {
|
interface IncidentsFiltersProps extends WithStoreProps {
|
||||||
value: IncidentsFiltersType;
|
value: IncidentsFiltersType;
|
||||||
onChange: (filters: { [key: string]: any }) => void;
|
onChange: (filters: { [key: string]: any }, isOnMount: boolean) => void;
|
||||||
query: { [key: string]: any };
|
query: { [key: string]: any };
|
||||||
}
|
}
|
||||||
interface IncidentsFiltersState {
|
interface IncidentsFiltersState {
|
||||||
|
|
@ -79,7 +79,9 @@ class IncidentsFilters extends Component<IncidentsFiltersProps, IncidentsFilters
|
||||||
({ filters, values } = parseFilters(newQuery, filterOptions));
|
({ filters, values } = parseFilters(newQuery, filterOptions));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ filterOptions, filters, values }, this.onChange);
|
this.setState({ filterOptions, filters, values }, () => {
|
||||||
|
this.onChange(true);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
@ -421,11 +423,11 @@ class IncidentsFilters extends Component<IncidentsFiltersProps, IncidentsFilters
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
onChange = () => {
|
onChange = (isOnMount = false) => {
|
||||||
const { onChange } = this.props;
|
const { onChange } = this.props;
|
||||||
const { values } = this.state;
|
const { values } = this.state;
|
||||||
|
|
||||||
onChange(values);
|
onChange(values, isOnMount);
|
||||||
};
|
};
|
||||||
|
|
||||||
debouncedOnChange = debounce(this.onChange, 500);
|
debouncedOnChange = debounce(this.onChange, 500);
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,10 @@ export class AlertGroupStore extends BaseStore {
|
||||||
initialQuery = qs.parse(window.location.search);
|
initialQuery = qs.parse(window.location.search);
|
||||||
|
|
||||||
@observable
|
@observable
|
||||||
incidentsPage: any = this.initialQuery.p ? Number(this.initialQuery.p) : 1;
|
incidentsCursor?: string;
|
||||||
|
|
||||||
|
@observable
|
||||||
|
incidentsItemsPerPage?: number;
|
||||||
|
|
||||||
@observable
|
@observable
|
||||||
alertsSearchResult: any = {};
|
alertsSearchResult: any = {};
|
||||||
|
|
@ -215,54 +218,63 @@ export class AlertGroupStore extends BaseStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async updateIncidentFilters(params: any, resetPage = true) {
|
async updateIncidentFilters(params: any, keepCursor = false) {
|
||||||
if (resetPage) {
|
if (!keepCursor) {
|
||||||
this.incidentsPage = 1;
|
this.incidentsCursor = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.incidentFilters = params;
|
this.incidentFilters = params;
|
||||||
|
|
||||||
this.updateIncidents();
|
this.updateIncidents();
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async setIncidentsPage(page: number) {
|
async setIncidentsCursor(cursor: string) {
|
||||||
this.incidentsPage = page;
|
this.incidentsCursor = cursor;
|
||||||
|
|
||||||
this.updateAlertGroups();
|
this.updateAlertGroups();
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async updateAlertGroups(skip_slow_rendering = true) {
|
async setIncidentsItemsPerPage(value: number) {
|
||||||
this.alertGroupsLoading = skip_slow_rendering;
|
this.incidentsCursor = undefined;
|
||||||
|
this.incidentsItemsPerPage = value;
|
||||||
|
|
||||||
const result = await makeRequest(`${this.path}`, {
|
this.updateAlertGroups();
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async updateAlertGroups() {
|
||||||
|
this.alertGroupsLoading = true;
|
||||||
|
|
||||||
|
const {
|
||||||
|
results,
|
||||||
|
next: nextRaw,
|
||||||
|
previous: previousRaw,
|
||||||
|
} = await makeRequest(`${this.path}`, {
|
||||||
params: {
|
params: {
|
||||||
...this.incidentFilters,
|
...this.incidentFilters,
|
||||||
page: this.incidentsPage,
|
cursor: this.incidentsCursor,
|
||||||
|
perpage: this.incidentsItemsPerPage,
|
||||||
is_root: true,
|
is_root: true,
|
||||||
skip_slow_rendering,
|
|
||||||
},
|
},
|
||||||
}).catch(refreshPageError);
|
}).catch(refreshPageError);
|
||||||
|
|
||||||
const newAlerts = new Map(result.results.map((alert: Alert) => [alert.pk, alert]));
|
const prevCursor = previousRaw ? qs.parse(qs.extract(previousRaw)).cursor : previousRaw;
|
||||||
|
const nextCursor = nextRaw ? qs.parse(qs.extract(nextRaw)).cursor : nextRaw;
|
||||||
|
|
||||||
|
const newAlerts = new Map(results.map((alert: Alert) => [alert.pk, alert]));
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.alerts = new Map<number, Alert>([...this.alerts, ...newAlerts]);
|
this.alerts = new Map<number, Alert>([...this.alerts, ...newAlerts]);
|
||||||
|
|
||||||
this.alertsSearchResult['default'] = {
|
this.alertsSearchResult['default'] = {
|
||||||
count: result.count,
|
prev: prevCursor,
|
||||||
results: result.results.map((alert: Alert) => alert.pk),
|
next: nextCursor,
|
||||||
|
results: results.map((alert: Alert) => alert.pk),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.alertGroupsLoading = false;
|
this.alertGroupsLoading = false;
|
||||||
|
|
||||||
if (skip_slow_rendering) {
|
|
||||||
const hasShortened = result.results.some((alert: Alert) => alert.short);
|
|
||||||
|
|
||||||
if (hasShortened) {
|
|
||||||
this.updateAlertGroups(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getAlertSearchResult(query: string) {
|
getAlertSearchResult(query: string) {
|
||||||
|
|
@ -273,27 +285,6 @@ export class AlertGroupStore extends BaseStore {
|
||||||
return this.alertsSearchResult[query].results.map((pk: Alert['pk']) => this.alerts.get(pk));
|
return this.alertsSearchResult[query].results.map((pk: Alert['pk']) => this.alerts.get(pk));
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
async searchIncidents(search: string) {
|
|
||||||
const result = await makeRequest(`${this.path}`, {
|
|
||||||
params: {
|
|
||||||
search,
|
|
||||||
resolved: false,
|
|
||||||
is_root: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const newAlerts = new Map(result.results.map((alert: Alert) => [alert.pk, alert]));
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
this.alerts = new Map<number, Alert>([...this.alerts, ...newAlerts]);
|
|
||||||
|
|
||||||
this.alertsSearchResult[search] = {
|
|
||||||
count: result.count,
|
|
||||||
results: result.results.map((alert: Alert) => alert.pk),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
getAlert(pk: Alert['pk']) {
|
getAlert(pk: Alert['pk']) {
|
||||||
return makeRequest(`${this.path}${pk}`, {}).then((alert: Alert) => {
|
return makeRequest(`${this.path}${pk}`, {}).then((alert: Alert) => {
|
||||||
|
|
|
||||||
|
|
@ -42,17 +42,16 @@ export interface Alert {
|
||||||
title: string;
|
title: string;
|
||||||
message: string;
|
message: string;
|
||||||
image_url: string;
|
image_url: string;
|
||||||
alerts: any[];
|
alerts?: any[];
|
||||||
acknowledged: boolean;
|
acknowledged: boolean;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
acknowledged_at: string;
|
acknowledged_at: string;
|
||||||
acknowledged_at_verbose: string;
|
|
||||||
acknowledged_by_user: User;
|
acknowledged_by_user: User;
|
||||||
acknowledged_on_source: boolean;
|
acknowledged_on_source: boolean;
|
||||||
channel: Channel;
|
channel: Channel;
|
||||||
permalink: string;
|
permalink?: string;
|
||||||
related_users: User[];
|
related_users: User[];
|
||||||
render_after_resolve_report_json: TimeLineItem[];
|
render_after_resolve_report_json?: TimeLineItem[];
|
||||||
render_for_slack: { attachments: any[] };
|
render_for_slack: { attachments: any[] };
|
||||||
render_for_web: {
|
render_for_web: {
|
||||||
message: any;
|
message: any;
|
||||||
|
|
@ -63,17 +62,13 @@ export interface Alert {
|
||||||
inside_organization_number: number;
|
inside_organization_number: number;
|
||||||
resolved: boolean;
|
resolved: boolean;
|
||||||
resolved_at: string;
|
resolved_at: string;
|
||||||
resolved_at_verbose: string;
|
|
||||||
resolved_by: number;
|
resolved_by: number;
|
||||||
resolved_by_user: User;
|
resolved_by_user: User;
|
||||||
resolved_by_verbose: string;
|
|
||||||
silenced: boolean;
|
silenced: boolean;
|
||||||
silenced_at: string;
|
silenced_at: string;
|
||||||
silenced_at_verbose: string;
|
|
||||||
silenced_by_user: Partial<User>;
|
silenced_by_user: Partial<User>;
|
||||||
silenced_until: string;
|
silenced_until: string;
|
||||||
started_at: string;
|
started_at: string;
|
||||||
started_at_verbose: string;
|
|
||||||
last_alert_at: string;
|
last_alert_at: string;
|
||||||
verbose_name: string;
|
verbose_name: string;
|
||||||
dependent_alert_groups: Alert[];
|
dependent_alert_groups: Alert[];
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
store,
|
store,
|
||||||
query: { id },
|
query: { id, cursor, start, perpage },
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const { showIntegrationSettings, showAttachIncidentForm, notFound } = this.state;
|
const { showIntegrationSettings, showAttachIncidentForm, notFound } = this.state;
|
||||||
|
|
@ -112,7 +112,7 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
||||||
<VerticalGroup spacing="lg" align="center">
|
<VerticalGroup spacing="lg" align="center">
|
||||||
<Text.Title level={1}>404</Text.Title>
|
<Text.Title level={1}>404</Text.Title>
|
||||||
<Text.Title level={4}>Incident not found</Text.Title>
|
<Text.Title level={4}>Incident not found</Text.Title>
|
||||||
<PluginLink query={{ page: 'incidents' }}>
|
<PluginLink query={{ page: 'incidents', cursor, start, perpage }}>
|
||||||
<Button variant="secondary" icon="arrow-left" size="md">
|
<Button variant="secondary" icon="arrow-left" size="md">
|
||||||
Go to incidents page
|
Go to incidents page
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -182,7 +182,7 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
||||||
renderHeader = () => {
|
renderHeader = () => {
|
||||||
const {
|
const {
|
||||||
store,
|
store,
|
||||||
query: { id },
|
query: { id, cursor, start, perpage },
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const { alerts } = store.alertGroupStore;
|
const { alerts } = store.alertGroupStore;
|
||||||
|
|
@ -197,7 +197,7 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
||||||
<Block withBackground>
|
<Block withBackground>
|
||||||
<VerticalGroup>
|
<VerticalGroup>
|
||||||
<HorizontalGroup className={cx('title')}>
|
<HorizontalGroup className={cx('title')}>
|
||||||
<PluginLink query={{ page: 'incidents' }}>
|
<PluginLink query={{ page: 'incidents', cursor, start, perpage }}>
|
||||||
<IconButton name="arrow-left" size="xxl" />
|
<IconButton name="arrow-left" size="xxl" />
|
||||||
</PluginLink>
|
</PluginLink>
|
||||||
{/* @ts-ignore*/}
|
{/* @ts-ignore*/}
|
||||||
|
|
@ -293,7 +293,13 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
||||||
};
|
};
|
||||||
|
|
||||||
renderIncident = (incident: Alert) => {
|
renderIncident = (incident: Alert) => {
|
||||||
const m = moment(incident.last_alert_at || incident.created_at);
|
let datetimeReference;
|
||||||
|
|
||||||
|
if (incident.last_alert_at || incident.created_at) {
|
||||||
|
const m = moment(incident.last_alert_at || incident.created_at);
|
||||||
|
datetimeReference = `(${m.fromNow()}, ${m.toString()})`;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={incident.pk} className={cx('incident')}>
|
<div key={incident.pk} className={cx('incident')}>
|
||||||
<HorizontalGroup wrap>
|
<HorizontalGroup wrap>
|
||||||
|
|
@ -302,9 +308,7 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
||||||
? `#${incident.inside_organization_number} ${incident.render_for_web.title}`
|
? `#${incident.inside_organization_number} ${incident.render_for_web.title}`
|
||||||
: incident.render_for_web.title}
|
: incident.render_for_web.title}
|
||||||
</Text.Title>
|
</Text.Title>
|
||||||
<Text type="secondary">
|
<Text type="secondary">{datetimeReference}</Text>
|
||||||
({m.fromNow()}, {m.toString()})
|
|
||||||
</Text>
|
|
||||||
</HorizontalGroup>
|
</HorizontalGroup>
|
||||||
<div
|
<div
|
||||||
className={cx('message')}
|
className={cx('message')}
|
||||||
|
|
@ -326,6 +330,9 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
||||||
const incident = store.alertGroupStore.alerts.get(id);
|
const incident = store.alertGroupStore.alerts.get(id);
|
||||||
|
|
||||||
const alerts = incident.alerts;
|
const alerts = incident.alerts;
|
||||||
|
if (!alerts) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const latestAlert = alerts[alerts.length - 1];
|
const latestAlert = alerts[alerts.length - 1];
|
||||||
const latestAlertMoment = moment(latestAlert.created_at);
|
const latestAlertMoment = moment(latestAlert.created_at);
|
||||||
|
|
@ -407,6 +414,10 @@ class IncidentPage extends React.Component<IncidentPageProps, IncidentPageState>
|
||||||
|
|
||||||
const incident = store.alertGroupStore.alerts.get(id);
|
const incident = store.alertGroupStore.alerts.get(id);
|
||||||
|
|
||||||
|
if (!incident.render_after_resolve_report_json) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const timeline = this.filterTimeline(incident.render_after_resolve_report_json);
|
const timeline = this.filterTimeline(incident.render_after_resolve_report_json);
|
||||||
const { timelineFilter, resolutionNoteText } = this.state;
|
const { timelineFilter, resolutionNoteText } = this.state;
|
||||||
const isResolutionNoteTextEmpty = resolutionNoteText === '';
|
const isResolutionNoteTextEmpty = resolutionNoteText === '';
|
||||||
|
|
|
||||||
|
|
@ -34,3 +34,8 @@
|
||||||
height: 24px;
|
height: 24px;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import moment from 'moment';
|
||||||
import Emoji from 'react-emoji-render';
|
import Emoji from 'react-emoji-render';
|
||||||
|
|
||||||
import CardButton from 'components/CardButton/CardButton';
|
import CardButton from 'components/CardButton/CardButton';
|
||||||
|
import CursorPagination from 'components/CursorPagination/CursorPagination';
|
||||||
import GTable from 'components/GTable/GTable';
|
import GTable from 'components/GTable/GTable';
|
||||||
import IntegrationLogo from 'components/IntegrationLogo/IntegrationLogo';
|
import IntegrationLogo from 'components/IntegrationLogo/IntegrationLogo';
|
||||||
import PluginLink from 'components/PluginLink/PluginLink';
|
import PluginLink from 'components/PluginLink/PluginLink';
|
||||||
|
|
@ -35,7 +36,10 @@ import styles from './Incidents.module.css';
|
||||||
|
|
||||||
const cx = cn.bind(styles);
|
const cx = cn.bind(styles);
|
||||||
|
|
||||||
const ITEMS_PER_PAGE = 50;
|
interface Pagination {
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
}
|
||||||
|
|
||||||
function withSkeleton(fn: (alert: AlertType) => ReactElement | ReactElement[]) {
|
function withSkeleton(fn: (alert: AlertType) => ReactElement | ReactElement[]) {
|
||||||
return (alert: AlertType) => {
|
return (alert: AlertType) => {
|
||||||
|
|
@ -53,28 +57,41 @@ interface IncidentsPageState {
|
||||||
selectedIncidentIds: Array<Alert['pk']>;
|
selectedIncidentIds: Array<Alert['pk']>;
|
||||||
affectedRows: { [key: string]: boolean };
|
affectedRows: { [key: string]: boolean };
|
||||||
filters?: IncidentsFiltersType;
|
filters?: IncidentsFiltersType;
|
||||||
|
pagination: Pagination;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ITEMS_PER_PAGE = 25;
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState> {
|
class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState> {
|
||||||
constructor(props: IncidentsPageProps) {
|
constructor(props: IncidentsPageProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const { store } = props;
|
const {
|
||||||
|
store,
|
||||||
|
query: { id, cursor: cursorQuery, start: startQuery, perpage: perpageQuery },
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const cursor = cursorQuery || undefined;
|
||||||
|
const start = !isNaN(startQuery) ? Number(startQuery) : 1;
|
||||||
|
const itemsPerPage = !isNaN(perpageQuery) ? Number(perpageQuery) : ITEMS_PER_PAGE;
|
||||||
|
|
||||||
|
store.alertGroupStore.incidentsCursor = cursor;
|
||||||
|
store.alertGroupStore.incidentsItemsPerPage = itemsPerPage;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
selectedIncidentIds: [],
|
selectedIncidentIds: [],
|
||||||
affectedRows: {},
|
affectedRows: {},
|
||||||
|
pagination: {
|
||||||
|
start,
|
||||||
|
end: start + itemsPerPage - 1,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
store.alertGroupStore.updateBulkActions();
|
store.alertGroupStore.updateBulkActions();
|
||||||
store.alertGroupStore.updateSilenceOptions();
|
store.alertGroupStore.updateSilenceOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {}
|
|
||||||
|
|
||||||
componentDidUpdate() {}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className={cx('root')}>
|
<div className={cx('root')}>
|
||||||
|
|
@ -95,24 +112,55 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFiltersChange = (filters: IncidentsFiltersType) => {
|
handleFiltersChange = (filters: IncidentsFiltersType, isOnMount: boolean) => {
|
||||||
const { store } = this.props;
|
const { store } = this.props;
|
||||||
|
|
||||||
this.setState({ filters, selectedIncidentIds: [] });
|
this.setState({
|
||||||
|
filters,
|
||||||
|
selectedIncidentIds: [],
|
||||||
|
});
|
||||||
|
|
||||||
store.alertGroupStore.updateIncidentFilters(filters, true);
|
if (!isOnMount) {
|
||||||
|
this.setState({
|
||||||
|
pagination: {
|
||||||
|
start: 1,
|
||||||
|
end: store.alertGroupStore.incidentsItemsPerPage,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getLocationSrv().update({ query: { page: 'incidents', ...store.incidentFilters, p: store.incidentsPage } }); // todo fix
|
store.alertGroupStore.updateIncidentFilters(filters, isOnMount);
|
||||||
|
|
||||||
|
getLocationSrv().update({ query: { page: 'incidents', ...store.alertGroupStore.incidentFilters } });
|
||||||
};
|
};
|
||||||
|
|
||||||
onChangePagination = (page: number) => {
|
onChangeCursor = (cursor: string, direction: 'prev' | 'next') => {
|
||||||
const { store } = this.props;
|
const { store } = this.props;
|
||||||
|
|
||||||
store.alertGroupStore.setIncidentsPage(page);
|
store.alertGroupStore.setIncidentsCursor(cursor);
|
||||||
|
|
||||||
this.setState({ selectedIncidentIds: [] });
|
this.setState({
|
||||||
|
selectedIncidentIds: [],
|
||||||
|
pagination: {
|
||||||
|
start:
|
||||||
|
this.state.pagination.start + store.alertGroupStore.incidentsItemsPerPage * (direction === 'prev' ? -1 : 1),
|
||||||
|
end: this.state.pagination.end + store.alertGroupStore.incidentsItemsPerPage * (direction === 'prev' ? -1 : 1),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
getLocationSrv().update({ partial: true, query: { p: store.incidentsPage } });
|
handleChangeItemsPerPage = (value: number) => {
|
||||||
|
const { store } = this.props;
|
||||||
|
|
||||||
|
store.alertGroupStore.setIncidentsItemsPerPage(value);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
selectedIncidentIds: [],
|
||||||
|
pagination: {
|
||||||
|
start: 1,
|
||||||
|
end: store.alertGroupStore.incidentsItemsPerPage,
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
renderBulkActions = () => {
|
renderBulkActions = () => {
|
||||||
|
|
@ -214,7 +262,7 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
|
||||||
};
|
};
|
||||||
|
|
||||||
renderTable() {
|
renderTable() {
|
||||||
const { selectedIncidentIds, affectedRows } = this.state;
|
const { selectedIncidentIds, affectedRows, pagination } = this.state;
|
||||||
const { store } = this.props;
|
const { store } = this.props;
|
||||||
const {
|
const {
|
||||||
teamStore: { currentTeam },
|
teamStore: { currentTeam },
|
||||||
|
|
@ -222,7 +270,8 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
|
||||||
const { alertGroupsLoading } = store.alertGroupStore;
|
const { alertGroupsLoading } = store.alertGroupStore;
|
||||||
|
|
||||||
const results = store.alertGroupStore.getAlertSearchResult('default');
|
const results = store.alertGroupStore.getAlertSearchResult('default');
|
||||||
const count = get(store.alertGroupStore.alertsSearchResult, `default.count`);
|
const prev = get(store.alertGroupStore.alertsSearchResult, `default.prev`);
|
||||||
|
const next = get(store.alertGroupStore.alertsSearchResult, `default.next`);
|
||||||
|
|
||||||
if (results && !results.length) {
|
if (results && !results.length) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -319,12 +368,22 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
|
||||||
data={results}
|
data={results}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
// rowClassName={getUserRowClassNameFn(userPkToEdit, userStore.currentUserPk)}
|
// rowClassName={getUserRowClassNameFn(userPkToEdit, userStore.currentUserPk)}
|
||||||
pagination={{
|
|
||||||
page: store.incidentsPage,
|
|
||||||
total: Math.ceil((count || 0) / ITEMS_PER_PAGE),
|
|
||||||
onChange: this.onChangePagination,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
<div className={cx('pagination')}>
|
||||||
|
<CursorPagination
|
||||||
|
current={`${pagination.start}-${pagination.end}`}
|
||||||
|
itemsPerPage={store.alertGroupStore.incidentsItemsPerPage}
|
||||||
|
itemsPerPageOptions={[
|
||||||
|
{ label: '25', value: 25 },
|
||||||
|
{ label: '50', value: 50 },
|
||||||
|
{ label: '100', value: 100 },
|
||||||
|
]}
|
||||||
|
prev={prev}
|
||||||
|
next={next}
|
||||||
|
onChange={this.onChangeCursor}
|
||||||
|
onChangeItemsPerPage={this.handleChangeItemsPerPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -338,9 +397,20 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTitle = (record: AlertType) => {
|
renderTitle = (record: AlertType) => {
|
||||||
|
const { store } = this.props;
|
||||||
|
const {
|
||||||
|
pagination: { start },
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const { incidentsItemsPerPage, incidentsCursor } = store.alertGroupStore;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VerticalGroup spacing="none" justify="center">
|
<VerticalGroup spacing="none" justify="center">
|
||||||
<PluginLink query={{ page: 'incident', id: record.pk }}>{record.render_for_web.title}</PluginLink>
|
<PluginLink
|
||||||
|
query={{ page: 'incident', id: record.pk, cursor: incidentsCursor, perpage: incidentsItemsPerPage, start }}
|
||||||
|
>
|
||||||
|
{record.render_for_web.title}
|
||||||
|
</PluginLink>
|
||||||
{Boolean(record.dependent_alert_groups.length) && `+ ${record.dependent_alert_groups.length} attached`}
|
{Boolean(record.dependent_alert_groups.length) && `+ ${record.dependent_alert_groups.length} attached`}
|
||||||
</VerticalGroup>
|
</VerticalGroup>
|
||||||
);
|
);
|
||||||
|
|
@ -366,48 +436,6 @@ class Incidents extends React.Component<IncidentsPageProps, IncidentsPageState>
|
||||||
|
|
||||||
renderStatus(record: AlertType) {
|
renderStatus(record: AlertType) {
|
||||||
return getIncidentStatusTag(record);
|
return getIncidentStatusTag(record);
|
||||||
|
|
||||||
/*if (record.resolved) {
|
|
||||||
return (
|
|
||||||
<div className={cx('status')}>
|
|
||||||
<Tooltip title={`Resolved ${record.resolved_at_verbose}`}>
|
|
||||||
<CheckCircleOutlined className={cx('icon-small')} style={{ color: '#52c41a' }} />
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (record.acknowledged) {
|
|
||||||
return (
|
|
||||||
<div className={cx('status')}>
|
|
||||||
<Tooltip title={`Acknowledged ${record.acknowledged_at_verbose}`}>
|
|
||||||
<Icon className={cx('icon-small')} component={AcknowledgedIncidentIcon} />
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (record.silenced) {
|
|
||||||
const silencedUntilText = record.silenced_until
|
|
||||||
? `Silenced until ${moment(record.silenced_until).toLocaleString()}`
|
|
||||||
: 'Silenced forever';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cx('status')}>
|
|
||||||
<Tooltip title={silencedUntilText}>
|
|
||||||
<Icon className={cx('icon-small')} component={SilencedIncidentIcon} />
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cx('status')}>
|
|
||||||
<Tooltip title={`Started ${record.started_at_verbose}`}>
|
|
||||||
<Icon className={cx('icon-small')} component={NewIncidentIcon} />
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
);*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderStartedAt(alert: AlertType) {
|
renderStartedAt(alert: AlertType) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue