diff --git a/CHANGELOG.md b/CHANGELOG.md index bd9a34ee..9fff38e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ### Improvements - Paused ping handling can be controlled via API (#376) +- Add "Get a list of checks's logged pings" API call (#371) + ### Bug Fixes - Removing Pager Team integration, project appears to be discontinued diff --git a/hc/api/models.py b/hc/api/models.py index 59a97f95..3962a1ad 100644 --- a/hc/api/models.py +++ b/hc/api/models.py @@ -342,6 +342,17 @@ class Ping(models.Model): ua = models.CharField(max_length=200, blank=True) body = models.TextField(blank=True, null=True) + def to_dict(self): + return { + "type": self.kind or "success", + "date": self.created.isoformat(), + "n": self.n, + "scheme": self.scheme, + "remote_addr": self.remote_addr, + "method": self.method, + "ua": self.ua, + } + class Channel(models.Model): name = models.CharField(max_length=100, blank=True) diff --git a/hc/api/tests/test_get_pings.py b/hc/api/tests/test_get_pings.py new file mode 100644 index 00000000..d032e727 --- /dev/null +++ b/hc/api/tests/test_get_pings.py @@ -0,0 +1,58 @@ +from datetime import timedelta as td + +from hc.api.models import Check +from hc.test import BaseTestCase + + +class GetPingsTestCase(BaseTestCase): + def setUp(self): + super(GetPingsTestCase, 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() + + self.url = "/api/v1/checks/%s/pings/" % self.a1.code + + def get(self, api_key="X" * 32): + return self.client.get(self.url, HTTP_X_API_KEY=api_key) + + def test_it_works(self): + self.a1.ping( + remote_addr="1.2.3.4", + scheme="https", + method="get", + ua="foo-agent", + body="", + action=None, + ) + + r = self.get() + self.assertEqual(r.status_code, 200) + self.assertEqual(r["Access-Control-Allow-Origin"], "*") + + doc = r.json() + self.assertEqual(len(doc["pings"]), 1) + + ping = doc["pings"][0] + self.assertEqual(ping["n"], 1) + self.assertEqual(ping["remote_addr"], "1.2.3.4") + self.assertEqual(ping["scheme"], "https") + self.assertEqual(ping["method"], "get") + self.assertEqual(ping["ua"], "foo-agent") + + def test_readonly_key_is_not_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, 401) + + def test_it_rejects_post(self): + r = self.client.post(self.url, HTTP_X_API_KEY="X" * 32) + self.assertEqual(r.status_code, 405) diff --git a/hc/api/urls.py b/hc/api/urls.py index bdd72182..1316602a 100644 --- a/hc/api/urls.py +++ b/hc/api/urls.py @@ -25,6 +25,7 @@ urlpatterns = [ path("api/v1/checks/", views.single, name="hc-api-single"), path("api/v1/checks//pause", views.pause, name="hc-api-pause"), path("api/v1/notifications//bounce", views.bounce, name="hc-api-bounce"), + path("api/v1/checks//pings/", views.pings, name="hc-api-pings"), path("api/v1/channels/", views.channels), path( "badge///.", diff --git a/hc/api/views.py b/hc/api/views.py index 300ae376..d18b8aff 100644 --- a/hc/api/views.py +++ b/hc/api/views.py @@ -16,9 +16,10 @@ from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST +from hc.accounts.models import Profile from hc.api import schemas from hc.api.decorators import authorize, authorize_read, cors, validate_json -from hc.api.models import Flip, Channel, Check, Notification, Ping +from hc.api.models import MAX_DELTA, Flip, Channel, Check, Notification, Ping from hc.lib.badges import check_signature, get_badge_svg @@ -246,6 +247,39 @@ def pause(request, code): return JsonResponse(check.to_dict()) +@cors("GET") +@validate_json() +@authorize +def pings(request, code): + check = get_object_or_404(Check, code=code) + if check.project_id != request.project.id: + return HttpResponseForbidden() + + # Look up ping log limit from account's profile. + # There might be more pings in the database (depends on how pruning is handled) + # but we will not return more than the limit allows. + profile = Profile.objects.get(user__project=request.project) + limit = profile.ping_log_limit + + # Query in descending order so we're sure to get the most recent + # pings, regardless of the limit restriction + pings = Ping.objects.filter(owner=check).order_by("-id")[:limit] + + # Ascending order is more convenient for calculating duration, so use reverse() + prev, dicts = None, [] + for ping in reversed(pings): + d = ping.to_dict() + if ping.kind != "start" and prev and prev.kind == "start": + delta = ping.created - prev.created + if delta < MAX_DELTA: + d["duration"] = delta.total_seconds() + + dicts.insert(0, d) + prev = ping + + return JsonResponse({"pings": dicts}) + + @never_cache @cors("GET") def badge(request, badge_key, signature, tag, fmt="svg"): diff --git a/templates/docs/api.html b/templates/docs/api.html index faea2f74..c613369e 100644 --- a/templates/docs/api.html +++ b/templates/docs/api.html @@ -35,6 +35,10 @@ checks in user's account.

