forked from GithubBackups/healthchecks
Merge branch 'jameskirsop-return-single-history'
This commit is contained in:
commit
276c36841a
@ -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
|
||||
|
28
hc/api/forms.py
Normal file
28
hc/api/forms.py
Normal file
@ -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)
|
@ -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
|
||||
|
77
hc/api/tests/test_get_flips.py
Normal file
77
hc/api/tests/test_get_flips.py
Normal file
@ -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)
|
@ -38,6 +38,8 @@ urlpatterns = [
|
||||
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/checks/<uuid:code>/flips/", views.flips_by_uuid, name="hc-api-flips"),
|
||||
path("api/v1/checks/<sha1:unique_key>/flips/", views.flips_by_unique_key),
|
||||
path("api/v1/channels/", views.channels),
|
||||
path(
|
||||
"badge/<slug:badge_key>/<slug:signature>/<quoted:tag>.<slug:fmt>",
|
||||
|
@ -8,6 +8,7 @@ from django.http import (
|
||||
HttpResponse,
|
||||
HttpResponseForbidden,
|
||||
HttpResponseNotFound,
|
||||
HttpResponseBadRequest,
|
||||
JsonResponse,
|
||||
)
|
||||
from django.shortcuts import get_object_or_404
|
||||
@ -19,6 +20,7 @@ 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.forms import FlipsFiltersForm
|
||||
from hc.api.models import MAX_DELTA, Flip, Channel, Check, Notification, Ping
|
||||
from hc.lib.badges import check_signature, get_badge_svg
|
||||
|
||||
@ -188,7 +190,6 @@ def get_check(request, code):
|
||||
check = get_object_or_404(Check, code=code)
|
||||
if check.project_id != request.project.id:
|
||||
return HttpResponseForbidden()
|
||||
|
||||
return JsonResponse(check.to_dict(readonly=request.readonly))
|
||||
|
||||
|
||||
@ -292,6 +293,50 @@ def pings(request, code):
|
||||
return JsonResponse({"pings": dicts})
|
||||
|
||||
|
||||
def flips(request, check):
|
||||
if check.project_id != request.project.id:
|
||||
return HttpResponseForbidden()
|
||||
|
||||
form = FlipsFiltersForm(request.GET)
|
||||
if not form.is_valid():
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
flips = Flip.objects.filter(owner=check).order_by("-id")
|
||||
|
||||
if form.cleaned_data["start"]:
|
||||
flips = flips.filter(created__gte=form.cleaned_data["start"])
|
||||
|
||||
if form.cleaned_data["end"]:
|
||||
flips = flips.filter(created__lt=form.cleaned_data["end"])
|
||||
|
||||
if form.cleaned_data["seconds"]:
|
||||
threshold = timezone.now() - td(seconds=form.cleaned_data["seconds"])
|
||||
flips = flips.filter(created__gte=threshold)
|
||||
|
||||
return JsonResponse({"flips": [flip.to_dict() for flip in flips]})
|
||||
|
||||
|
||||
@cors("GET")
|
||||
@csrf_exempt
|
||||
@validate_json()
|
||||
@authorize_read
|
||||
def flips_by_uuid(request, code):
|
||||
check = get_object_or_404(Check, code=code)
|
||||
return flips(request, check)
|
||||
|
||||
|
||||
@cors("GET")
|
||||
@csrf_exempt
|
||||
@validate_json()
|
||||
@authorize_read
|
||||
def flips_by_unique_key(request, unique_key):
|
||||
checks = Check.objects.filter(project=request.project.id)
|
||||
for check in checks:
|
||||
if check.unique_key == unique_key:
|
||||
return flips(request, check)
|
||||
return HttpResponseNotFound()
|
||||
|
||||
|
||||
@never_cache
|
||||
@cors("GET")
|
||||
def badge(request, badge_key, signature, tag, fmt="svg"):
|
||||
|
@ -43,6 +43,10 @@ checks in user's account.</p>
|
||||
<td><code>GET SITE_ROOT/api/v1/checks/<uuid>/pings/</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="#list-flips">Get a list of check's status changes</a></td>
|
||||
<td><code>GET SITE_ROOT/api/v1/checks/<uuid>/flips/</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>
|
||||
@ -679,6 +683,69 @@ number of returned pings depends on account's billing plan: 100 for free account
|
||||
</code></pre></div>
|
||||
|
||||
|
||||
<h2 class="rule" id="list-flips">Get a list of check's status changes</h2>
|
||||
<p><code>GET SITE_ROOT/api/v1/checks/<uuid>/flips/</code><br>
|
||||
<code>GET SITE_ROOT/api/v1/checks/<unique_key>/flips/</code></p>
|
||||
<p>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").</p>
|
||||
<h3>Query String Parameters</h3>
|
||||
<dl>
|
||||
<dt>seconds=<value></dt>
|
||||
<dd>
|
||||
<p>Returns the flips from the last <code>value</code> seconds</p>
|
||||
<p>Example:</p>
|
||||
<p><code>SITE_ROOT/api/v1/checks/<uuid|unique_key>/flips/?seconds=3600</code></p>
|
||||
</dd>
|
||||
<dt>start=<value></dt>
|
||||
<dd>
|
||||
<p>Returns flips that are newer than the specified UNIX timestamp.</p>
|
||||
<p>Example:</p>
|
||||
<p><code>SITE_ROOT/api/v1/checks/<uuid|unique_key>/flips/?start=1592214380</code></p>
|
||||
</dd>
|
||||
<dt>end=<value></dt>
|
||||
<dd>
|
||||
<p>Returns flips that are older than the specified UNIX timestamp.</p>
|
||||
<p>Example:</p>
|
||||
<p><code>SITE_ROOT/api/v1/checks/<uuid|unique_key>/flips/?end=1592217980</code></p>
|
||||
</dd>
|
||||
</dl>
|
||||
<h3>Response Codes</h3>
|
||||
<dl>
|
||||
<dt>200 OK</dt>
|
||||
<dd>The request succeeded.</dd>
|
||||
<dt>400 Bad Request</dt>
|
||||
<dd>Invalid query parameters.</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/flips/ <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="p">{</span>
|
||||
<span class="nt">"timestamp"</span><span class="p">:</span> <span class="s2">"2020-03-23T10:18:23+00:00"</span><span class="p">,</span>
|
||||
<span class="nt">"up"</span><span class="p">:</span> <span class="mi">1</span>
|
||||
<span class="p">},</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"timestamp"</span><span class="p">:</span> <span class="s2">"2020-03-23T10:17:15+00:00"</span><span class="p">,</span>
|
||||
<span class="nt">"up"</span><span class="p">:</span> <span class="mi">0</span>
|
||||
<span class="p">},</span>
|
||||
<span class="p">{</span>
|
||||
<span class="nt">"timestamp"</span><span class="p">:</span> <span class="s2">"2020-03-23T10:16:18+00:00"</span><span class="p">,</span>
|
||||
<span class="nt">"up"</span><span class="p">:</span> <span class="mi">1</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 project.</p>
|
||||
|
@ -14,7 +14,8 @@ 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 check's logged pings](#list-pings) | `GET SITE_ROOT/api/v1/checks/<uuid>/pings/`
|
||||
[Get a list of check's logged pings](#list-pings) | `GET SITE_ROOT/api/v1/checks/<uuid>/pings/`
|
||||
[Get a list of check's status changes](#list-flips) | `GET SITE_ROOT/api/v1/checks/<uuid>/flips/`
|
||||
[Get a list of existing integrations](#list-channels) | `GET SITE_ROOT/api/v1/channels/`
|
||||
|
||||
## Authentication
|
||||
@ -746,6 +747,81 @@ curl SITE_ROOT/api/v1/checks/f618072a-7bde-4eee-af63-71a77c5723bc/pings/ \
|
||||
```
|
||||
|
||||
|
||||
## Get a list of check's status changes {: #list-flips .rule }
|
||||
|
||||
`GET SITE_ROOT/api/v1/checks/<uuid>/flips/`<br>
|
||||
`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").
|
||||
|
||||
### Query String Parameters
|
||||
|
||||
seconds=<value>
|
||||
: Returns the flips from the last `value` seconds
|
||||
|
||||
Example:
|
||||
|
||||
`SITE_ROOT/api/v1/checks/<uuid|unique_key>/flips/?seconds=3600`
|
||||
|
||||
start=<value>
|
||||
: Returns flips that are newer than the specified UNIX timestamp.
|
||||
|
||||
Example:
|
||||
|
||||
`SITE_ROOT/api/v1/checks/<uuid|unique_key>/flips/?start=1592214380`
|
||||
|
||||
end=<value>
|
||||
: Returns flips that are older than the specified UNIX timestamp.
|
||||
|
||||
Example:
|
||||
|
||||
`SITE_ROOT/api/v1/checks/<uuid|unique_key>/flips/?end=1592217980`
|
||||
|
||||
|
||||
### Response Codes
|
||||
|
||||
200 OK
|
||||
: The request succeeded.
|
||||
|
||||
400 Bad Request
|
||||
: Invalid query parameters.
|
||||
|
||||
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/flips/ \
|
||||
--header "X-Api-Key: your-api-key"
|
||||
```
|
||||
|
||||
### Example Response
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"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 a List of Existing Integrations {: #list-channels .rule }
|
||||
|
||||
`GET SITE_ROOT/api/v1/channels/`
|
||||
|
Loading…
x
Reference in New Issue
Block a user