forked from GithubBackups/healthchecks
Experimental Prometheus metrics endpoint. cc: #300
This commit is contained in:
parent
0ff4bd01e0
commit
12b946acf3
@ -1,10 +1,11 @@
|
||||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## v1.14.0 - Unreleased
|
||||
## v1.14.0-dev - Unreleased
|
||||
|
||||
### Improvements
|
||||
- Improved UI to invite users from account's other projects (#258)
|
||||
- Experimental Prometheus metrics endpoint (#300)
|
||||
|
||||
### Bug Fixes
|
||||
- The "render_docs" command checks if markdown and pygments is installed (#329)
|
||||
|
@ -199,6 +199,11 @@ class Check(models.Model):
|
||||
codes = self.channel_set.order_by("code").values_list("code", flat=True)
|
||||
return ",".join(map(str, codes))
|
||||
|
||||
@property
|
||||
def unique_key(self):
|
||||
code_half = self.code.hex[:16]
|
||||
return hashlib.sha1(code_half.encode()).hexdigest()
|
||||
|
||||
def to_dict(self, readonly=False):
|
||||
|
||||
result = {
|
||||
@ -216,8 +221,7 @@ class Check(models.Model):
|
||||
result["last_duration"] = int(self.last_duration.total_seconds())
|
||||
|
||||
if readonly:
|
||||
code_half = self.code.hex[:16]
|
||||
result["unique_key"] = hashlib.sha1(code_half.encode()).hexdigest()
|
||||
result["unique_key"] = self.unique_key
|
||||
else:
|
||||
update_rel_url = reverse("hc-api-update", args=[self.code])
|
||||
pause_rel_url = reverse("hc-api-pause", args=[self.code])
|
||||
|
43
hc/front/tests/test_metrics.py
Normal file
43
hc/front/tests/test_metrics.py
Normal file
@ -0,0 +1,43 @@
|
||||
from hc.api.models import Check
|
||||
from hc.test import BaseTestCase
|
||||
|
||||
|
||||
class MetricsTestCase(BaseTestCase):
|
||||
def setUp(self):
|
||||
super(MetricsTestCase, self).setUp()
|
||||
self.project.api_key_readonly = "R" * 32
|
||||
self.project.save()
|
||||
|
||||
self.check = Check(project=self.project, name="Alice Was Here")
|
||||
self.check.tags = "foo"
|
||||
self.check.save()
|
||||
|
||||
key = "R" * 32
|
||||
self.url = "/projects/%s/checks/metrics/?api_key=%s" % (self.project.code, key)
|
||||
|
||||
def test_it_works(self):
|
||||
r = self.client.get(self.url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertContains(r, 'name="Alice Was Here"')
|
||||
self.assertContains(r, 'tags="foo"')
|
||||
self.assertContains(r, 'tag="foo"')
|
||||
self.assertContains(r, "hc_checks_total 1")
|
||||
|
||||
def test_it_escapes_newline(self):
|
||||
self.check.name = "Line 1\nLine2"
|
||||
self.check.tags = "A\\C"
|
||||
self.check.save()
|
||||
|
||||
r = self.client.get(self.url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertContains(r, "Line 1\\nLine2")
|
||||
self.assertContains(r, "A\\\\C")
|
||||
|
||||
def test_it_checks_api_key_length(self):
|
||||
r = self.client.get(self.url + "R")
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
def test_it_checks_api_key(self):
|
||||
url = "/projects/%s/checks/metrics/?api_key=%s" % (self.project.code, "X" * 32)
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 403)
|
@ -70,6 +70,7 @@ urlpatterns = [
|
||||
path("projects/<uuid:code>/checks/add/", views.add_check, name="hc-add-check"),
|
||||
path("checks/cron_preview/", views.cron_preview),
|
||||
path("projects/<uuid:code>/checks/status/", views.status, name="hc-status"),
|
||||
path("projects/<uuid:code>/checks/metrics/", views.metrics, name="hc-metrics"),
|
||||
path("checks/<uuid:code>/", include(check_urls)),
|
||||
path("integrations/", include(channel_urls)),
|
||||
path("docs/", views.serve_doc, name="hc-docs"),
|
||||
|
@ -1540,3 +1540,50 @@ def add_msteams(request):
|
||||
|
||||
ctx = {"page": "channels", "project": request.project, "form": form}
|
||||
return render(request, "integrations/add_msteams.html", ctx)
|
||||
|
||||
|
||||
def metrics(request, code):
|
||||
api_key = request.GET.get("api_key", "")
|
||||
if len(api_key) != 32:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
q = Project.objects.filter(code=code, api_key_readonly=api_key)
|
||||
try:
|
||||
project = q.get()
|
||||
except Project.DoesNotExist:
|
||||
return HttpResponseForbidden()
|
||||
|
||||
checks = Check.objects.filter(project_id=project.id).order_by("id")
|
||||
|
||||
def esc(s):
|
||||
return s.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
|
||||
|
||||
def output(checks):
|
||||
yield "# HELP hc_check_up Whether the check is currently up (1 for yes, 0 for no).\n"
|
||||
yield "# TYPE hc_check_up gauge\n"
|
||||
|
||||
TMPL = """hc_check_up{name="%s", tags="%s", unique_key="%s"} %d\n"""
|
||||
for check in checks:
|
||||
value = 0 if check.get_status(with_started=False) == "down" else 1
|
||||
yield TMPL % (esc(check.name), esc(check.tags), check.unique_key, value)
|
||||
|
||||
tags_statuses, num_down = _tags_statuses(checks)
|
||||
yield "\n"
|
||||
yield "# HELP hc_tag_up Whether all checks with this tag are up (1 for yes, 0 for no).\n"
|
||||
yield "# TYPE hc_tag_up gauge\n"
|
||||
TMPL = """hc_tag_up{tag="%s"} %d\n"""
|
||||
for tag in sorted(tags_statuses):
|
||||
value = 0 if tags_statuses[tag] == "down" else 1
|
||||
yield TMPL % (esc(tag), value)
|
||||
|
||||
yield "\n"
|
||||
yield "# HELP hc_checks_total The total number of checks.\n"
|
||||
yield "# TYPE hc_checks_total gauge\n"
|
||||
yield "hc_checks_total %d\n" % len(checks)
|
||||
yield "\n"
|
||||
|
||||
yield "# HELP hc_checks_down_total The number of checks currently down.\n"
|
||||
yield "# TYPE hc_checks_down_total gauge\n"
|
||||
yield "hc_checks_down_total %d\n" % num_down
|
||||
|
||||
return HttpResponse(output(checks), content_type="text/plain")
|
||||
|
@ -43,6 +43,10 @@
|
||||
API key (read-only): <br />
|
||||
<code>{{ project.api_key_readonly }}</code>
|
||||
</p>
|
||||
<p>
|
||||
Prometheus metrics endpoint:
|
||||
<a href="{% url 'hc-metrics' project.code %}?api_key={{ project.api_key_readonly }}">here</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<button
|
||||
data-toggle="modal"
|
||||
|
Loading…
x
Reference in New Issue
Block a user