forked from GithubBackups/healthchecks
Foundation for "fail" pings (cc: #151)
This commit is contained in:
parent
7f4a568bea
commit
3fc84ca0ff
23
hc/api/migrations/0040_auto_20180517_1336.py
Normal file
23
hc/api/migrations/0040_auto_20180517_1336.py
Normal file
@ -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),
|
||||||
|
),
|
||||||
|
]
|
@ -27,6 +27,8 @@ DEFAULT_GRACE = td(hours=1)
|
|||||||
CHECK_KINDS = (("simple", "Simple"),
|
CHECK_KINDS = (("simple", "Simple"),
|
||||||
("cron", "Cron"))
|
("cron", "Cron"))
|
||||||
|
|
||||||
|
PING_KINDS = (("", "OK"), ("fail", "Fail"))
|
||||||
|
|
||||||
CHANNEL_KINDS = (("email", "Email"),
|
CHANNEL_KINDS = (("email", "Email"),
|
||||||
("webhook", "Webhook"),
|
("webhook", "Webhook"),
|
||||||
("hipchat", "HipChat"),
|
("hipchat", "HipChat"),
|
||||||
@ -70,6 +72,7 @@ class Check(models.Model):
|
|||||||
tz = models.CharField(max_length=36, default="UTC")
|
tz = models.CharField(max_length=36, default="UTC")
|
||||||
n_pings = models.IntegerField(default=0)
|
n_pings = models.IntegerField(default=0)
|
||||||
last_ping = models.DateTimeField(null=True, blank=True)
|
last_ping = models.DateTimeField(null=True, blank=True)
|
||||||
|
last_ping_was_fail = models.NullBooleanField(default=False)
|
||||||
has_confirmation_link = models.BooleanField(default=False)
|
has_confirmation_link = models.BooleanField(default=False)
|
||||||
alert_after = models.DateTimeField(null=True, blank=True, editable=False)
|
alert_after = models.DateTimeField(null=True, blank=True, editable=False)
|
||||||
status = models.CharField(max_length=6, choices=STATUSES, default="new")
|
status = models.CharField(max_length=6, choices=STATUSES, default="new")
|
||||||
@ -121,6 +124,9 @@ class Check(models.Model):
|
|||||||
if self.status in ("new", "paused"):
|
if self.status in ("new", "paused"):
|
||||||
return self.status
|
return self.status
|
||||||
|
|
||||||
|
if self.last_ping_was_fail:
|
||||||
|
return "down"
|
||||||
|
|
||||||
if now is None:
|
if now is None:
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
|
|
||||||
@ -129,6 +135,11 @@ class Check(models.Model):
|
|||||||
def get_alert_after(self):
|
def get_alert_after(self):
|
||||||
""" Return the datetime when check potentially goes down. """
|
""" 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
|
return self.get_grace_start() + self.grace
|
||||||
|
|
||||||
def in_grace_period(self):
|
def in_grace_period(self):
|
||||||
@ -182,9 +193,10 @@ class Check(models.Model):
|
|||||||
|
|
||||||
return result
|
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.n_pings = models.F("n_pings") + 1
|
||||||
self.last_ping = timezone.now()
|
self.last_ping = timezone.now()
|
||||||
|
self.last_ping_was_fail = is_fail
|
||||||
self.has_confirmation_link = "confirm" in str(body).lower()
|
self.has_confirmation_link = "confirm" in str(body).lower()
|
||||||
self.alert_after = self.get_alert_after()
|
self.alert_after = self.get_alert_after()
|
||||||
if self.status in ("new", "paused"):
|
if self.status in ("new", "paused"):
|
||||||
@ -195,6 +207,7 @@ class Check(models.Model):
|
|||||||
|
|
||||||
ping = Ping(owner=self)
|
ping = Ping(owner=self)
|
||||||
ping.n = self.n_pings
|
ping.n = self.n_pings
|
||||||
|
ping.fail = is_fail
|
||||||
ping.remote_addr = remote_addr
|
ping.remote_addr = remote_addr
|
||||||
ping.scheme = scheme
|
ping.scheme = scheme
|
||||||
ping.method = method
|
ping.method = method
|
||||||
@ -209,6 +222,7 @@ class Ping(models.Model):
|
|||||||
n = models.IntegerField(null=True)
|
n = models.IntegerField(null=True)
|
||||||
owner = models.ForeignKey(Check, models.CASCADE)
|
owner = models.ForeignKey(Check, models.CASCADE)
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
fail = models.NullBooleanField(default=False)
|
||||||
scheme = models.CharField(max_length=10, default="http")
|
scheme = models.CharField(max_length=10, default="http")
|
||||||
remote_addr = models.GenericIPAddressField(blank=True, null=True)
|
remote_addr = models.GenericIPAddressField(blank=True, null=True)
|
||||||
method = models.CharField(max_length=10, blank=True)
|
method = models.CharField(max_length=10, blank=True)
|
||||||
|
@ -87,3 +87,13 @@ class CheckModelTestCase(TestCase):
|
|||||||
|
|
||||||
d = check.to_dict()
|
d = check.to_dict()
|
||||||
self.assertEqual(d["next_ping"], "2000-01-01T01:00:00+00:00")
|
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")
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from django.test import Client, TestCase
|
from django.test import Client, TestCase
|
||||||
|
from django.utils.timezone import now
|
||||||
|
|
||||||
from hc.api.models import Check, Ping
|
from hc.api.models import Check, Ping
|
||||||
|
|
||||||
@ -114,3 +115,24 @@ class PingTestCase(TestCase):
|
|||||||
|
|
||||||
self.check.refresh_from_db()
|
self.check.refresh_from_db()
|
||||||
self.assertTrue(self.check.has_confirmation_link)
|
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)
|
||||||
|
@ -5,6 +5,9 @@ from hc.api import views
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('ping/<uuid:code>/', views.ping, name="hc-ping-slash"),
|
path('ping/<uuid:code>/', views.ping, name="hc-ping-slash"),
|
||||||
path('ping/<uuid:code>', views.ping, name="hc-ping"),
|
path('ping/<uuid:code>', views.ping, name="hc-ping"),
|
||||||
|
path('ping/<uuid:code>/fail', views.ping, {"is_fail": True},
|
||||||
|
name="hc-fail"),
|
||||||
|
|
||||||
path('api/v1/checks/', views.checks),
|
path('api/v1/checks/', views.checks),
|
||||||
path('api/v1/checks/<uuid:code>', views.update, name="hc-api-update"),
|
path('api/v1/checks/<uuid:code>', views.update, name="hc-api-update"),
|
||||||
path('api/v1/checks/<uuid:code>/pause', views.pause, name="hc-api-pause"),
|
path('api/v1/checks/<uuid:code>/pause', views.pause, name="hc-api-pause"),
|
||||||
|
@ -18,7 +18,7 @@ from hc.lib.badges import check_signature, get_badge_svg
|
|||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
@never_cache
|
@never_cache
|
||||||
def ping(request, code):
|
def ping(request, code, is_fail=False):
|
||||||
check = get_object_or_404(Check, code=code)
|
check = get_object_or_404(Check, code=code)
|
||||||
|
|
||||||
headers = request.META
|
headers = request.META
|
||||||
@ -29,7 +29,7 @@ def ping(request, code):
|
|||||||
ua = headers.get("HTTP_USER_AGENT", "")
|
ua = headers.get("HTTP_USER_AGENT", "")
|
||||||
body = request.body.decode()
|
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 = HttpResponse("OK")
|
||||||
response["Access-Control-Allow-Origin"] = "*"
|
response["Access-Control-Allow-Origin"] = "*"
|
||||||
|
@ -41,7 +41,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#log .details span {
|
#log .details span.ua-body {
|
||||||
font-family: "Lucida Console", Monaco, monospace;
|
font-family: "Lucida Console", Monaco, monospace;
|
||||||
font-size: 11.7px;
|
font-size: 11.7px;
|
||||||
color: #888;
|
color: #888;
|
||||||
|
@ -39,11 +39,18 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="date"></td>
|
<td class="date"></td>
|
||||||
<td class="time"></td>
|
<td class="time"></td>
|
||||||
|
<td class="text-right">
|
||||||
|
{% if event.fail %}
|
||||||
|
<span class="label label-danger">Failure</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="label label-success">OK</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
<td class="details">
|
<td class="details">
|
||||||
<div>
|
<div>
|
||||||
{% if event.scheme == "email" %}
|
{% if event.scheme == "email" %}
|
||||||
{{ event.ua }}
|
{{ event.ua }}
|
||||||
<span>
|
<span class="ua-body">
|
||||||
{% if event.body %}
|
{% if event.body %}
|
||||||
- {{ event.body|trunc }}
|
- {{ event.body|trunc }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -54,7 +61,7 @@
|
|||||||
{% if event.remote_addr %}
|
{% if event.remote_addr %}
|
||||||
from {{ event.remote_addr }}
|
from {{ event.remote_addr }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span>
|
<span class="ua-body">
|
||||||
{% if event.ua %}
|
{% if event.ua %}
|
||||||
- {{ event.ua }}
|
- {{ event.ua }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -74,7 +81,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="date"></td>
|
<td class="date"></td>
|
||||||
<td class="time"></td>
|
<td class="time"></td>
|
||||||
<td class="alert-info">
|
<td class="alert-info" colspan="2">
|
||||||
{% if event.channel.kind == "email" %}
|
{% if event.channel.kind == "email" %}
|
||||||
Sent email alert to {{ event.channel.value }}
|
Sent email alert to {{ event.channel.value }}
|
||||||
{% elif event.channel.kind == "slack" %}
|
{% elif event.channel.kind == "slack" %}
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<h3>Ping #{{ ping.n }}</h3>
|
<h3>Ping #{{ ping.n }}
|
||||||
|
{% if ping.fail %}
|
||||||
|
<span class="text-danger">(received via the <code>/fail</code> endpoint)</span>
|
||||||
|
{% endif %}
|
||||||
|
</h3>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
@ -49,9 +53,9 @@
|
|||||||
<span class="ua">{{ ping.ua }}</span>
|
<span class="ua">{{ ping.ua }}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if ping.body %}
|
{% if ping.body %}
|
||||||
<h4>Request Body</h4>
|
<h4>Request Body</h4>
|
||||||
<pre>{{ ping.body }}</pre>
|
<pre>{{ ping.body }}</pre>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user