From edbfd4b437a13da0d15a0b1910a34e413fdf674d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Caune?= Date: Sun, 26 Apr 2020 17:45:50 +0300 Subject: [PATCH] Added /api/v1/metrics/ endpoint, useful for monitoring the service itself --- CHANGELOG.md | 1 + hc/api/tests/test_metrics.py | 43 ++++++++++++++++++++++++++++++++++++ hc/api/tests/test_status.py | 12 ++++++++++ hc/api/urls.py | 1 + hc/api/views.py | 17 +++++++++++++- hc/settings.py | 1 + 6 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 hc/api/tests/test_metrics.py create mode 100644 hc/api/tests/test_status.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 2936923a..ca881c46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. - Users can edit their existing webhook integrations (#176) - Add a "Transfer Ownership" feature in Project Settings - In checks list, the pause button asks for confirmation (#356) +- Added /api/v1/metrics/ endpoint, useful for monitoring the service itself ### Bug Fixes - "Get a single check" API call now supports read-only API keys (#346) diff --git a/hc/api/tests/test_metrics.py b/hc/api/tests/test_metrics.py new file mode 100644 index 00000000..72a34544 --- /dev/null +++ b/hc/api/tests/test_metrics.py @@ -0,0 +1,43 @@ +from django.test.utils import override_settings +from django.utils.timezone import now +from hc.api.models import Check, Flip, Ping +from hc.test import BaseTestCase + + +@override_settings(METRICS_KEY="foo") +class MetricsTestCase(BaseTestCase): + url = "/api/v1/metrics/" + + def test_it_returns_num_unprocessed_flips(self): + check = Check.objects.create(project=self.project, status="down") + flip = Flip(owner=check) + flip.created = now() + flip.old_status = "up" + flip.new_status = "down" + flip.save() + + r = self.client.get(self.url, HTTP_X_METRICS_KEY="foo") + self.assertEqual(r.status_code, 200) + + doc = r.json() + self.assertEqual(doc["num_unprocessed_flips"], 1) + + def test_it_returns_max_ping_id(self): + check = Check.objects.create(project=self.project, status="down") + Ping.objects.create(owner=check, n=1) + last_ping = Ping.objects.last() + + r = self.client.get(self.url, HTTP_X_METRICS_KEY="foo") + self.assertEqual(r.status_code, 200) + + doc = r.json() + self.assertEqual(doc["max_ping_id"], last_ping.id) + + @override_settings(METRICS_KEY=None) + def test_it_handles_unset_metrics_key(self): + r = self.client.get(self.url, HTTP_X_METRICS_KEY="foo") + self.assertEqual(r.status_code, 403) + + def test_it_handles_incorrect_metrics_key(self): + r = self.client.get(self.url, HTTP_X_METRICS_KEY="bar") + self.assertEqual(r.status_code, 403) diff --git a/hc/api/tests/test_status.py b/hc/api/tests/test_status.py new file mode 100644 index 00000000..e47f7d98 --- /dev/null +++ b/hc/api/tests/test_status.py @@ -0,0 +1,12 @@ +from hc.test import BaseTestCase + + +class StatusTestCase(BaseTestCase): + url = "/api/v1/status/" + + def test_it_works(self): + r = self.client.get(self.url) + self.assertEqual(r.status_code, 200) + + self.assertNumQueries(1) + self.assertEqual(r.content, b"OK") diff --git a/hc/api/urls.py b/hc/api/urls.py index 28050a25..bdd72182 100644 --- a/hc/api/urls.py +++ b/hc/api/urls.py @@ -37,5 +37,6 @@ urlpatterns = [ {"tag": "*"}, name="hc-badge-all", ), + path("api/v1/metrics/", views.metrics), path("api/v1/status/", views.status), ] diff --git a/hc/api/views.py b/hc/api/views.py index d3728e1a..662feddf 100644 --- a/hc/api/views.py +++ b/hc/api/views.py @@ -17,7 +17,7 @@ from django.views.decorators.http import require_POST from hc.api import schemas from hc.api.decorators import authorize, authorize_read, cors, validate_json -from hc.api.models import Check, Notification, Channel +from hc.api.models import Flip, Channel, Check, Notification, Ping from hc.lib.badges import check_signature, get_badge_svg @@ -320,6 +320,21 @@ def bounce(request, code): return HttpResponse() +def metrics(request): + if not settings.METRICS_KEY: + return HttpResponseForbidden() + + key = request.META.get("HTTP_X_METRICS_KEY") + if key != settings.METRICS_KEY: + return HttpResponseForbidden() + + doc = {} + doc["max_ping_id"] = Ping.objects.values_list("id", flat=True).last() + doc["num_unprocessed_flips"] = Flip.objects.filter(processed__isnull=True).count() + + return JsonResponse(doc) + + def status(request): with connection.cursor() as c: c.execute("SELECT 1") diff --git a/hc/settings.py b/hc/settings.py index b725da35..7662ea3a 100644 --- a/hc/settings.py +++ b/hc/settings.py @@ -28,6 +28,7 @@ def envint(s, default): SECRET_KEY = os.getenv("SECRET_KEY", "---") +METRICS_KEY = os.getenv("METRICS_KEY") DEBUG = envbool("DEBUG", "True") ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "*").split(",") DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", "healthchecks@example.org")