diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5932928d..139a88aa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file.
- Paused ping handling can be controlled via API (#376)
- Add "Get a list of checks's logged pings" API call (#371)
- The /api/v1/checks/ endpoint now accepts either UUID or `unique_key` (#370)
+- Added /api/v1/checks/uuid/flips/ endpoint (#349)
### Bug Fixes
diff --git a/hc/api/forms.py b/hc/api/forms.py
new file mode 100644
index 00000000..1d983b3f
--- /dev/null
+++ b/hc/api/forms.py
@@ -0,0 +1,28 @@
+from datetime import datetime as dt
+
+from django import forms
+from django.core.exceptions import ValidationError
+import pytz
+
+
+class TimestampField(forms.Field):
+ def to_python(self, value):
+ if value is None:
+ return None
+
+ try:
+ value_int = int(value)
+ except ValueError:
+ raise ValidationError(message="Must be an integer")
+
+ # 10000000000 is year 2286 (a sanity check)
+ if value_int < 0 or value_int > 10000000000:
+ raise ValidationError(message="Out of bounds")
+
+ return dt.fromtimestamp(value_int, pytz.UTC)
+
+
+class FlipsFiltersForm(forms.Form):
+ start = TimestampField(required=False)
+ end = TimestampField(required=False)
+ seconds = forms.IntegerField(required=False, min_value=0, max_value=31536000)
diff --git a/hc/api/models.py b/hc/api/models.py
index 3962a1ad..ba5aa13f 100644
--- a/hc/api/models.py
+++ b/hc/api/models.py
@@ -760,6 +760,12 @@ class Flip(models.Model):
)
]
+ def to_dict(self):
+ return {
+ "timestamp": isostring(self.created),
+ "up": 1 if self.new_status == "up" else 0,
+ }
+
def send_alerts(self):
if self.new_status == "up" and self.old_status in ("new", "paused"):
# Don't send alerts on new->up and paused->up transitions
diff --git a/hc/api/tests/test_get_flips.py b/hc/api/tests/test_get_flips.py
new file mode 100644
index 00000000..ac4b2ea6
--- /dev/null
+++ b/hc/api/tests/test_get_flips.py
@@ -0,0 +1,77 @@
+from datetime import timedelta as td
+from datetime import datetime as dt
+from datetime import timezone
+
+from hc.api.models import Check, Flip
+from hc.test import BaseTestCase
+
+
+class GetFlipsTestCase(BaseTestCase):
+ def setUp(self):
+ super(GetFlipsTestCase, self).setUp()
+
+ self.a1 = Check(project=self.project, name="Alice 1")
+ self.a1.timeout = td(seconds=3600)
+ self.a1.grace = td(seconds=900)
+ self.a1.n_pings = 0
+ self.a1.status = "new"
+ self.a1.tags = "a1-tag a1-additional-tag"
+ self.a1.desc = "This is description"
+ self.a1.save()
+
+ Flip.objects.create(
+ owner=self.a1,
+ created=dt(2020, 6, 1, 12, 24, 32, 123000, tzinfo=timezone.utc),
+ old_status="new",
+ new_status="up",
+ )
+
+ self.url = "/api/v1/checks/%s/flips/" % self.a1.code
+
+ def get(self, api_key="X" * 32, qs=""):
+ return self.client.get(self.url + qs, HTTP_X_API_KEY=api_key)
+
+ def test_it_works(self):
+
+ r = self.get()
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r["Access-Control-Allow-Origin"], "*")
+
+ doc = r.json()
+ self.assertEqual(len(doc["flips"]), 1)
+
+ flip = doc["flips"][0]
+ # Microseconds (123000) should be stripped out
+ self.assertEqual(flip["timestamp"], "2020-06-01T12:24:32+00:00")
+ self.assertEqual(flip["up"], 1)
+
+ def test_readonly_key_is_allowed(self):
+ self.project.api_key_readonly = "R" * 32
+ self.project.save()
+
+ r = self.get(api_key=self.project.api_key_readonly)
+ self.assertEqual(r.status_code, 200)
+
+ def test_it_rejects_post(self):
+ r = self.client.post(self.url, HTTP_X_API_KEY="X" * 32)
+ self.assertEqual(r.status_code, 405)
+
+ def test_it_rejects_non_integer_start(self):
+ r = self.get(qs="?start=abc")
+ self.assertEqual(r.status_code, 400)
+
+ def test_it_rejects_negative_start(self):
+ r = self.get(qs="?start=-123")
+ self.assertEqual(r.status_code, 400)
+
+ def test_it_rejects_huge_start(self):
+ r = self.get(qs="?start=12345678901234567890")
+ self.assertEqual(r.status_code, 400)
+
+ def test_it_rejects_negative_seconds(self):
+ r = self.get(qs="?seconds=-123")
+ self.assertEqual(r.status_code, 400)
+
+ def test_it_rejects_huge_seconds(self):
+ r = self.get(qs="?seconds=12345678901234567890")
+ self.assertEqual(r.status_code, 400)
diff --git a/hc/api/urls.py b/hc/api/urls.py
index 422ee154..09be5cfb 100644
--- a/hc/api/urls.py
+++ b/hc/api/urls.py
@@ -38,6 +38,8 @@ urlpatterns = [
path("api/v1/checks/
GET SITE_ROOT/api/v1/checks/<uuid>/pings/
GET SITE_ROOT/api/v1/checks/<uuid>/flips/
GET SITE_ROOT/api/v1/channels/
GET SITE_ROOT/api/v1/checks/<uuid>/flips/
+GET SITE_ROOT/api/v1/checks/<unique_key>/flips/
Returns a list of "flips" this check has experienced. A flip is a change of status +(from "down" to "up", or from "up" to "down").
+Returns the flips from the last value
seconds
Example:
+SITE_ROOT/api/v1/checks/<uuid|unique_key>/flips/?seconds=3600
Returns flips that are newer than the specified UNIX timestamp.
+Example:
+SITE_ROOT/api/v1/checks/<uuid|unique_key>/flips/?start=1592214380
Returns flips that are older than the specified UNIX timestamp.
+Example:
+SITE_ROOT/api/v1/checks/<uuid|unique_key>/flips/?end=1592217980
curl SITE_ROOT/api/v1/checks/f618072a-7bde-4eee-af63-71a77c5723bc/flips/ \
+ --header "X-Api-Key: your-api-key"
+
[
+ {
+ "timestamp": "2020-03-23T10:18:23+00:00",
+ "up": 1
+ },
+ {
+ "timestamp": "2020-03-23T10:17:15+00:00",
+ "up": 0
+ },
+ {
+ "timestamp": "2020-03-23T10:16:18+00:00",
+ "up": 1
+ }
+]
+
GET SITE_ROOT/api/v1/channels/
Returns a list of integrations belonging to the project.
diff --git a/templates/docs/api.md b/templates/docs/api.md index dd1ca5b5..631c8bec 100644 --- a/templates/docs/api.md +++ b/templates/docs/api.md @@ -14,7 +14,8 @@ Endpoint Name | Endpoint Address [Update an existing check](#update-check) | `POST SITE_ROOT/api/v1/checks/