diff --git a/hc/api/migrations/0040_auto_20180517_1336.py b/hc/api/migrations/0040_auto_20180517_1336.py new file mode 100644 index 00000000..1be49e56 --- /dev/null +++ b/hc/api/migrations/0040_auto_20180517_1336.py @@ -0,0 +1,23 @@ +# Generated by Django 2.0.4 on 2018-05-17 13:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0039_remove_check_last_ping_body'), + ] + + operations = [ + migrations.AddField( + model_name='check', + name='last_ping_was_fail', + field=models.NullBooleanField(default=False), + ), + migrations.AddField( + model_name='ping', + name='fail', + field=models.NullBooleanField(default=False), + ), + ] diff --git a/hc/api/models.py b/hc/api/models.py index ac684940..efe154f1 100644 --- a/hc/api/models.py +++ b/hc/api/models.py @@ -27,6 +27,8 @@ DEFAULT_GRACE = td(hours=1) CHECK_KINDS = (("simple", "Simple"), ("cron", "Cron")) +PING_KINDS = (("", "OK"), ("fail", "Fail")) + CHANNEL_KINDS = (("email", "Email"), ("webhook", "Webhook"), ("hipchat", "HipChat"), @@ -70,6 +72,7 @@ class Check(models.Model): tz = models.CharField(max_length=36, default="UTC") n_pings = models.IntegerField(default=0) last_ping = models.DateTimeField(null=True, blank=True) + last_ping_was_fail = models.NullBooleanField(default=False) has_confirmation_link = models.BooleanField(default=False) alert_after = models.DateTimeField(null=True, blank=True, editable=False) status = models.CharField(max_length=6, choices=STATUSES, default="new") @@ -121,6 +124,9 @@ class Check(models.Model): if self.status in ("new", "paused"): return self.status + if self.last_ping_was_fail: + return "down" + if now is None: now = timezone.now() @@ -129,6 +135,11 @@ class Check(models.Model): def get_alert_after(self): """ Return the datetime when check potentially goes down. """ + # For "fail" pings, sendalerts should the check right + # after receiving the ping, without waiting for the grace time: + if self.last_ping_was_fail: + return self.last_ping + return self.get_grace_start() + self.grace def in_grace_period(self): @@ -182,9 +193,10 @@ class Check(models.Model): return result - def ping(self, remote_addr, scheme, method, ua, body): + def ping(self, remote_addr, scheme, method, ua, body, is_fail=False): self.n_pings = models.F("n_pings") + 1 self.last_ping = timezone.now() + self.last_ping_was_fail = is_fail self.has_confirmation_link = "confirm" in str(body).lower() self.alert_after = self.get_alert_after() if self.status in ("new", "paused"): @@ -195,6 +207,7 @@ class Check(models.Model): ping = Ping(owner=self) ping.n = self.n_pings + ping.fail = is_fail ping.remote_addr = remote_addr ping.scheme = scheme ping.method = method @@ -209,6 +222,7 @@ class Ping(models.Model): n = models.IntegerField(null=True) owner = models.ForeignKey(Check, models.CASCADE) created = models.DateTimeField(auto_now_add=True) + fail = models.NullBooleanField(default=False) scheme = models.CharField(max_length=10, default="http") remote_addr = models.GenericIPAddressField(blank=True, null=True) method = models.CharField(max_length=10, blank=True) diff --git a/hc/api/tests/test_check_model.py b/hc/api/tests/test_check_model.py index b214b90f..0f28b346 100644 --- a/hc/api/tests/test_check_model.py +++ b/hc/api/tests/test_check_model.py @@ -87,3 +87,13 @@ class CheckModelTestCase(TestCase): d = check.to_dict() self.assertEqual(d["next_ping"], "2000-01-01T01:00:00+00:00") + + def test_status_checks_the_fail_flag(self): + check = Check() + check.status = "up" + check.last_ping = timezone.now() - timedelta(minutes=5) + check.last_ping_was_fail = True + + # The computed status should be "down" because last_ping_was_fail + # is set. + self.assertEqual(check.get_status(), "down") diff --git a/hc/api/tests/test_ping.py b/hc/api/tests/test_ping.py index eab31a9f..0697ff73 100644 --- a/hc/api/tests/test_ping.py +++ b/hc/api/tests/test_ping.py @@ -1,4 +1,5 @@ from django.test import Client, TestCase +from django.utils.timezone import now from hc.api.models import Check, Ping @@ -114,3 +115,24 @@ class PingTestCase(TestCase): self.check.refresh_from_db() self.assertTrue(self.check.has_confirmation_link) + + def test_ping_resets_fail_flag(self): + self.check.last_ping_was_fail = True + self.check.save() + + r = self.client.get("/ping/%s/" % self.check.code) + self.assertEqual(r.status_code, 200) + + self.check.refresh_from_db() + self.assertFalse(self.check.last_ping_was_fail) + + def test_fail_endpoint_works(self): + r = self.client.get("/ping/%s/fail" % self.check.code) + self.assertEqual(r.status_code, 200) + + self.check.refresh_from_db() + self.assertTrue(self.check.last_ping_was_fail) + self.assertTrue(self.check.alert_after <= now()) + + ping = Ping.objects.latest("id") + self.assertTrue(ping.fail) diff --git a/hc/api/urls.py b/hc/api/urls.py index fc1dceac..6fbec827 100644 --- a/hc/api/urls.py +++ b/hc/api/urls.py @@ -5,6 +5,9 @@ from hc.api import views urlpatterns = [ path('ping//', views.ping, name="hc-ping-slash"), path('ping/', views.ping, name="hc-ping"), + path('ping//fail', views.ping, {"is_fail": True}, + name="hc-fail"), + path('api/v1/checks/', views.checks), path('api/v1/checks/', views.update, name="hc-api-update"), path('api/v1/checks//pause', views.pause, name="hc-api-pause"), diff --git a/hc/api/views.py b/hc/api/views.py index 5df7d74c..609daa0d 100644 --- a/hc/api/views.py +++ b/hc/api/views.py @@ -18,7 +18,7 @@ from hc.lib.badges import check_signature, get_badge_svg @csrf_exempt @never_cache -def ping(request, code): +def ping(request, code, is_fail=False): check = get_object_or_404(Check, code=code) headers = request.META @@ -29,7 +29,7 @@ def ping(request, code): ua = headers.get("HTTP_USER_AGENT", "") body = request.body.decode() - check.ping(remote_addr, scheme, method, ua, body) + check.ping(remote_addr, scheme, method, ua, body, is_fail) response = HttpResponse("OK") response["Access-Control-Allow-Origin"] = "*" diff --git a/static/css/log.css b/static/css/log.css index f28ee467..485d2062 100644 --- a/static/css/log.css +++ b/static/css/log.css @@ -41,7 +41,7 @@ } } -#log .details span { +#log .details span.ua-body { font-family: "Lucida Console", Monaco, monospace; font-size: 11.7px; color: #888; @@ -72,4 +72,4 @@ #log .alert-info { font-size: 12px; -} \ No newline at end of file +} diff --git a/templates/front/log.html b/templates/front/log.html index cc1f012e..65c7d57e 100644 --- a/templates/front/log.html +++ b/templates/front/log.html @@ -39,11 +39,18 @@ + + {% if event.fail %} + Failure + {% else %} + OK + {% endif %} +
{% if event.scheme == "email" %} {{ event.ua }} - + {% if event.body %} - {{ event.body|trunc }} {% endif %} @@ -54,7 +61,7 @@ {% if event.remote_addr %} from {{ event.remote_addr }} {% endif %} - + {% if event.ua %} - {{ event.ua }} {% endif %} @@ -74,7 +81,7 @@ - + {% if event.channel.kind == "email" %} Sent email alert to {{ event.channel.value }} {% elif event.channel.kind == "slack" %} diff --git a/templates/front/ping_details.html b/templates/front/ping_details.html index 811884b4..22cededa 100644 --- a/templates/front/ping_details.html +++ b/templates/front/ping_details.html @@ -1,5 +1,9 @@ \ No newline at end of file +