remove deprecated heartbeat_heartbeat table/model (#2534)
# What this PR does - Remove `heartbeat_heartbeat` table. This model/table does not seems to be deprecated/used anywhere (no data in this in production/staging; see more comments in the code about this).
This commit is contained in:
parent
aa4edad4a7
commit
63ac0972c5
14 changed files with 111 additions and 324 deletions
|
|
@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Remove deprecated `heartbeat.HeartBeat` model/table by @joeyorlando ([#2534](https://github.com/grafana/oncall/pull/2534))
|
||||||
|
|
||||||
## v1.3.12 (2023-07-14)
|
## v1.3.12 (2023-07-14)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -303,7 +303,7 @@ To configure this feature as such:
|
||||||
|
|
||||||
1. Create a Webhook, or Formatted Webhook, Integration type.
|
1. Create a Webhook, or Formatted Webhook, Integration type.
|
||||||
1. Under the "Heartbeat" tab in the Integration modal, copy the unique heartbeat URL that is shown.
|
1. Under the "Heartbeat" tab in the Integration modal, copy the unique heartbeat URL that is shown.
|
||||||
1. Set the hearbeat's expected time interval to 15 minutes (see note below regarding `ALERT_GROUP_ESCALATION_AUDITOR_CELERY_TASK_HEARTBEAT_INTERVAL`)
|
1. Set the heartbeat's expected time interval to 15 minutes (see note below regarding `ALERT_GROUP_ESCALATION_AUDITOR_CELERY_TASK_HEARTBEAT_INTERVAL`)
|
||||||
1. Configure the integration's escalation chain as necessary
|
1. Configure the integration's escalation chain as necessary
|
||||||
1. Populate the following env variables:
|
1. Populate the following env variables:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
from .models import HeartBeat
|
|
||||||
|
|
||||||
admin.site.register(HeartBeat)
|
|
||||||
18
engine/apps/heartbeat/migrations/0002_delete_heartbeat.py
Normal file
18
engine/apps/heartbeat/migrations/0002_delete_heartbeat.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.2.20 on 2023-07-14 11:36
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import django_migration_linter as linter
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('heartbeat', '0001_squashed_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
linter.IgnoreMigration(),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='HeartBeat',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
|
import typing
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
@ -26,13 +27,19 @@ def generate_public_primary_key_for_integration_heart_beat():
|
||||||
return new_public_primary_key
|
return new_public_primary_key
|
||||||
|
|
||||||
|
|
||||||
class BaseHeartBeat(models.Model):
|
class IntegrationHeartBeat(models.Model):
|
||||||
"""
|
TIMEOUT_CHOICES = (
|
||||||
Implements base heartbeat logic
|
(60, "1 minute"),
|
||||||
"""
|
(120, "2 minutes"),
|
||||||
|
(180, "3 minutes"),
|
||||||
class Meta:
|
(300, "5 minutes"),
|
||||||
abstract = True
|
(600, "10 minutes"),
|
||||||
|
(900, "15 minutes"),
|
||||||
|
(1800, "30 minutes"),
|
||||||
|
(3600, "1 hour"),
|
||||||
|
(43200, "12 hours"),
|
||||||
|
(86400, "1 day"),
|
||||||
|
)
|
||||||
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
timeout_seconds = models.IntegerField(default=0)
|
timeout_seconds = models.IntegerField(default=0)
|
||||||
|
|
@ -41,8 +48,43 @@ class BaseHeartBeat(models.Model):
|
||||||
actual_check_up_task_id = models.CharField(max_length=100)
|
actual_check_up_task_id = models.CharField(max_length=100)
|
||||||
previous_alerted_state_was_life = models.BooleanField(default=True)
|
previous_alerted_state_was_life = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
public_primary_key = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
validators=[MinLengthValidator(settings.PUBLIC_PRIMARY_KEY_MIN_LENGTH + 1)],
|
||||||
|
unique=True,
|
||||||
|
default=generate_public_primary_key_for_integration_heart_beat,
|
||||||
|
)
|
||||||
|
|
||||||
|
alert_receive_channel = models.OneToOneField(
|
||||||
|
"alerts.AlertReceiveChannel", on_delete=models.CASCADE, related_name="integration_heartbeat"
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_expired(self) -> bool:
|
||||||
|
if self.last_heartbeat_time is None:
|
||||||
|
# else heartbeat flow was not received, so heartbeat can't expire.
|
||||||
|
return False
|
||||||
|
|
||||||
|
# if heartbeat signal was received check timeout
|
||||||
|
return self.last_heartbeat_time + timezone.timedelta(seconds=self.timeout_seconds) < timezone.now()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status(self) -> bool:
|
||||||
|
"""
|
||||||
|
Return bool indicates heartbeat status.
|
||||||
|
True if first heartbeat signal was sent and flow is ok else False.
|
||||||
|
If first heartbeat signal was not send it means that configuration was not finished and status not ok.
|
||||||
|
"""
|
||||||
|
if self.last_heartbeat_time is None:
|
||||||
|
return False
|
||||||
|
return not self.is_expired
|
||||||
|
|
||||||
|
@property
|
||||||
|
def link(self) -> str:
|
||||||
|
return urljoin(self.alert_receive_channel.integration_url, "heartbeat/")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def perform_heartbeat_check(cls, heartbeat_id, task_request_id):
|
def perform_heartbeat_check(cls, heartbeat_id: int, task_request_id: str) -> None:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
heartbeats = cls.objects.filter(pk=heartbeat_id).select_for_update()
|
heartbeats = cls.objects.filter(pk=heartbeat_id).select_for_update()
|
||||||
if len(heartbeats) == 0:
|
if len(heartbeats) == 0:
|
||||||
|
|
@ -54,7 +96,7 @@ class BaseHeartBeat(models.Model):
|
||||||
else:
|
else:
|
||||||
logger.info(f"Heartbeat {heartbeat_id} is not actual {task_request_id}")
|
logger.info(f"Heartbeat {heartbeat_id} is not actual {task_request_id}")
|
||||||
|
|
||||||
def check_heartbeat_state_and_save(self):
|
def check_heartbeat_state_and_save(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Use this method if you want just check heartbeat status.
|
Use this method if you want just check heartbeat status.
|
||||||
"""
|
"""
|
||||||
|
|
@ -63,7 +105,7 @@ class BaseHeartBeat(models.Model):
|
||||||
self.save(update_fields=["previous_alerted_state_was_life"])
|
self.save(update_fields=["previous_alerted_state_was_life"])
|
||||||
return state_changed
|
return state_changed
|
||||||
|
|
||||||
def check_heartbeat_state(self):
|
def check_heartbeat_state(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Actually checking heartbeat.
|
Actually checking heartbeat.
|
||||||
Use this method if you want to do changes of heartbeat instance while checking its status.
|
Use this method if you want to do changes of heartbeat instance while checking its status.
|
||||||
|
|
@ -82,120 +124,7 @@ class BaseHeartBeat(models.Model):
|
||||||
state_changed = True
|
state_changed = True
|
||||||
return state_changed
|
return state_changed
|
||||||
|
|
||||||
def on_heartbeat_restored(self):
|
def on_heartbeat_restored(self) -> None:
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def on_heartbeat_expired(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_expired(self):
|
|
||||||
return self.last_heartbeat_time + timezone.timedelta(seconds=self.timeout_seconds) < timezone.now()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def expiration_time(self):
|
|
||||||
return self.last_heartbeat_time + timezone.timedelta(seconds=self.timeout_seconds)
|
|
||||||
|
|
||||||
|
|
||||||
class HeartBeat(BaseHeartBeat):
|
|
||||||
"""
|
|
||||||
HeartBeat Integration itself
|
|
||||||
"""
|
|
||||||
|
|
||||||
alert_receive_channel = models.ForeignKey(
|
|
||||||
"alerts.AlertReceiveChannel", on_delete=models.CASCADE, related_name="heartbeats"
|
|
||||||
)
|
|
||||||
|
|
||||||
message = models.TextField(default="")
|
|
||||||
title = models.TextField(default="HeartBeat Title")
|
|
||||||
link = models.URLField(max_length=500, default=None, null=True)
|
|
||||||
user_defined_id = models.CharField(default="default", max_length=100)
|
|
||||||
|
|
||||||
def on_heartbeat_restored(self):
|
|
||||||
create_alert.apply_async(
|
|
||||||
kwargs={
|
|
||||||
"title": "[OK] " + self.title,
|
|
||||||
"message": self.title,
|
|
||||||
"image_url": None,
|
|
||||||
"link_to_upstream_details": self.link,
|
|
||||||
"alert_receive_channel_pk": self.alert_receive_channel.pk,
|
|
||||||
"integration_unique_data": {},
|
|
||||||
"raw_request_data": {
|
|
||||||
"is_resolve": True,
|
|
||||||
"id": self.pk,
|
|
||||||
"user_defined_id": self.user_defined_id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
def on_heartbeat_expired(self):
|
|
||||||
create_alert.apply_async(
|
|
||||||
kwargs={
|
|
||||||
"title": "[EXPIRED] " + self.title,
|
|
||||||
"message": self.message
|
|
||||||
+ "\nCreated: {}\nExpires: {}\nLast HeartBeat: {}".format(
|
|
||||||
self.created_at,
|
|
||||||
self.expiration_time,
|
|
||||||
self.last_checkup_task_time,
|
|
||||||
),
|
|
||||||
"image_url": None,
|
|
||||||
"link_to_upstream_details": self.link,
|
|
||||||
"alert_receive_channel_pk": self.alert_receive_channel.pk,
|
|
||||||
"integration_unique_data": {},
|
|
||||||
"raw_request_data": {
|
|
||||||
"is_resolve": False,
|
|
||||||
"id": self.pk,
|
|
||||||
"user_defined_id": self.user_defined_id,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
unique_together = (("alert_receive_channel", "user_defined_id"),)
|
|
||||||
|
|
||||||
|
|
||||||
class IntegrationHeartBeat(BaseHeartBeat):
|
|
||||||
"""
|
|
||||||
HeartBeat for Integration (FormattedWebhook, Grafana, etc.)
|
|
||||||
"""
|
|
||||||
|
|
||||||
public_primary_key = models.CharField(
|
|
||||||
max_length=20,
|
|
||||||
validators=[MinLengthValidator(settings.PUBLIC_PRIMARY_KEY_MIN_LENGTH + 1)],
|
|
||||||
unique=True,
|
|
||||||
default=generate_public_primary_key_for_integration_heart_beat,
|
|
||||||
)
|
|
||||||
|
|
||||||
alert_receive_channel = models.OneToOneField(
|
|
||||||
"alerts.AlertReceiveChannel", on_delete=models.CASCADE, related_name="integration_heartbeat"
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_expired(self):
|
|
||||||
if self.last_heartbeat_time is not None:
|
|
||||||
# if heartbeat signal was received check timeout
|
|
||||||
return self.last_heartbeat_time + timezone.timedelta(seconds=self.timeout_seconds) < timezone.now()
|
|
||||||
else:
|
|
||||||
# else heartbeat flow was not received, so heartbeat can't expire.
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def status(self):
|
|
||||||
"""
|
|
||||||
Return bool indicates heartbeat status.
|
|
||||||
True if first heartbeat signal was sent and flow is ok else False.
|
|
||||||
If first heartbeat signal was not send it means that configuration was not finished and status not ok.
|
|
||||||
"""
|
|
||||||
if self.last_heartbeat_time is not None:
|
|
||||||
return not self.is_expired
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def link(self):
|
|
||||||
return urljoin(self.alert_receive_channel.integration_url, "heartbeat/")
|
|
||||||
|
|
||||||
def on_heartbeat_restored(self):
|
|
||||||
create_alert.apply_async(
|
create_alert.apply_async(
|
||||||
kwargs={
|
kwargs={
|
||||||
"title": self.alert_receive_channel.heartbeat_restored_title,
|
"title": self.alert_receive_channel.heartbeat_restored_title,
|
||||||
|
|
@ -208,7 +137,7 @@ class IntegrationHeartBeat(BaseHeartBeat):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_heartbeat_expired(self):
|
def on_heartbeat_expired(self) -> None:
|
||||||
create_alert.apply_async(
|
create_alert.apply_async(
|
||||||
kwargs={
|
kwargs={
|
||||||
"title": self.alert_receive_channel.heartbeat_expired_title,
|
"title": self.alert_receive_channel.heartbeat_expired_title,
|
||||||
|
|
@ -221,36 +150,23 @@ class IntegrationHeartBeat(BaseHeartBeat):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
TIMEOUT_CHOICES = (
|
|
||||||
(60, "1 minute"),
|
|
||||||
(120, "2 minutes"),
|
|
||||||
(180, "3 minutes"),
|
|
||||||
(300, "5 minutes"),
|
|
||||||
(600, "10 minutes"),
|
|
||||||
(900, "15 minutes"),
|
|
||||||
(1800, "30 minutes"),
|
|
||||||
(3600, "1 hour"),
|
|
||||||
(43200, "12 hours"),
|
|
||||||
(86400, "1 day"),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Insight logs
|
# Insight logs
|
||||||
@property
|
@property
|
||||||
def insight_logs_type_verbal(self):
|
def insight_logs_type_verbal(self) -> str:
|
||||||
return "integration_heartbeat"
|
return "integration_heartbeat"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def insight_logs_verbal(self):
|
def insight_logs_verbal(self) -> str:
|
||||||
return f"Integration Heartbeat for {self.alert_receive_channel.insight_logs_verbal}"
|
return f"Integration Heartbeat for {self.alert_receive_channel.insight_logs_verbal}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def insight_logs_serialized(self):
|
def insight_logs_serialized(self) -> typing.Dict[str, str | int]:
|
||||||
return {
|
return {
|
||||||
"timeout": self.timeout_seconds,
|
"timeout": self.timeout_seconds,
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def insight_logs_metadata(self):
|
def insight_logs_metadata(self) -> typing.Dict[str, str]:
|
||||||
return {
|
return {
|
||||||
"integration": self.alert_receive_channel.insight_logs_verbal,
|
"integration": self.alert_receive_channel.insight_logs_verbal,
|
||||||
"integration_id": self.alert_receive_channel.public_primary_key,
|
"integration_id": self.alert_receive_channel.public_primary_key,
|
||||||
|
|
|
||||||
|
|
@ -10,36 +10,12 @@ from common.custom_celery_tasks import shared_dedicated_queue_retry_task
|
||||||
logger = get_task_logger(__name__)
|
logger = get_task_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@shared_dedicated_queue_retry_task(bind=True)
|
|
||||||
def heartbeat_checkup(self, heartbeat_id):
|
|
||||||
HeartBeat = apps.get_model("heartbeat", "HeartBeat")
|
|
||||||
HeartBeat.perform_heartbeat_check(heartbeat_id, heartbeat_checkup.request.id)
|
|
||||||
|
|
||||||
|
|
||||||
@shared_dedicated_queue_retry_task()
|
@shared_dedicated_queue_retry_task()
|
||||||
def integration_heartbeat_checkup(heartbeat_id):
|
def integration_heartbeat_checkup(heartbeat_id: int) -> None:
|
||||||
IntegrationHeartBeat = apps.get_model("heartbeat", "IntegrationHeartBeat")
|
IntegrationHeartBeat = apps.get_model("heartbeat", "IntegrationHeartBeat")
|
||||||
IntegrationHeartBeat.perform_heartbeat_check(heartbeat_id, integration_heartbeat_checkup.request.id)
|
IntegrationHeartBeat.perform_heartbeat_check(heartbeat_id, integration_heartbeat_checkup.request.id)
|
||||||
|
|
||||||
|
|
||||||
@shared_dedicated_queue_retry_task()
|
|
||||||
def restore_heartbeat_tasks():
|
|
||||||
"""
|
|
||||||
Restore heartbeat tasks in case they got lost for some reason
|
|
||||||
"""
|
|
||||||
HeartBeat = apps.get_model("heartbeat", "HeartBeat")
|
|
||||||
for heartbeat in HeartBeat.objects.all():
|
|
||||||
if (
|
|
||||||
heartbeat.last_checkup_task_time
|
|
||||||
+ timezone.timedelta(minutes=5)
|
|
||||||
+ timezone.timedelta(seconds=heartbeat.timeout_seconds)
|
|
||||||
< timezone.now()
|
|
||||||
):
|
|
||||||
task = heartbeat_checkup.apply_async((heartbeat.pk,), countdown=5)
|
|
||||||
heartbeat.actual_check_up_task_id = task.id
|
|
||||||
heartbeat.save()
|
|
||||||
|
|
||||||
|
|
||||||
@shared_dedicated_queue_retry_task()
|
@shared_dedicated_queue_retry_task()
|
||||||
def process_heartbeat_task(alert_receive_channel_pk):
|
def process_heartbeat_task(alert_receive_channel_pk):
|
||||||
start = perf_counter()
|
start = perf_counter()
|
||||||
|
|
|
||||||
|
|
@ -34,10 +34,10 @@ class HeartBeatTextCreator:
|
||||||
def _get_heartbeat_expired_message(self):
|
def _get_heartbeat_expired_message(self):
|
||||||
heartbeat_docs_url = create_engine_url("/#/integrations/heartbeat", override_base=settings.DOCS_URL)
|
heartbeat_docs_url = create_engine_url("/#/integrations/heartbeat", override_base=settings.DOCS_URL)
|
||||||
heartbeat_expired_message = (
|
heartbeat_expired_message = (
|
||||||
f"Amixr was waiting for a heartbeat from {self.integration_verbal}. "
|
f"Grafana OnCall was waiting for a heartbeat from {self.integration_verbal} "
|
||||||
f"Heartbeat is missing. That could happen because {self.integration_verbal} stopped or"
|
f"and one was not received. This can happen when {self.integration_verbal} has stopped or "
|
||||||
f" there are connectivity issues between Amixr and {self.integration_verbal}. "
|
f"there are connectivity issues between Grafana OnCall and {self.integration_verbal}. "
|
||||||
f"Read more in Amixr docs: {heartbeat_docs_url}"
|
f"You can read more in the Grafana OnCall docs here: {heartbeat_docs_url}"
|
||||||
)
|
)
|
||||||
return heartbeat_expired_message
|
return heartbeat_expired_message
|
||||||
|
|
||||||
|
|
@ -46,7 +46,9 @@ class HeartBeatTextCreator:
|
||||||
return heartbeat_expired_title
|
return heartbeat_expired_title
|
||||||
|
|
||||||
def _get_heartbeat_restored_message(self):
|
def _get_heartbeat_restored_message(self):
|
||||||
heartbeat_expired_message = f"Amixr received a signal from {self.integration_verbal}. Heartbeat restored."
|
heartbeat_expired_message = (
|
||||||
|
f"Grafana OnCall received a signal from {self.integration_verbal}. Heartbeat has been restored."
|
||||||
|
)
|
||||||
return heartbeat_expired_message
|
return heartbeat_expired_message
|
||||||
|
|
||||||
def _get_heartbeat_instruction_template(self):
|
def _get_heartbeat_instruction_template(self):
|
||||||
|
|
@ -59,9 +61,9 @@ class HeartBeatTextCreatorForTitleGrouping(HeartBeatTextCreator):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _get_heartbeat_expired_title(self):
|
def _get_heartbeat_expired_title(self):
|
||||||
heartbeat_expired_title = "Amixr heartbeat"
|
heartbeat_expired_title = "Grafana OnCall heartbeat"
|
||||||
return heartbeat_expired_title
|
return heartbeat_expired_title
|
||||||
|
|
||||||
def _get_heartbeat_restored_title(self):
|
def _get_heartbeat_restored_title(self):
|
||||||
heartbeat_expired_title = "Amixr heartbeat"
|
heartbeat_expired_title = "Grafana OnCall heartbeat"
|
||||||
return heartbeat_expired_title
|
return heartbeat_expired_title
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ from .views import (
|
||||||
AmazonSNS,
|
AmazonSNS,
|
||||||
GrafanaAlertingAPIView,
|
GrafanaAlertingAPIView,
|
||||||
GrafanaAPIView,
|
GrafanaAPIView,
|
||||||
HeartBeatAPIView,
|
|
||||||
IntegrationHeartBeatAPIView,
|
IntegrationHeartBeatAPIView,
|
||||||
UniversalAPIView,
|
UniversalAPIView,
|
||||||
)
|
)
|
||||||
|
|
@ -33,7 +32,6 @@ urlpatterns = [
|
||||||
path("grafana_alerting/<str:alert_channel_key>/", GrafanaAlertingAPIView.as_view(), name="grafana_alerting"),
|
path("grafana_alerting/<str:alert_channel_key>/", GrafanaAlertingAPIView.as_view(), name="grafana_alerting"),
|
||||||
path("alertmanager/<str:alert_channel_key>/", AlertManagerAPIView.as_view(), name="alertmanager"),
|
path("alertmanager/<str:alert_channel_key>/", AlertManagerAPIView.as_view(), name="alertmanager"),
|
||||||
path("amazon_sns/<str:alert_channel_key>/", AmazonSNS.as_view(), name="amazon_sns"),
|
path("amazon_sns/<str:alert_channel_key>/", AmazonSNS.as_view(), name="amazon_sns"),
|
||||||
path("heartbeat/<str:alert_channel_key>/", HeartBeatAPIView.as_view(), name="heartbeat"),
|
|
||||||
path("alertmanager_v2/<str:alert_channel_key>/", AlertManagerV2View.as_view(), name="alertmanager_v2"),
|
path("alertmanager_v2/<str:alert_channel_key>/", AlertManagerV2View.as_view(), name="alertmanager_v2"),
|
||||||
path("<str:integration_type>/<str:alert_channel_key>/", UniversalAPIView.as_view(), name="universal"),
|
path("<str:integration_type>/<str:alert_channel_key>/", UniversalAPIView.as_view(), name="universal"),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,9 @@
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.apps import apps
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.db import transaction
|
from django.http import HttpResponseBadRequest, JsonResponse
|
||||||
from django.db.utils import IntegrityError
|
|
||||||
from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
|
|
||||||
from django.template import loader
|
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django_sns_view.views import SNSEndpoint
|
from django_sns_view.views import SNSEndpoint
|
||||||
|
|
@ -16,7 +11,7 @@ from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
from apps.alerts.models import AlertReceiveChannel
|
from apps.alerts.models import AlertReceiveChannel
|
||||||
from apps.heartbeat.tasks import heartbeat_checkup, process_heartbeat_task
|
from apps.heartbeat.tasks import process_heartbeat_task
|
||||||
from apps.integrations.mixins import (
|
from apps.integrations.mixins import (
|
||||||
AlertChannelDefiningMixin,
|
AlertChannelDefiningMixin,
|
||||||
BrowsableInstructionMixin,
|
BrowsableInstructionMixin,
|
||||||
|
|
@ -262,122 +257,6 @@ class UniversalAPIView(BrowsableInstructionMixin, AlertChannelDefiningMixin, Int
|
||||||
return Response("Ok.")
|
return Response("Ok.")
|
||||||
|
|
||||||
|
|
||||||
# TODO: restore HeartBeatAPIView integration or clean it up as it is not used now
|
|
||||||
class HeartBeatAPIView(AlertChannelDefiningMixin, APIView):
|
|
||||||
def get(self, request):
|
|
||||||
template = loader.get_template("heartbeat_link.html")
|
|
||||||
docs_url = create_engine_url("/#/integrations/heartbeat", override_base=settings.DOCS_URL)
|
|
||||||
return HttpResponse(
|
|
||||||
template.render(
|
|
||||||
{
|
|
||||||
"docs_url": docs_url,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
alert_receive_channel = self.request.alert_receive_channel
|
|
||||||
HeartBeat = apps.get_model("heartbeat", "HeartBeat")
|
|
||||||
|
|
||||||
if request.data.get("action") == "activate":
|
|
||||||
# timeout_seconds
|
|
||||||
timeout_seconds = request.data.get("timeout_seconds")
|
|
||||||
try:
|
|
||||||
timeout_seconds = int(timeout_seconds)
|
|
||||||
except ValueError:
|
|
||||||
timeout_seconds = None
|
|
||||||
|
|
||||||
if timeout_seconds is None:
|
|
||||||
return Response(status=400, data="timeout_seconds int expected")
|
|
||||||
# id
|
|
||||||
_id = request.data.get("id", "default")
|
|
||||||
# title
|
|
||||||
title = request.data.get("title", "Title")
|
|
||||||
# title
|
|
||||||
link = request.data.get("link")
|
|
||||||
# message
|
|
||||||
message = request.data.get("message")
|
|
||||||
|
|
||||||
heartbeat = HeartBeat(
|
|
||||||
alert_receive_channel=alert_receive_channel,
|
|
||||||
timeout_seconds=timeout_seconds,
|
|
||||||
title=title,
|
|
||||||
message=message,
|
|
||||||
link=link,
|
|
||||||
user_defined_id=_id,
|
|
||||||
last_heartbeat_time=timezone.now(),
|
|
||||||
last_checkup_task_time=timezone.now(),
|
|
||||||
actual_check_up_task_id="none",
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
heartbeat.save()
|
|
||||||
with transaction.atomic():
|
|
||||||
heartbeat = HeartBeat.objects.filter(pk=heartbeat.pk).select_for_update()[0]
|
|
||||||
task = heartbeat_checkup.apply_async(
|
|
||||||
(heartbeat.pk,),
|
|
||||||
countdown=heartbeat.timeout_seconds,
|
|
||||||
)
|
|
||||||
heartbeat.actual_check_up_task_id = task.id
|
|
||||||
heartbeat.save()
|
|
||||||
except IntegrityError:
|
|
||||||
return Response(status=400, data="id should be unique")
|
|
||||||
|
|
||||||
elif request.data.get("action") == "deactivate":
|
|
||||||
_id = request.data.get("id", "default")
|
|
||||||
try:
|
|
||||||
heartbeat = HeartBeat.objects.filter(
|
|
||||||
alert_receive_channel=alert_receive_channel,
|
|
||||||
user_defined_id=_id,
|
|
||||||
).get()
|
|
||||||
heartbeat.delete()
|
|
||||||
except HeartBeat.DoesNotExist:
|
|
||||||
return Response(status=400, data="heartbeat not found")
|
|
||||||
|
|
||||||
elif request.data.get("action") == "list":
|
|
||||||
result = []
|
|
||||||
heartbeats = HeartBeat.objects.filter(
|
|
||||||
alert_receive_channel=alert_receive_channel,
|
|
||||||
).all()
|
|
||||||
for heartbeat in heartbeats:
|
|
||||||
result.append(
|
|
||||||
{
|
|
||||||
"created_at": heartbeat.created_at,
|
|
||||||
"last_heartbeat": heartbeat.last_heartbeat_time,
|
|
||||||
"expiration_time": heartbeat.expiration_time,
|
|
||||||
"is_expired": heartbeat.is_expired,
|
|
||||||
"id": heartbeat.user_defined_id,
|
|
||||||
"title": heartbeat.title,
|
|
||||||
"timeout_seconds": heartbeat.timeout_seconds,
|
|
||||||
"link": heartbeat.link,
|
|
||||||
"message": heartbeat.message,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return Response(result)
|
|
||||||
|
|
||||||
elif request.data.get("action") == "heartbeat":
|
|
||||||
_id = request.data.get("id", "default")
|
|
||||||
with transaction.atomic():
|
|
||||||
try:
|
|
||||||
heartbeat = HeartBeat.objects.filter(
|
|
||||||
alert_receive_channel=alert_receive_channel,
|
|
||||||
user_defined_id=_id,
|
|
||||||
).select_for_update()[0]
|
|
||||||
task = heartbeat_checkup.apply_async(
|
|
||||||
(heartbeat.pk,),
|
|
||||||
countdown=heartbeat.timeout_seconds,
|
|
||||||
)
|
|
||||||
heartbeat.actual_check_up_task_id = task.id
|
|
||||||
heartbeat.last_heartbeat_time = timezone.now()
|
|
||||||
update_fields = ["actual_check_up_task_id", "last_heartbeat_time"]
|
|
||||||
state_changed = heartbeat.check_heartbeat_state()
|
|
||||||
if state_changed:
|
|
||||||
update_fields.append("previous_alerted_state_was_life")
|
|
||||||
heartbeat.save(update_fields=update_fields)
|
|
||||||
except IndexError:
|
|
||||||
return Response(status=400, data="heartbeat not found")
|
|
||||||
return Response("Ok.")
|
|
||||||
|
|
||||||
|
|
||||||
class IntegrationHeartBeatAPIView(AlertChannelDefiningMixin, IntegrationHeartBeatRateLimitMixin, APIView):
|
class IntegrationHeartBeatAPIView(AlertChannelDefiningMixin, IntegrationHeartBeatRateLimitMixin, APIView):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
self._process_heartbeat_signal(request, request.alert_receive_channel)
|
self._process_heartbeat_signal(request, request.alert_receive_channel)
|
||||||
|
|
|
||||||
|
|
@ -415,11 +415,6 @@ ALERT_GROUP_ESCALATION_AUDITOR_CELERY_TASK_HEARTBEAT_URL = os.getenv(
|
||||||
)
|
)
|
||||||
|
|
||||||
CELERY_BEAT_SCHEDULE = {
|
CELERY_BEAT_SCHEDULE = {
|
||||||
"restore_heartbeat_tasks": {
|
|
||||||
"task": "apps.heartbeat.tasks.restore_heartbeat_tasks",
|
|
||||||
"schedule": 10 * 60,
|
|
||||||
"args": (),
|
|
||||||
},
|
|
||||||
"start_refresh_ical_final_schedules": {
|
"start_refresh_ical_final_schedules": {
|
||||||
"task": "apps.schedules.tasks.refresh_ical_files.start_refresh_ical_final_schedules",
|
"task": "apps.schedules.tasks.refresh_ical_files.start_refresh_ical_final_schedules",
|
||||||
"schedule": crontab(minute=15, hour=0),
|
"schedule": crontab(minute=15, hour=0),
|
||||||
|
|
|
||||||
|
|
@ -51,10 +51,8 @@ CELERY_TASK_ROUTES = {
|
||||||
"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.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"},
|
||||||
"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"},
|
||||||
"apps.heartbeat.tasks.restore_heartbeat_tasks": {"queue": "default"},
|
|
||||||
"apps.metrics_exporter.tasks.start_calculate_and_cache_metrics": {"queue": "default"},
|
"apps.metrics_exporter.tasks.start_calculate_and_cache_metrics": {"queue": "default"},
|
||||||
"apps.metrics_exporter.tasks.start_recalculation_for_new_metric": {"queue": "default"},
|
"apps.metrics_exporter.tasks.start_recalculation_for_new_metric": {"queue": "default"},
|
||||||
"apps.metrics_exporter.tasks.save_organizations_ids_in_cache": {"queue": "default"},
|
"apps.metrics_exporter.tasks.save_organizations_ids_in_cache": {"queue": "default"},
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,12 @@ import { UserActions } from 'utils/authorization';
|
||||||
|
|
||||||
const cx = cn.bind({});
|
const cx = cn.bind({});
|
||||||
|
|
||||||
interface IntegrationHearbeatFormProps {
|
interface IntegrationHeartbeatFormProps {
|
||||||
alertReceveChannelId: AlertReceiveChannel['id'];
|
alertReceveChannelId: AlertReceiveChannel['id'];
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const IntegrationHearbeatForm = observer(({ alertReceveChannelId, onClose }: IntegrationHearbeatFormProps) => {
|
const IntegrationHeartbeatForm = observer(({ alertReceveChannelId, onClose }: IntegrationHeartbeatFormProps) => {
|
||||||
const [interval, setInterval] = useState<number>(undefined);
|
const [interval, setInterval] = useState<number>(undefined);
|
||||||
|
|
||||||
const { heartbeatStore, alertReceiveChannelStore } = useStore();
|
const { heartbeatStore, alertReceiveChannelStore } = useStore();
|
||||||
|
|
@ -110,6 +110,6 @@ const IntegrationHearbeatForm = observer(({ alertReceveChannelId, onClose }: Int
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export default withMobXProviderContext(IntegrationHearbeatForm) as ({
|
export default withMobXProviderContext(IntegrationHeartbeatForm) as ({
|
||||||
alertReceveChannelId,
|
alertReceveChannelId,
|
||||||
}: IntegrationHearbeatFormProps) => JSX.Element;
|
}: IntegrationHeartbeatFormProps) => JSX.Element;
|
||||||
|
|
@ -43,7 +43,7 @@ import { WithContextMenu } from 'components/WithContextMenu/WithContextMenu';
|
||||||
import EditRegexpRouteTemplateModal from 'containers/EditRegexpRouteTemplateModal/EditRegexpRouteTemplateModal';
|
import EditRegexpRouteTemplateModal from 'containers/EditRegexpRouteTemplateModal/EditRegexpRouteTemplateModal';
|
||||||
import CollapsedIntegrationRouteDisplay from 'containers/IntegrationContainers/CollapsedIntegrationRouteDisplay/CollapsedIntegrationRouteDisplay';
|
import CollapsedIntegrationRouteDisplay from 'containers/IntegrationContainers/CollapsedIntegrationRouteDisplay/CollapsedIntegrationRouteDisplay';
|
||||||
import ExpandedIntegrationRouteDisplay from 'containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay';
|
import ExpandedIntegrationRouteDisplay from 'containers/IntegrationContainers/ExpandedIntegrationRouteDisplay/ExpandedIntegrationRouteDisplay';
|
||||||
import IntegrationHeartbeatForm from 'containers/IntegrationContainers/IntegrationHearbeatForm/IntegrationHeartbeatForm';
|
import IntegrationHeartbeatForm from 'containers/IntegrationContainers/IntegrationHeartbeatForm/IntegrationHeartbeatForm';
|
||||||
import IntegrationTemplateList from 'containers/IntegrationContainers/IntegrationTemplatesList';
|
import IntegrationTemplateList from 'containers/IntegrationContainers/IntegrationTemplatesList';
|
||||||
import IntegrationForm from 'containers/IntegrationForm/IntegrationForm';
|
import IntegrationForm from 'containers/IntegrationForm/IntegrationForm';
|
||||||
import IntegrationTemplate from 'containers/IntegrationTemplate/IntegrationTemplate';
|
import IntegrationTemplate from 'containers/IntegrationTemplate/IntegrationTemplate';
|
||||||
|
|
@ -742,7 +742,7 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
|
||||||
}>(undefined);
|
}>(undefined);
|
||||||
|
|
||||||
const [isIntegrationSettingsOpen, setIsIntegrationSettingsOpen] = useState(false);
|
const [isIntegrationSettingsOpen, setIsIntegrationSettingsOpen] = useState(false);
|
||||||
const [isHearbeatFormOpen, setIsHearbeatFormOpen] = useState(false);
|
const [isHeartbeatFormOpen, setIsHeartbeatFormOpen] = useState(false);
|
||||||
const [isDemoModalOpen, setIsDemoModalOpen] = useState(false);
|
const [isDemoModalOpen, setIsDemoModalOpen] = useState(false);
|
||||||
const [maintenanceData, setMaintenanceData] = useState<{
|
const [maintenanceData, setMaintenanceData] = useState<{
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
|
@ -784,10 +784,10 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isHearbeatFormOpen && (
|
{isHeartbeatFormOpen && (
|
||||||
<IntegrationHeartbeatForm
|
<IntegrationHeartbeatForm
|
||||||
alertReceveChannelId={alertReceiveChannel['id']}
|
alertReceveChannelId={alertReceiveChannel['id']}
|
||||||
onClose={() => setIsHearbeatFormOpen(false)}
|
onClose={() => setIsHeartbeatFormOpen(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -823,7 +823,7 @@ const IntegrationActions: React.FC<IntegrationActionsProps> = ({
|
||||||
|
|
||||||
{showHeartbeatSettings() && (
|
{showHeartbeatSettings() && (
|
||||||
<WithPermissionControlTooltip key="ok" userAction={UserActions.IntegrationsWrite}>
|
<WithPermissionControlTooltip key="ok" userAction={UserActions.IntegrationsWrite}>
|
||||||
<div className={cx('integration__actionItem')} onClick={() => setIsHearbeatFormOpen(true)}>
|
<div className={cx('integration__actionItem')} onClick={() => setIsHeartbeatFormOpen(true)}>
|
||||||
Heartbeat Settings
|
Heartbeat Settings
|
||||||
</div>
|
</div>
|
||||||
</WithPermissionControlTooltip>
|
</WithPermissionControlTooltip>
|
||||||
|
|
@ -1117,7 +1117,7 @@ const IntegrationHeader: React.FC<IntegrationHeaderProps> = ({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{renderHearbeat(alertReceiveChannel)}
|
{renderHeartbeat(alertReceiveChannel)}
|
||||||
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'row', gap: '16px', marginLeft: '8px' }}>
|
<div style={{ display: 'flex', flexDirection: 'row', gap: '16px', marginLeft: '8px' }}>
|
||||||
<div className={cx('headerTop__item')}>
|
<div className={cx('headerTop__item')}>
|
||||||
|
|
@ -1153,7 +1153,7 @@ const IntegrationHeader: React.FC<IntegrationHeaderProps> = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderHearbeat(alertReceiveChannel: AlertReceiveChannel) {
|
function renderHeartbeat(alertReceiveChannel: AlertReceiveChannel) {
|
||||||
const heartbeatId = alertReceiveChannelStore.alertReceiveChannelToHeartbeat[alertReceiveChannel.id];
|
const heartbeatId = alertReceiveChannelStore.alertReceiveChannelToHeartbeat[alertReceiveChannel.id];
|
||||||
const heartbeat = heartbeatStore.items[heartbeatId];
|
const heartbeat = heartbeatStore.items[heartbeatId];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,9 @@ import RemoteFilters from 'containers/RemoteFilters/RemoteFilters';
|
||||||
import TeamName from 'containers/TeamName/TeamName';
|
import TeamName from 'containers/TeamName/TeamName';
|
||||||
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
import { WithPermissionControlTooltip } from 'containers/WithPermissionControl/WithPermissionControlTooltip';
|
||||||
import { HeartIcon, HeartRedIcon } from 'icons';
|
import { HeartIcon, HeartRedIcon } from 'icons';
|
||||||
|
import { AlertReceiveChannelStore } from 'models/alert_receive_channel/alert_receive_channel';
|
||||||
import { AlertReceiveChannel, MaintenanceMode } from 'models/alert_receive_channel/alert_receive_channel.types';
|
import { AlertReceiveChannel, MaintenanceMode } from 'models/alert_receive_channel/alert_receive_channel.types';
|
||||||
|
import { HeartbeatStore } from 'models/heartbeat/heartbeat';
|
||||||
import IntegrationHelper from 'pages/integration/Integration.helper';
|
import IntegrationHelper from 'pages/integration/Integration.helper';
|
||||||
import { PageProps, WithStoreProps } from 'state/types';
|
import { PageProps, WithStoreProps } from 'state/types';
|
||||||
import { withMobXProviderContext } from 'state/withStore';
|
import { withMobXProviderContext } from 'state/withStore';
|
||||||
|
|
@ -343,7 +345,11 @@ class Integrations extends React.Component<IntegrationsProps, IntegrationsState>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderHeartbeat(item: AlertReceiveChannel, alertReceiveChannelStore, heartbeatStore) {
|
renderHeartbeat(
|
||||||
|
item: AlertReceiveChannel,
|
||||||
|
alertReceiveChannelStore: AlertReceiveChannelStore,
|
||||||
|
heartbeatStore: HeartbeatStore
|
||||||
|
) {
|
||||||
const alertReceiveChannel = alertReceiveChannelStore.items[item.id];
|
const alertReceiveChannel = alertReceiveChannelStore.items[item.id];
|
||||||
|
|
||||||
const heartbeatId = alertReceiveChannelStore.alertReceiveChannelToHeartbeat[alertReceiveChannel.id];
|
const heartbeatId = alertReceiveChannelStore.alertReceiveChannelToHeartbeat[alertReceiveChannel.id];
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue