Skip to content

Commit

Permalink
remove deprecated heartbeat_heartbeat table/model (#2534)
Browse files Browse the repository at this point in the history
# 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).
  • Loading branch information
joeyorlando authored Jul 17, 2023
1 parent aa4edad commit 63ac097
Show file tree
Hide file tree
Showing 14 changed files with 111 additions and 324 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Changed

- Remove deprecated `heartbeat.HeartBeat` model/table by @joeyorlando ([#2534](https://github.com/grafana/oncall/pull/2534))

## v1.3.12 (2023-07-14)

### Added
Expand Down
2 changes: 1 addition & 1 deletion docs/sources/open-source/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ To configure this feature as such:

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. 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. Populate the following env variables:

Expand Down
5 changes: 0 additions & 5 deletions engine/apps/heartbeat/admin.py

This file was deleted.

18 changes: 18 additions & 0 deletions engine/apps/heartbeat/migrations/0002_delete_heartbeat.py
Original file line number Diff line number Diff line change
@@ -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',
),
]
200 changes: 58 additions & 142 deletions engine/apps/heartbeat/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import typing
from urllib.parse import urljoin

from django.conf import settings
Expand Down Expand Up @@ -26,13 +27,19 @@ def generate_public_primary_key_for_integration_heart_beat():
return new_public_primary_key


class BaseHeartBeat(models.Model):
"""
Implements base heartbeat logic
"""

class Meta:
abstract = True
class IntegrationHeartBeat(models.Model):
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"),
)

created_at = models.DateTimeField(auto_now_add=True)
timeout_seconds = models.IntegerField(default=0)
Expand All @@ -41,8 +48,43 @@ class Meta:
actual_check_up_task_id = models.CharField(max_length=100)
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
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():
heartbeats = cls.objects.filter(pk=heartbeat_id).select_for_update()
if len(heartbeats) == 0:
Expand All @@ -54,7 +96,7 @@ def perform_heartbeat_check(cls, heartbeat_id, task_request_id):
else:
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.
"""
Expand All @@ -63,7 +105,7 @@ def check_heartbeat_state_and_save(self):
self.save(update_fields=["previous_alerted_state_was_life"])
return state_changed

def check_heartbeat_state(self):
def check_heartbeat_state(self) -> bool:
"""
Actually checking heartbeat.
Use this method if you want to do changes of heartbeat instance while checking its status.
Expand All @@ -82,120 +124,7 @@ def check_heartbeat_state(self):
state_changed = True
return state_changed

def on_heartbeat_restored(self):
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):
def on_heartbeat_restored(self) -> None:
create_alert.apply_async(
kwargs={
"title": self.alert_receive_channel.heartbeat_restored_title,
Expand All @@ -208,7 +137,7 @@ def on_heartbeat_restored(self):
},
)

def on_heartbeat_expired(self):
def on_heartbeat_expired(self) -> None:
create_alert.apply_async(
kwargs={
"title": self.alert_receive_channel.heartbeat_expired_title,
Expand All @@ -221,36 +150,23 @@ def on_heartbeat_expired(self):
},
)

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
@property
def insight_logs_type_verbal(self):
def insight_logs_type_verbal(self) -> str:
return "integration_heartbeat"

@property
def insight_logs_verbal(self):
def insight_logs_verbal(self) -> str:
return f"Integration Heartbeat for {self.alert_receive_channel.insight_logs_verbal}"

@property
def insight_logs_serialized(self):
def insight_logs_serialized(self) -> typing.Dict[str, str | int]:
return {
"timeout": self.timeout_seconds,
}

@property
def insight_logs_metadata(self):
def insight_logs_metadata(self) -> typing.Dict[str, str]:
return {
"integration": self.alert_receive_channel.insight_logs_verbal,
"integration_id": self.alert_receive_channel.public_primary_key,
Expand Down
26 changes: 1 addition & 25 deletions engine/apps/heartbeat/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,36 +10,12 @@
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()
def integration_heartbeat_checkup(heartbeat_id):
def integration_heartbeat_checkup(heartbeat_id: int) -> None:
IntegrationHeartBeat = apps.get_model("heartbeat", "IntegrationHeartBeat")
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()
def process_heartbeat_task(alert_receive_channel_pk):
start = perf_counter()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ def _get_heartbeat_expired_title(self):
def _get_heartbeat_expired_message(self):
heartbeat_docs_url = create_engine_url("/#/integrations/heartbeat", override_base=settings.DOCS_URL)
heartbeat_expired_message = (
f"Amixr was waiting for a heartbeat from {self.integration_verbal}. "
f"Heartbeat is missing. That could happen because {self.integration_verbal} stopped or"
f" there are connectivity issues between Amixr and {self.integration_verbal}. "
f"Read more in Amixr docs: {heartbeat_docs_url}"
f"Grafana OnCall was waiting for a heartbeat from {self.integration_verbal} "
f"and one was not received. This can happen when {self.integration_verbal} has stopped or "
f"there are connectivity issues between Grafana OnCall and {self.integration_verbal}. "
f"You can read more in the Grafana OnCall docs here: {heartbeat_docs_url}"
)
return heartbeat_expired_message

Expand All @@ -46,7 +46,9 @@ def _get_heartbeat_restored_title(self):
return heartbeat_expired_title

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

def _get_heartbeat_instruction_template(self):
Expand All @@ -59,9 +61,9 @@ class HeartBeatTextCreatorForTitleGrouping(HeartBeatTextCreator):
"""

def _get_heartbeat_expired_title(self):
heartbeat_expired_title = "Amixr heartbeat"
heartbeat_expired_title = "Grafana OnCall heartbeat"
return heartbeat_expired_title

def _get_heartbeat_restored_title(self):
heartbeat_expired_title = "Amixr heartbeat"
heartbeat_expired_title = "Grafana OnCall heartbeat"
return heartbeat_expired_title
Loading

0 comments on commit 63ac097

Please sign in to comment.