mirror of
https://github.com/healthchecks/healthchecks.git
synced 2025-03-15 20:54:53 +00:00
parent
dd45c888a7
commit
524d1a7375
10 changed files with 149 additions and 111 deletions
|
@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file.
|
|||
- Add retries to the the email sending logic
|
||||
- Require confirmation codes (sent to email) before sensitive actions
|
||||
- Implement WebAuthn two-factor authentication
|
||||
- Implement badge mode (up/down vs up/late/down) selector (#282)
|
||||
|
||||
## v1.17.0 - 2020-10-14
|
||||
|
||||
|
|
|
@ -15,12 +15,15 @@ class BadgeTestCase(BaseTestCase):
|
|||
|
||||
sig = base64_hmac(str(self.project.badge_key), "foo", settings.SECRET_KEY)
|
||||
sig = sig[:8]
|
||||
self.svg_url = "/badge/%s/%s/foo.svg" % (self.project.badge_key, sig)
|
||||
self.json_url = "/badge/%s/%s/foo.json" % (self.project.badge_key, sig)
|
||||
|
||||
self.svg_url = "/badge/%s/%s-2/foo.svg" % (self.project.badge_key, sig)
|
||||
self.json_url = "/badge/%s/%s-2/foo.json" % (self.project.badge_key, sig)
|
||||
self.with_late_url = "/badge/%s/%s/foo.json" % (self.project.badge_key, sig)
|
||||
self.shields_url = "/badge/%s/%s-2/foo.shields" % (self.project.badge_key, sig)
|
||||
|
||||
def test_it_rejects_bad_signature(self):
|
||||
r = self.client.get("/badge/%s/12345678/foo.svg" % self.project.badge_key)
|
||||
assert r.status_code == 404
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
def test_it_returns_svg(self):
|
||||
r = self.client.get(self.svg_url)
|
||||
|
@ -37,52 +40,24 @@ class BadgeTestCase(BaseTestCase):
|
|||
self.assertEqual(r["Access-Control-Allow-Origin"], "*")
|
||||
|
||||
def test_it_handles_new(self):
|
||||
r = self.client.get(self.json_url)
|
||||
doc = r.json()
|
||||
self.assertEqual(doc["status"], "up")
|
||||
self.assertEqual(doc["total"], 1)
|
||||
self.assertEqual(doc["grace"], 0)
|
||||
self.assertEqual(doc["down"], 0)
|
||||
doc = self.client.get(self.json_url).json()
|
||||
self.assertEqual(doc, {"status": "up", "total": 1, "grace": 0, "down": 0})
|
||||
|
||||
def test_it_handles_started_but_down(self):
|
||||
def test_it_ignores_started_when_down(self):
|
||||
self.check.last_start = now()
|
||||
self.check.tags = "foo"
|
||||
self.check.status = "down"
|
||||
self.check.save()
|
||||
|
||||
r = self.client.get(self.json_url)
|
||||
doc = r.json()
|
||||
self.assertEqual(doc["status"], "down")
|
||||
self.assertEqual(doc["total"], 1)
|
||||
self.assertEqual(doc["grace"], 0)
|
||||
self.assertEqual(doc["down"], 1)
|
||||
doc = self.client.get(self.json_url).json()
|
||||
self.assertEqual(doc, {"status": "down", "total": 1, "grace": 0, "down": 1})
|
||||
|
||||
def test_it_shows_grace_badge(self):
|
||||
def test_it_treats_late_as_up(self):
|
||||
self.check.last_ping = now() - td(days=1, minutes=10)
|
||||
self.check.tags = "foo"
|
||||
self.check.status = "up"
|
||||
self.check.save()
|
||||
|
||||
r = self.client.get(self.json_url)
|
||||
doc = r.json()
|
||||
self.assertEqual(doc["status"], "late")
|
||||
self.assertEqual(doc["total"], 1)
|
||||
self.assertEqual(doc["grace"], 1)
|
||||
self.assertEqual(doc["down"], 0)
|
||||
|
||||
def test_it_shows_started_but_grace_badge(self):
|
||||
self.check.last_start = now()
|
||||
self.check.last_ping = now() - td(days=1, minutes=10)
|
||||
self.check.tags = "foo"
|
||||
self.check.status = "up"
|
||||
self.check.save()
|
||||
|
||||
r = self.client.get(self.json_url)
|
||||
doc = r.json()
|
||||
self.assertEqual(doc["status"], "late")
|
||||
self.assertEqual(doc["total"], 1)
|
||||
self.assertEqual(doc["grace"], 1)
|
||||
self.assertEqual(doc["down"], 0)
|
||||
doc = self.client.get(self.json_url).json()
|
||||
self.assertEqual(doc, {"status": "up", "total": 1, "grace": 1, "down": 0})
|
||||
|
||||
def test_it_handles_special_characters(self):
|
||||
self.check.tags = "db@dc1"
|
||||
|
@ -94,3 +69,24 @@ class BadgeTestCase(BaseTestCase):
|
|||
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
def test_late_mode_returns_late_status(self):
|
||||
self.check.last_ping = now() - td(days=1, minutes=10)
|
||||
self.check.status = "up"
|
||||
self.check.save()
|
||||
|
||||
doc = self.client.get(self.with_late_url).json()
|
||||
self.assertEqual(doc, {"status": "late", "total": 1, "grace": 1, "down": 0})
|
||||
|
||||
def test_late_mode_ignores_started_when_late(self):
|
||||
self.check.last_start = now()
|
||||
self.check.last_ping = now() - td(days=1, minutes=10)
|
||||
self.check.status = "up"
|
||||
self.check.save()
|
||||
|
||||
doc = self.client.get(self.with_late_url).json()
|
||||
self.assertEqual(doc, {"status": "late", "total": 1, "grace": 1, "down": 0})
|
||||
|
||||
def test_it_returns_shields_json(self):
|
||||
doc = self.client.get(self.shields_url).json()
|
||||
self.assertEqual(doc, {"label": "foo", "message": "up", "color": "success"})
|
||||
|
|
|
@ -375,11 +375,15 @@ def flips_by_unique_key(request, unique_key):
|
|||
|
||||
@never_cache
|
||||
@cors("GET")
|
||||
def badge(request, badge_key, signature, tag, fmt="svg"):
|
||||
if not check_signature(badge_key, tag, signature):
|
||||
def badge(request, badge_key, signature, tag, fmt):
|
||||
if fmt not in ("svg", "json", "shields"):
|
||||
return HttpResponseNotFound()
|
||||
|
||||
if fmt not in ("svg", "json", "shields"):
|
||||
with_late = True
|
||||
if len(signature) == 10 and signature.endswith("-2"):
|
||||
with_late = False
|
||||
|
||||
if not check_signature(badge_key, tag, signature):
|
||||
return HttpResponseNotFound()
|
||||
|
||||
q = Check.objects.filter(project__badge_key=badge_key)
|
||||
|
@ -406,7 +410,7 @@ def badge(request, badge_key, signature, tag, fmt="svg"):
|
|||
break
|
||||
elif check_status == "grace":
|
||||
grace += 1
|
||||
if status == "up":
|
||||
if status == "up" and with_late:
|
||||
status = "late"
|
||||
|
||||
if fmt == "shields":
|
||||
|
|
|
@ -676,14 +676,18 @@ def badges(request, code):
|
|||
sorted_tags = sorted(tags, key=lambda s: s.lower())
|
||||
sorted_tags.append("*") # For the "overall status" badge
|
||||
|
||||
key = project.badge_key
|
||||
urls = []
|
||||
for tag in sorted_tags:
|
||||
urls.append(
|
||||
{
|
||||
"tag": tag,
|
||||
"svg": get_badge_url(project.badge_key, tag),
|
||||
"json": get_badge_url(project.badge_key, tag, fmt="json"),
|
||||
"shields": get_badge_url(project.badge_key, tag, fmt="shields"),
|
||||
"svg": get_badge_url(key, tag),
|
||||
"svg3": get_badge_url(key, tag, with_late=True),
|
||||
"json": get_badge_url(key, tag, fmt="json"),
|
||||
"json3": get_badge_url(key, tag, fmt="json", with_late=True),
|
||||
"shields": get_badge_url(key, tag, fmt="shields"),
|
||||
"shields3": get_badge_url(key, tag, fmt="shields", with_late=True),
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -99,16 +99,17 @@ def get_badge_svg(tag, status):
|
|||
|
||||
def check_signature(username, tag, sig):
|
||||
ours = base64_hmac(str(username), tag, settings.SECRET_KEY)
|
||||
ours = ours[:8]
|
||||
return ours == sig
|
||||
return ours[:8] == sig[:8]
|
||||
|
||||
|
||||
def get_badge_url(username, tag, fmt="svg"):
|
||||
sig = base64_hmac(str(username), tag, settings.SECRET_KEY)
|
||||
def get_badge_url(username, tag, fmt="svg", with_late=False):
|
||||
sig = base64_hmac(str(username), tag, settings.SECRET_KEY)[:8]
|
||||
if not with_late:
|
||||
sig += "-2"
|
||||
|
||||
if tag == "*":
|
||||
url = reverse("hc-badge-all", args=[username, sig[:8], fmt])
|
||||
url = reverse("hc-badge-all", args=[username, sig, fmt])
|
||||
else:
|
||||
url = reverse("hc-badge", args=[username, sig[:8], tag, fmt])
|
||||
url = reverse("hc-badge", args=[username, sig, tag, fmt])
|
||||
|
||||
return settings.SITE_ROOT + url
|
||||
|
|
16
static/css/badges.css
Normal file
16
static/css/badges.css
Normal file
|
@ -0,0 +1,16 @@
|
|||
.table.badge-preview th {
|
||||
border-top: 0;
|
||||
color: #777777;
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
padding-top: 32px;
|
||||
}
|
||||
|
||||
#badges-json .fetch-json {
|
||||
background: #eee;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
#badges-json, #badges-shields, .badge-preview .with-late {
|
||||
display: none;
|
||||
}
|
|
@ -25,28 +25,6 @@
|
|||
background-color: #ffebea;
|
||||
}
|
||||
|
||||
.table.badges th {
|
||||
border-top: 0;
|
||||
color: #777777;
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
padding-top: 32px;
|
||||
}
|
||||
|
||||
#badges-json, #badges-shields {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#badges-shields label:first-child {
|
||||
margin: 20px 0 10px 0;
|
||||
}
|
||||
|
||||
.json-response code {
|
||||
display: inline-block;
|
||||
background: #eee;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.invite-suggestion {
|
||||
color: #888;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
$(function() {
|
||||
|
||||
$(".json-response").each(function(idx, el) {
|
||||
$(".fetch-json").each(function(idx, el) {
|
||||
$.getJSON(el.dataset.url, function(data) {
|
||||
el.innerHTML = "<code>" + JSON.stringify(data) + "</code>";
|
||||
el.innerText = JSON.stringify(data);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -23,4 +23,15 @@ $(function() {
|
|||
$("#badges-json").hide();
|
||||
$("#badges-shields").show();
|
||||
})
|
||||
|
||||
$("#show-with-late").click(function() {
|
||||
$(".no-late").hide();
|
||||
$(".with-late").show();
|
||||
})
|
||||
|
||||
$("#show-no-late").click(function() {
|
||||
$(".with-late").hide();
|
||||
$(".no-late").show();
|
||||
})
|
||||
|
||||
});
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
<link rel="stylesheet" href="{% static 'css/add_project_modal.css' %}" type="text/css">
|
||||
<link rel="stylesheet" href="{% static 'css/add_pushover.css' %}" type="text/css">
|
||||
<link rel="stylesheet" href="{% static 'css/webhook_form.css' %}" type="text/css">
|
||||
<link rel="stylesheet" href="{% static 'css/badges.css' %}" type="text/css">
|
||||
<link rel="stylesheet" href="{% static 'css/base.css' %}" type="text/css">
|
||||
<link rel="stylesheet" href="{% static 'css/billing.css' %}" type="text/css">
|
||||
<link rel="stylesheet" href="{% static 'css/channel_checks.css' %}" type="text/css">
|
||||
|
|
|
@ -10,15 +10,27 @@
|
|||
|
||||
<p id="badges-description">
|
||||
{{ site_name }} provides status badges for each of the tags
|
||||
you have used. Additionally, the "{{ site_name }}"
|
||||
badge shows the overall status of all checks in a
|
||||
project. The badges have public, but hard-to-guess
|
||||
you have used. The badges have public, but hard-to-guess
|
||||
URLs. You can use them in your READMEs,
|
||||
dashboards or status pages.
|
||||
</p>
|
||||
|
||||
<div id="b-format" class="btn-group" data-toggle="buttons">
|
||||
<label id="show-svg" class="btn btn-default active">
|
||||
<p>Each badge can be in one of the following states:</p>
|
||||
<ul>
|
||||
<li><strong>up</strong> – all matching checks are up.</li>
|
||||
<li><strong>down</strong> – at least one check is currently down.</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
As an option, the badges can report a third state:
|
||||
<strong>late</strong> (when at least one check is running late but has not
|
||||
exceeded its grace time yet).
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
||||
<div class="btn-group" data-toggle="buttons">
|
||||
<label id="show-svg" class="btn btn-default active" data->
|
||||
<input type="radio" autocomplete="off" checked> SVG
|
||||
</label>
|
||||
<label id="show-json" class="btn btn-default">
|
||||
|
@ -28,8 +40,42 @@
|
|||
<input type="radio" autocomplete="off"> Shields.io
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
<table id="badges-svg" class="badges table">
|
||||
<div class="btn-group" data-toggle="buttons">
|
||||
<label id="show-no-late" class="btn btn-default active">
|
||||
<input type="radio" autocomplete="off" checked> Badge states: <b>up</b> or <b>down</b>
|
||||
</label>
|
||||
<label id="show-with-late" class="btn btn-default">
|
||||
<input type="radio" autocomplete="off"> Badge states: <b>up</b>, <b>late</b> or <b>down</b>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<table id="badges-svg" class="table badge-preview">
|
||||
{% if have_tags %}
|
||||
<tr><th colspan="2">Tags</th></tr>
|
||||
{% endif %}
|
||||
|
||||
{% for urldict in badges %}
|
||||
{% if urldict.tag == "*" %}
|
||||
<tr>
|
||||
<th colspan="2">Overall Status</th>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<img class="no-late" src="{{ urldict.svg }}" alt="" />
|
||||
<img class="with-late" src="{{ urldict.svg3 }}" alt="" />
|
||||
</td>
|
||||
<td>
|
||||
<code class="no-late">{{ urldict.svg }}</code>
|
||||
<code class="with-late">{{ urldict.svg3 }}</code>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<table id="badges-json" class="table badge-preview">
|
||||
{% if have_tags %}
|
||||
<tr>
|
||||
<th colspan="2">Tags</th>
|
||||
|
@ -45,40 +91,18 @@
|
|||
|
||||
<tr>
|
||||
<td>
|
||||
<img src="{{ urldict.svg }}" alt="" />
|
||||
<code class="fetch-json no-late" data-url="{{ urldict.json }}"></code>
|
||||
<code class="fetch-json with-late" data-url="{{ urldict.json3 }}"></code>
|
||||
</td>
|
||||
<td>
|
||||
<code>{{ urldict.svg }}</code>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<table id="badges-json" class="badges table">
|
||||
{% if have_tags %}
|
||||
<tr>
|
||||
<th colspan="2">Tags</th>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
{% for urldict in badges %}
|
||||
{% if urldict.tag == "*" %}
|
||||
<tr>
|
||||
<th colspan="2">Overall Status</th>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
<tr>
|
||||
<td class="json-response" data-url="{{ urldict.json }}">
|
||||
</td>
|
||||
<td>
|
||||
<code>{{ urldict.json }}</code>
|
||||
<code class="no-late">{{ urldict.json }}</code>
|
||||
<code class="with-late">{{ urldict.json3 }}</code>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
<div id="badges-shields">
|
||||
<table class="badges table">
|
||||
<table id="badges-shields" class="table badge-preview">
|
||||
{% if have_tags %}
|
||||
<tr>
|
||||
<th>Shields.io badge</th>
|
||||
|
@ -95,10 +119,12 @@
|
|||
|
||||
<tr>
|
||||
<td>
|
||||
<img src="https://img.shields.io/endpoint?url={{ urldict.shields|urlencode:"" }}" alt="" />
|
||||
<img class="no-late" src="https://img.shields.io/endpoint?url={{ urldict.shields|urlencode:"" }}" alt="" />
|
||||
<img class="with-late" src="https://img.shields.io/endpoint?url={{ urldict.shields3|urlencode:"" }}" alt="" />
|
||||
</td>
|
||||
<td>
|
||||
<code>{{ urldict.shields }}</code>
|
||||
<code class="no-late">{{ urldict.shields }}</code>
|
||||
<code class="with-late">{{ urldict.shields3 }}</code>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
|
Loading…
Reference in a new issue