DELETE SITE_ROOT/api/v1/checks/<uuid> +Get a list of checks's logged pings +GET SITE_ROOT/api/v1/checks/<uuid>/pings/ + + Get a list of existing integrations GET SITE_ROOT/api/v1/channels/ @@ -56,11 +60,11 @@ an API key. You can create read-write and read-only API keys in the Regular API key -Have full access to all documented API endpoints. +Has full access to all documented API endpoints. Read-only API key -Only work with the Get a list of existing checks endpoint. Some fields are omitted from the API responses. +Only works with the Get a list of existing checks and Get a single check endpoints. Some fields are omitted from the API responses. @@ -599,9 +603,78 @@ check that was just deleted.

+

Get a list of checks's logged pings

+

GET SITE_ROOT/api/v1/checks/<uuid>/pings/

+

Returns a list of pings this check has received.

+

This endpoint returns pings in reverse order (most recent first), and the total +number of returned pings depends on account's billing plan: 100 for free accounts, +1000 for paid accounts.

+

Response Codes

+
+
200 OK
+
The request succeeded.
+
401 Unauthorized
+
The API key is either missing or invalid.
+
403 Forbidden
+
Access denied, wrong API key.
+
404 Not Found
+
The specified check does not exist.
+
+

Example Request

+
curl SITE_ROOT/api/v1/checks/f618072a-7bde-4eee-af63-71a77c5723bc/pings/ \
+    --header "X-Api-Key: your-api-key"
+
+ + +

Example Response

+
{
+  "pings": [
+    {
+      "type": "success",
+      "date": "2020-06-09T14:51:06.113073+00:00",
+      "n": 4,
+      "scheme": "http",
+      "remote_addr": "192.0.2.0",
+      "method": "GET",
+      "ua": "curl/7.68.0",
+      "duration": 2.896736
+    },
+    {
+      "type": "start",
+      "date": "2020-06-09T14:51:03.216337+00:00",
+      "n": 3,
+      "scheme": "http",
+      "remote_addr": "192.0.2.0",
+      "method": "GET",
+      "ua": "curl/7.68.0"
+    },
+    {
+      "type": "success",
+      "date": "2020-06-09T14:50:59.633577+00:00",
+      "n": 2,
+      "scheme": "http",
+      "remote_addr": "192.0.2.0",
+      "method": "GET",
+      "ua": "curl/7.68.0",
+      "duration": 2.997976
+    },
+    {
+      "type": "start",
+      "date": "2020-06-09T14:50:56.635601+00:00",
+      "n": 1,
+      "scheme": "http",
+      "remote_addr": "192.0.2.0",
+      "method": "GET",
+      "ua": "curl/7.68.0"
+    }
+  ]
+}
+
+ +

Get a List of Existing Integrations

GET SITE_ROOT/api/v1/channels/

-

Returns a list of integrations belonging to the user.

+

Returns a list of integrations belonging to the project.

Response Codes

200 OK
diff --git a/templates/docs/api.md b/templates/docs/api.md index 42923080..7f2c1222 100644 --- a/templates/docs/api.md +++ b/templates/docs/api.md @@ -8,11 +8,12 @@ checks in user's account. Endpoint Name | Endpoint Address ------------------------------------------------------|------- [Get a list of existing checks](#list-checks) | `GET SITE_ROOT/api/v1/checks/` -[Get a single check](#get-check) | `GET SITE_ROOT/api/v1/checks/` +[Get a single check](#get-check) | `GET SITE_ROOT/api/v1/checks/` [Create a new check](#create-check) | `POST SITE_ROOT/api/v1/checks/` [Update an existing check](#update-check) | `POST SITE_ROOT/api/v1/checks/` [Pause monitoring of a check](#pause-check) | `POST SITE_ROOT/api/v1/checks//pause` [Delete check](#delete-check) | `DELETE SITE_ROOT/api/v1/checks/` +[Get a list of checks's logged pings](#list-pings) | `GET SITE_ROOT/api/v1/checks//pings/` [Get a list of existing integrations](#list-channels) | `GET SITE_ROOT/api/v1/channels/` ## Authentication @@ -25,8 +26,8 @@ an API key. You can create read-write and read-only API keys in the Key Type | Description -------------------|------------ -Regular API key | Have full access to all documented API endpoints. -Read-only API key | Only work with the [Get a list of existing checks](#list-checks) endpoint. Some fields are omitted from the API responses. +Regular API key | Has full access to all documented API endpoints. +Read-only API key | Only works with the [Get a list of existing checks](#list-checks) and [Get a single check](#get-check) endpoints. Some fields are omitted from the API responses. The client can authenticate itself by sending an appropriate HTTP request header. The header's name should be `X-Api-Key` and @@ -662,11 +663,90 @@ curl SITE_ROOT/api/v1/checks/f618072a-7bde-4eee-af63-71a77c5723bc \ } ``` +## Get a list of checks's logged pings {: #list-pings .rule } + +`GET SITE_ROOT/api/v1/checks//pings/` + +Returns a list of pings this check has received. + +This endpoint returns pings in reverse order (most recent first), and the total +number of returned pings depends on account's billing plan: 100 for free accounts, +1000 for paid accounts. + +### Response Codes + +200 OK +: The request succeeded. + +401 Unauthorized +: The API key is either missing or invalid. + +403 Forbidden +: Access denied, wrong API key. + +404 Not Found +: The specified check does not exist. + +### Example Request + +```bash +curl SITE_ROOT/api/v1/checks/f618072a-7bde-4eee-af63-71a77c5723bc/pings/ \ + --header "X-Api-Key: your-api-key" +``` + +### Example Response + +```json +{ + "pings": [ + { + "type": "success", + "date": "2020-06-09T14:51:06.113073+00:00", + "n": 4, + "scheme": "http", + "remote_addr": "192.0.2.0", + "method": "GET", + "ua": "curl/7.68.0", + "duration": 2.896736 + }, + { + "type": "start", + "date": "2020-06-09T14:51:03.216337+00:00", + "n": 3, + "scheme": "http", + "remote_addr": "192.0.2.0", + "method": "GET", + "ua": "curl/7.68.0" + }, + { + "type": "success", + "date": "2020-06-09T14:50:59.633577+00:00", + "n": 2, + "scheme": "http", + "remote_addr": "192.0.2.0", + "method": "GET", + "ua": "curl/7.68.0", + "duration": 2.997976 + }, + { + "type": "start", + "date": "2020-06-09T14:50:56.635601+00:00", + "n": 1, + "scheme": "http", + "remote_addr": "192.0.2.0", + "method": "GET", + "ua": "curl/7.68.0" + } + ] +} +``` + + ## Get a List of Existing Integrations {: #list-channels .rule } `GET SITE_ROOT/api/v1/channels/` -Returns a list of integrations belonging to the user. +Returns a list of integrations belonging to the project. ### Response Codes