forked from GithubBackups/healthchecks
Experimental Prometheus metrics endpoint. cc: #300
This commit is contained in:
parent
0ff4bd01e0
commit
12b946acf3
@ -1,10 +1,11 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
## v1.14.0 - Unreleased
|
## v1.14.0-dev - Unreleased
|
||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
- Improved UI to invite users from account's other projects (#258)
|
- Improved UI to invite users from account's other projects (#258)
|
||||||
|
- Experimental Prometheus metrics endpoint (#300)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
- The "render_docs" command checks if markdown and pygments is installed (#329)
|
- 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)
|
codes = self.channel_set.order_by("code").values_list("code", flat=True)
|
||||||
return ",".join(map(str, codes))
|
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):
|
def to_dict(self, readonly=False):
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
@ -216,8 +221,7 @@ class Check(models.Model):
|
|||||||
result["last_duration"] = int(self.last_duration.total_seconds())
|
result["last_duration"] = int(self.last_duration.total_seconds())
|
||||||
|
|
||||||
if readonly:
|
if readonly:
|
||||||
code_half = self.code.hex[:16]
|
result["unique_key"] = self.unique_key
|
||||||
result["unique_key"] = hashlib.sha1(code_half.encode()).hexdigest()
|
|
||||||
else:
|
else:
|
||||||
update_rel_url = reverse("hc-api-update", args=[self.code])
|
update_rel_url = reverse("hc-api-update", args=[self.code])
|
||||||
pause_rel_url = reverse("hc-api-pause", 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("projects/<uuid:code>/checks/add/", views.add_check, name="hc-add-check"),
|
||||||
path("checks/cron_preview/", views.cron_preview),
|
path("checks/cron_preview/", views.cron_preview),
|
||||||
path("projects/<uuid:code>/checks/status/", views.status, name="hc-status"),
|
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("checks/<uuid:code>/", include(check_urls)),
|
||||||
path("integrations/", include(channel_urls)),
|
path("integrations/", include(channel_urls)),
|
||||||
path("docs/", views.serve_doc, name="hc-docs"),
|
path("docs/", views.serve_doc, name="hc-docs"),
|
||||||
|
@ -1540,3 +1540,50 @@ def add_msteams(request):
|
|||||||
|
|
||||||
ctx = {"page": "channels", "project": request.project, "form": form}
|
ctx = {"page": "channels", "project": request.project, "form": form}
|
||||||
return render(request, "integrations/add_msteams.html", ctx)
|
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 />
|
API key (read-only): <br />
|
||||||
<code>{{ project.api_key_readonly }}</code>
|
<code>{{ project.api_key_readonly }}</code>
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
Prometheus metrics endpoint:
|
||||||
|
<a href="{% url 'hc-metrics' project.code %}?api_key={{ project.api_key_readonly }}">here</a>
|
||||||
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<button
|
<button
|
||||||
data-toggle="modal"
|
data-toggle="modal"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user