forked from GithubBackups/healthchecks
Calculate alert_after
in Python code instead of a database trigger. This will allow complex calculations down the road.
This commit is contained in:
parent
e524ea3db7
commit
ce57a1cc8b
36
hc/api/management/commands/droptriggers.py
Normal file
36
hc/api/management/commands/droptriggers.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.db import connection
|
||||||
|
|
||||||
|
|
||||||
|
def _pg(cursor):
|
||||||
|
cursor.execute("""
|
||||||
|
DROP TRIGGER IF EXISTS update_alert_after ON api_check;
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
def _mysql(cursor):
|
||||||
|
cursor.execute("""
|
||||||
|
DROP TRIGGER IF EXISTS update_alert_after;
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
def _sqlite(cursor):
|
||||||
|
cursor.execute("""
|
||||||
|
DROP TRIGGER IF EXISTS update_alert_after;
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Drops the `update_alert_after` trigger'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
if connection.vendor == "postgresql":
|
||||||
|
_pg(cursor)
|
||||||
|
return "Dropped PostgreSQL trigger"
|
||||||
|
if connection.vendor == "mysql":
|
||||||
|
_mysql(cursor)
|
||||||
|
return "Dropped MySQL trigger"
|
||||||
|
if connection.vendor == "sqlite":
|
||||||
|
_sqlite(cursor)
|
||||||
|
return "Dropped SQLite trigger"
|
@ -1,70 +0,0 @@
|
|||||||
from django.core.management.base import BaseCommand
|
|
||||||
from django.db import connection
|
|
||||||
|
|
||||||
|
|
||||||
def _pg(cursor):
|
|
||||||
cursor.execute("""
|
|
||||||
CREATE OR REPLACE FUNCTION update_alert_after()
|
|
||||||
RETURNS trigger AS $update_alert_after$
|
|
||||||
BEGIN
|
|
||||||
IF NEW.last_ping IS NOT NULL THEN
|
|
||||||
NEW.alert_after := NEW.last_ping + NEW.timeout + NEW.grace;
|
|
||||||
END IF;
|
|
||||||
RETURN NEW;
|
|
||||||
END;
|
|
||||||
$update_alert_after$ LANGUAGE plpgsql;
|
|
||||||
|
|
||||||
DROP TRIGGER IF EXISTS update_alert_after ON api_check;
|
|
||||||
|
|
||||||
CREATE TRIGGER update_alert_after
|
|
||||||
BEFORE INSERT OR UPDATE OF last_ping, timeout, grace ON api_check
|
|
||||||
FOR EACH ROW EXECUTE PROCEDURE update_alert_after();
|
|
||||||
""")
|
|
||||||
|
|
||||||
|
|
||||||
def _mysql(cursor):
|
|
||||||
cursor.execute("""
|
|
||||||
DROP TRIGGER IF EXISTS update_alert_after;
|
|
||||||
""")
|
|
||||||
|
|
||||||
cursor.execute("""
|
|
||||||
CREATE TRIGGER update_alert_after
|
|
||||||
BEFORE UPDATE ON api_check
|
|
||||||
FOR EACH ROW SET
|
|
||||||
NEW.alert_after =
|
|
||||||
NEW.last_ping + INTERVAL (NEW.timeout + NEW.grace) MICROSECOND;
|
|
||||||
""")
|
|
||||||
|
|
||||||
|
|
||||||
def _sqlite(cursor):
|
|
||||||
cursor.execute("""
|
|
||||||
DROP TRIGGER IF EXISTS update_alert_after;
|
|
||||||
""")
|
|
||||||
|
|
||||||
cursor.execute("""
|
|
||||||
CREATE TRIGGER update_alert_after
|
|
||||||
AFTER UPDATE OF last_ping, timeout, grace ON api_check
|
|
||||||
FOR EACH ROW BEGIN
|
|
||||||
UPDATE api_check
|
|
||||||
SET alert_after =
|
|
||||||
datetime(strftime('%s', last_ping) +
|
|
||||||
timeout/1000000 + grace/1000000, 'unixepoch')
|
|
||||||
WHERE id = OLD.id;
|
|
||||||
END;
|
|
||||||
""")
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = 'Ensures triggers exist in database'
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
with connection.cursor() as cursor:
|
|
||||||
if connection.vendor == "postgresql":
|
|
||||||
_pg(cursor)
|
|
||||||
return "Created PostgreSQL trigger"
|
|
||||||
if connection.vendor == "mysql":
|
|
||||||
_mysql(cursor)
|
|
||||||
return "Created MySQL trigger"
|
|
||||||
if connection.vendor == "sqlite":
|
|
||||||
_sqlite(cursor)
|
|
||||||
return "Created SQLite trigger"
|
|
@ -17,6 +17,11 @@ def notify(check_id, stdout):
|
|||||||
stdout.write("ERROR: %s %s %s\n" % (ch.kind, ch.value, error))
|
stdout.write("ERROR: %s %s %s\n" % (ch.kind, ch.value, error))
|
||||||
|
|
||||||
|
|
||||||
|
def notify_on_thread(check_id, stdout):
|
||||||
|
t = Thread(target=notify, args=(check_id, stdout))
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = 'Sends UP/DOWN email alerts'
|
help = 'Sends UP/DOWN email alerts'
|
||||||
owned = Check.objects.filter(user__isnull=False)
|
owned = Check.objects.filter(user__isnull=False)
|
||||||
@ -27,25 +32,31 @@ class Command(BaseCommand):
|
|||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
|
|
||||||
# Look for checks that are going down
|
# Look for checks that are going down
|
||||||
flipped = "down"
|
|
||||||
q = self.owned.filter(alert_after__lt=now, status="up")
|
q = self.owned.filter(alert_after__lt=now, status="up")
|
||||||
check = q.first()
|
check = q.first()
|
||||||
|
|
||||||
|
# If none found, look for checks that are going up
|
||||||
if not check:
|
if not check:
|
||||||
# If none found, look for checks that are going up
|
|
||||||
flipped = "up"
|
|
||||||
q = self.owned.filter(alert_after__gt=now, status="down")
|
q = self.owned.filter(alert_after__gt=now, status="down")
|
||||||
check = q.first()
|
check = q.first()
|
||||||
|
|
||||||
if check:
|
if check is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
q = Check.objects.filter(id=check.id, status=check.status)
|
||||||
|
current_status = check.get_status()
|
||||||
|
if check.status == current_status:
|
||||||
|
# Stored status is already up-to-date. Update alert_after
|
||||||
|
# as needed but don't send notifications
|
||||||
|
q.update(alert_after=check.get_alert_after())
|
||||||
|
return True
|
||||||
|
else:
|
||||||
# Atomically update status to the opposite
|
# Atomically update status to the opposite
|
||||||
q = Check.objects.filter(id=check.id, status=check.status)
|
num_updated = q.update(status=current_status)
|
||||||
num_updated = q.update(status=flipped)
|
|
||||||
if num_updated == 1:
|
if num_updated == 1:
|
||||||
# Send notifications only if status update succeeded
|
# Send notifications only if status update succeeded
|
||||||
# (no other sendalerts process got there first)
|
# (no other sendalerts process got there first)
|
||||||
t = Thread(target=notify, args=(check.id, self.stdout))
|
notify_on_thread(check.id, self.stdout)
|
||||||
t.start()
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
@ -96,6 +96,9 @@ class Check(models.Model):
|
|||||||
|
|
||||||
return "down"
|
return "down"
|
||||||
|
|
||||||
|
def get_alert_after(self):
|
||||||
|
return self.last_ping + self.timeout + self.grace
|
||||||
|
|
||||||
def in_grace_period(self):
|
def in_grace_period(self):
|
||||||
if self.status in ("new", "paused"):
|
if self.status in ("new", "paused"):
|
||||||
return False
|
return False
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from django.test import TestCase
|
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
from hc.api.management.commands.ensuretriggers import Command
|
|
||||||
from hc.api.models import Check
|
|
||||||
|
|
||||||
|
|
||||||
class EnsureTriggersTestCase(TestCase):
|
|
||||||
|
|
||||||
def test_ensure_triggers(self):
|
|
||||||
Command().handle()
|
|
||||||
|
|
||||||
check = Check.objects.create()
|
|
||||||
assert check.alert_after is None
|
|
||||||
|
|
||||||
check.last_ping = timezone.now()
|
|
||||||
check.save()
|
|
||||||
check.refresh_from_db()
|
|
||||||
assert check.alert_after is not None
|
|
||||||
alert_after = check.alert_after
|
|
||||||
|
|
||||||
check.last_ping += timedelta(days=1)
|
|
||||||
check.save()
|
|
||||||
check.refresh_from_db()
|
|
||||||
assert check.alert_after > alert_after
|
|
@ -14,7 +14,8 @@ class PingTestCase(TestCase):
|
|||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
|
|
||||||
self.check.refresh_from_db()
|
self.check.refresh_from_db()
|
||||||
assert self.check.status == "up"
|
self.assertEqual(self.check.status, "up")
|
||||||
|
self.assertEqual(self.check.alert_after, self.check.get_alert_after())
|
||||||
|
|
||||||
ping = Ping.objects.latest("id")
|
ping = Ping.objects.latest("id")
|
||||||
assert ping.scheme == "http"
|
assert ping.scheme == "http"
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from mock import patch
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from hc.api.management.commands.sendalerts import Command
|
from hc.api.management.commands.sendalerts import Command
|
||||||
@ -12,7 +13,70 @@ class SendAlertsTestCase(BaseTestCase):
|
|||||||
check = Check(user=self.alice, status="up")
|
check = Check(user=self.alice, status="up")
|
||||||
# 1 day 30 minutes after ping the check is in grace period:
|
# 1 day 30 minutes after ping the check is in grace period:
|
||||||
check.last_ping = timezone.now() - timedelta(days=1, minutes=30)
|
check.last_ping = timezone.now() - timedelta(days=1, minutes=30)
|
||||||
|
check.alert_after = check.get_alert_after()
|
||||||
check.save()
|
check.save()
|
||||||
|
|
||||||
# Expect no exceptions--
|
# Expect no exceptions--
|
||||||
Command().handle_one()
|
Command().handle_one()
|
||||||
|
|
||||||
|
@patch("hc.api.management.commands.sendalerts.notify_on_thread")
|
||||||
|
def test_it_notifies_when_check_goes_down(self, mock_notify):
|
||||||
|
check = Check(user=self.alice, status="up")
|
||||||
|
check.last_ping = timezone.now() - timedelta(days=2)
|
||||||
|
check.alert_after = check.get_alert_after()
|
||||||
|
check.save()
|
||||||
|
|
||||||
|
result = Command().handle_one()
|
||||||
|
|
||||||
|
# If it finds work, it should return True
|
||||||
|
self.assertTrue(result)
|
||||||
|
|
||||||
|
# It should change stored status to "down"
|
||||||
|
check.refresh_from_db()
|
||||||
|
self.assertEqual(check.status, "down")
|
||||||
|
|
||||||
|
# It should call `notify`
|
||||||
|
self.assertTrue(mock_notify.called)
|
||||||
|
|
||||||
|
@patch("hc.api.management.commands.sendalerts.notify_on_thread")
|
||||||
|
def test_it_notifies_when_check_goes_up(self, mock_notify):
|
||||||
|
check = Check(user=self.alice, status="down")
|
||||||
|
check.last_ping = timezone.now()
|
||||||
|
check.alert_after = check.get_alert_after()
|
||||||
|
check.save()
|
||||||
|
|
||||||
|
result = Command().handle_one()
|
||||||
|
|
||||||
|
# If it finds work, it should return True
|
||||||
|
self.assertTrue(result)
|
||||||
|
|
||||||
|
# It should change stored status to "up"
|
||||||
|
check.refresh_from_db()
|
||||||
|
self.assertEqual(check.status, "up")
|
||||||
|
|
||||||
|
# It should call `notify`
|
||||||
|
self.assertTrue(mock_notify.called)
|
||||||
|
|
||||||
|
# alert_after now should be set
|
||||||
|
self.assertTrue(check.alert_after)
|
||||||
|
|
||||||
|
@patch("hc.api.management.commands.sendalerts.notify_on_thread")
|
||||||
|
def test_it_updates_alert_after(self, mock_notify):
|
||||||
|
check = Check(user=self.alice, status="up")
|
||||||
|
check.last_ping = timezone.now() - timedelta(hours=1)
|
||||||
|
check.alert_after = check.last_ping
|
||||||
|
check.save()
|
||||||
|
|
||||||
|
result = Command().handle_one()
|
||||||
|
|
||||||
|
# If it finds work, it should return True
|
||||||
|
self.assertTrue(result)
|
||||||
|
|
||||||
|
# It should change stored status to "down"
|
||||||
|
check.refresh_from_db()
|
||||||
|
|
||||||
|
# alert_after should have been increased
|
||||||
|
self.assertTrue(check.alert_after > check.last_ping)
|
||||||
|
|
||||||
|
# notify should *not* have been called
|
||||||
|
self.assertFalse(mock_notify.called)
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
from datetime import timedelta as td
|
from datetime import timedelta as td
|
||||||
|
|
||||||
from django.core.exceptions import FieldError
|
|
||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
|
from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@ -24,6 +23,7 @@ def ping(request, code):
|
|||||||
|
|
||||||
check.n_pings = F("n_pings") + 1
|
check.n_pings = F("n_pings") + 1
|
||||||
check.last_ping = timezone.now()
|
check.last_ping = timezone.now()
|
||||||
|
check.alert_after = check.get_alert_after()
|
||||||
if check.status in ("new", "paused"):
|
if check.status in ("new", "paused"):
|
||||||
check.status = "up"
|
check.status = "up"
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from django.utils import timezone
|
||||||
from hc.api.models import Check
|
from hc.api.models import Check
|
||||||
from hc.test import BaseTestCase
|
from hc.test import BaseTestCase
|
||||||
|
|
||||||
@ -7,6 +8,7 @@ class UpdateTimeoutTestCase(BaseTestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(UpdateTimeoutTestCase, self).setUp()
|
super(UpdateTimeoutTestCase, self).setUp()
|
||||||
self.check = Check(user=self.alice)
|
self.check = Check(user=self.alice)
|
||||||
|
self.check.last_ping = timezone.now()
|
||||||
self.check.save()
|
self.check.save()
|
||||||
|
|
||||||
def test_it_works(self):
|
def test_it_works(self):
|
||||||
@ -17,9 +19,12 @@ class UpdateTimeoutTestCase(BaseTestCase):
|
|||||||
r = self.client.post(url, data=payload)
|
r = self.client.post(url, data=payload)
|
||||||
self.assertRedirects(r, "/checks/")
|
self.assertRedirects(r, "/checks/")
|
||||||
|
|
||||||
check = Check.objects.get(code=self.check.code)
|
self.check.refresh_from_db()
|
||||||
assert check.timeout.total_seconds() == 3600
|
self.assertEqual(self.check.timeout.total_seconds(), 3600)
|
||||||
assert check.grace.total_seconds() == 60
|
self.assertEqual(self.check.grace.total_seconds(), 60)
|
||||||
|
|
||||||
|
# alert_after should be updated too
|
||||||
|
self.assertEqual(self.check.alert_after, self.check.get_alert_after())
|
||||||
|
|
||||||
def test_team_access_works(self):
|
def test_team_access_works(self):
|
||||||
url = "/checks/%s/timeout/" % self.check.code
|
url = "/checks/%s/timeout/" % self.check.code
|
||||||
|
@ -165,6 +165,9 @@ def update_timeout(request, code):
|
|||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
check.timeout = td(seconds=form.cleaned_data["timeout"])
|
check.timeout = td(seconds=form.cleaned_data["timeout"])
|
||||||
check.grace = td(seconds=form.cleaned_data["grace"])
|
check.grace = td(seconds=form.cleaned_data["grace"])
|
||||||
|
if check.last_ping:
|
||||||
|
check.alert_after = check.get_alert_after()
|
||||||
|
|
||||||
check.save()
|
check.save()
|
||||||
|
|
||||||
return redirect("hc-checks")
|
return redirect("hc-checks")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user