forked from GithubBackups/healthchecks
Add "Get a list of checks's logged pings" API call (#371)
This commit is contained in:
parent
461ef5e088
commit
a07325e40f
@ -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
|
||||
|
@ -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)
|
||||
|
58
hc/api/tests/test_get_pings.py
Normal file
58
hc/api/tests/test_get_pings.py
Normal file
@ -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)
|
@ -25,6 +25,7 @@ urlpatterns = [
|
||||
path("api/v1/checks/<uuid:code>", views.single, name="hc-api-single"),
|
||||
path("api/v1/checks/<uuid:code>/pause", views.pause, name="hc-api-pause"),
|
||||
path("api/v1/notifications/<uuid:code>/bounce", views.bounce, name="hc-api-bounce"),
|
||||
path("api/v1/checks/<uuid:code>/pings/", views.pings, name="hc-api-pings"),
|
||||
path("api/v1/channels/", views.channels),
|
||||
path(
|
||||
"badge/<slug:badge_key>/<slug:signature>/<quoted:tag>.<slug:fmt>",
|
||||
|
@ -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"):
|
||||
|
@ -35,6 +35,10 @@ checks in user's account.</p>
|
||||
<td><code>DELETE SITE_ROOT/api/v1/checks/<uuid></code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="#list-pings">Get a list of checks's logged pings</a></td>
|
||||
<td><code>GET SITE_ROOT/api/v1/checks/<uuid>/pings/</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="#list-channels">Get a list of existing integrations</a></td>
|
||||
<td><code>GET SITE_ROOT/api/v1/channels/</code></td>
|
||||
</tr>
|
||||
@ -56,11 +60,11 @@ an API key. You can create read-write and read-only API keys in the
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Regular API key</td>
|
||||
<td>Have full access to all documented API endpoints.</td>
|
||||
<td>Has full access to all documented API endpoints.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Read-only API key</td>
|
||||
<td>Only work with the <a href="#list-checks">Get a list of existing checks</a> endpoint. Some fields are omitted from the API responses.</td>
|
||||
<td>Only works with the <a href="#list-checks">Get a list of existing checks</a> and <a href="#get-check">Get a single check</a> endpoints. Some fields are omitted from the API responses.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -599,9 +603,78 @@ check that was just deleted.</p>
|
||||
</code></pre></div>
|
||||
|
||||
|
||||
<h2 class="rule" id="list-pings">Get a list of checks's logged pings</h2>
|
||||
<p><code>GET SITE_ROOT/api/v1/checks/<uuid>/pings/</code></p>
|
||||
<p>Returns a list of pings this check has received.</p>
|
||||
<p>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.</p>
|
||||
<h3>Response Codes</h3>
|
||||
<dl>
|
||||
<dt>200 OK</dt>
|
||||
<dd>The request succeeded.</dd>
|
||||
<dt>401 Unauthorized</dt>
|
||||
<dd>The API key is either missing or invalid.</dd>
|
||||
<dt>403 Forbidden</dt>
|
||||
<dd>Access denied, wrong API key.</dd>
|
||||
<dt>404 Not Found</dt>
|
||||
<dd>The specified check does not exist.</dd>
|
||||
</dl>
|
||||
<h3>Example Request</h3>
|
||||
<div class="highlight"><pre><span></span><code>curl SITE_ROOT/api/v1/checks/f618072a-7bde-4eee-af63-71a77c5723bc/pings/ <span class="se">\</span>
|
||||
--header <span class="s2">"X-Api-Key: your-api-key"</span>
|
||||
</code></pre></div>
|
||||
|
||||
|
||||
<h3>Example Response</h3>
|
||||
<div class="highlight"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="nt">"pings"</span><span class="p">:</span> <span class="p">[</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"type"</span><span class="p">:</span> <span class="s2">"success"</span><span class="p">,</span>
|
||||
<span class="nt">"date"</span><span class="p">:</span> <span class="s2">"2020-06-09T14:51:06.113073+00:00"</span><span class="p">,</span>
|
||||
<span class="nt">"n"</span><span class="p">:</span> <span class="mi">4</span><span class="p">,</span>
|
||||
<span class="nt">"scheme"</span><span class="p">:</span> <span class="s2">"http"</span><span class="p">,</span>
|
||||
<span class="nt">"remote_addr"</span><span class="p">:</span> <span class="s2">"192.0.2.0"</span><span class="p">,</span>
|
||||
<span class="nt">"method"</span><span class="p">:</span> <span class="s2">"GET"</span><span class="p">,</span>
|
||||
<span class="nt">"ua"</span><span class="p">:</span> <span class="s2">"curl/7.68.0"</span><span class="p">,</span>
|
||||
<span class="nt">"duration"</span><span class="p">:</span> <span class="mf">2.896736</span>
|
||||
<span class="p">},</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"type"</span><span class="p">:</span> <span class="s2">"start"</span><span class="p">,</span>
|
||||
<span class="nt">"date"</span><span class="p">:</span> <span class="s2">"2020-06-09T14:51:03.216337+00:00"</span><span class="p">,</span>
|
||||
<span class="nt">"n"</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
|
||||
<span class="nt">"scheme"</span><span class="p">:</span> <span class="s2">"http"</span><span class="p">,</span>
|
||||
<span class="nt">"remote_addr"</span><span class="p">:</span> <span class="s2">"192.0.2.0"</span><span class="p">,</span>
|
||||
<span class="nt">"method"</span><span class="p">:</span> <span class="s2">"GET"</span><span class="p">,</span>
|
||||
<span class="nt">"ua"</span><span class="p">:</span> <span class="s2">"curl/7.68.0"</span>
|
||||
<span class="p">},</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"type"</span><span class="p">:</span> <span class="s2">"success"</span><span class="p">,</span>
|
||||
<span class="nt">"date"</span><span class="p">:</span> <span class="s2">"2020-06-09T14:50:59.633577+00:00"</span><span class="p">,</span>
|
||||
<span class="nt">"n"</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span>
|
||||
<span class="nt">"scheme"</span><span class="p">:</span> <span class="s2">"http"</span><span class="p">,</span>
|
||||
<span class="nt">"remote_addr"</span><span class="p">:</span> <span class="s2">"192.0.2.0"</span><span class="p">,</span>
|
||||
<span class="nt">"method"</span><span class="p">:</span> <span class="s2">"GET"</span><span class="p">,</span>
|
||||
<span class="nt">"ua"</span><span class="p">:</span> <span class="s2">"curl/7.68.0"</span><span class="p">,</span>
|
||||
<span class="nt">"duration"</span><span class="p">:</span> <span class="mf">2.997976</span>
|
||||
<span class="p">},</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"type"</span><span class="p">:</span> <span class="s2">"start"</span><span class="p">,</span>
|
||||
<span class="nt">"date"</span><span class="p">:</span> <span class="s2">"2020-06-09T14:50:56.635601+00:00"</span><span class="p">,</span>
|
||||
<span class="nt">"n"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
|
||||
<span class="nt">"scheme"</span><span class="p">:</span> <span class="s2">"http"</span><span class="p">,</span>
|
||||
<span class="nt">"remote_addr"</span><span class="p">:</span> <span class="s2">"192.0.2.0"</span><span class="p">,</span>
|
||||
<span class="nt">"method"</span><span class="p">:</span> <span class="s2">"GET"</span><span class="p">,</span>
|
||||
<span class="nt">"ua"</span><span class="p">:</span> <span class="s2">"curl/7.68.0"</span>
|
||||
<span class="p">}</span>
|
||||
<span class="p">]</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
|
||||
<h2 class="rule" id="list-channels">Get a List of Existing Integrations</h2>
|
||||
<p><code>GET SITE_ROOT/api/v1/channels/</code></p>
|
||||
<p>Returns a list of integrations belonging to the user.</p>
|
||||
<p>Returns a list of integrations belonging to the project.</p>
|
||||
<h3>Response Codes</h3>
|
||||
<dl>
|
||||
<dt>200 OK</dt>
|
||||
|
@ -13,6 +13,7 @@ Endpoint Name | Endpoint Address
|
||||
[Update an existing check](#update-check) | `POST SITE_ROOT/api/v1/checks/<uuid>`
|
||||
[Pause monitoring of a check](#pause-check) | `POST SITE_ROOT/api/v1/checks/<uuid>/pause`
|
||||
[Delete check](#delete-check) | `DELETE SITE_ROOT/api/v1/checks/<uuid>`
|
||||
[Get a list of checks's logged pings](#list-pings) | `GET SITE_ROOT/api/v1/checks/<uuid>/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/<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
|
||||
|
||||
```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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user