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