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:
Vadim Stepanov 2022-07-14 15:19:25 +01:00 committed by GitHub
parent 78598fb95b
commit 16bbfbbe73
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 438 additions and 653 deletions

View file

@ -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

View file

@ -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):

View 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',
),
]

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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:

View file

@ -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

View file

@ -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}")

View file

@ -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={})

View file

@ -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

View file

@ -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

View file

@ -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}")

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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(

View file

@ -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,

View file

@ -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"}},

View file

@ -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(

View file

@ -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})

View file

@ -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"

View file

@ -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")

View file

@ -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": {

View file

@ -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"},

View file

@ -0,0 +1,3 @@
.root {
display: block;
}

View file

@ -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;

View file

@ -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);

View file

@ -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) => {

View file

@ -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[];

View file

@ -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 === '';

View file

@ -34,3 +34,8 @@
height: 24px; height: 24px;
margin-right: 0; margin-right: 0;
} }
.pagination {
width: 100%;
margin-top: 20px;
}

View file

@ -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) {