mirror of
https://github.com/healthchecks/healthchecks.git
synced 2025-04-07 14:15:34 +00:00
parent
c99c357709
commit
d7948d9939
8 changed files with 60 additions and 21 deletions
|
@ -1,6 +1,11 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## v3.4-dev - Unreleased
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
- Show status changes (flips) in check's log page (#447)
|
||||||
|
|
||||||
## v3.3 - 2024-04-03
|
## v3.3 - 2024-04-03
|
||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
|
|
|
@ -391,6 +391,7 @@ class LogFiltersForm(forms.Form):
|
||||||
log = forms.BooleanField(required=False)
|
log = forms.BooleanField(required=False)
|
||||||
ign = forms.BooleanField(required=False)
|
ign = forms.BooleanField(required=False)
|
||||||
notification = forms.BooleanField(required=False)
|
notification = forms.BooleanField(required=False)
|
||||||
|
flip = forms.BooleanField(required=False)
|
||||||
|
|
||||||
def clean_u(self) -> datetime | None:
|
def clean_u(self) -> datetime | None:
|
||||||
if self.cleaned_data["u"]:
|
if self.cleaned_data["u"]:
|
||||||
|
@ -403,7 +404,7 @@ class LogFiltersForm(forms.Form):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def kinds(self) -> tuple[str, ...]:
|
def kinds(self) -> tuple[str, ...]:
|
||||||
kind_keys = ("success", "fail", "start", "log", "ign", "notification")
|
kind_keys = ("success", "fail", "start", "log", "ign", "notification", "flip")
|
||||||
return tuple(key for key in kind_keys if self.cleaned_data[key])
|
return tuple(key for key in kind_keys if self.cleaned_data[key])
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ from urllib.parse import urlencode
|
||||||
|
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
|
||||||
from hc.api.models import Channel, Check, Notification, Ping
|
from hc.api.models import Channel, Check, Flip, Notification, Ping
|
||||||
from hc.test import BaseTestCase
|
from hc.test import BaseTestCase
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,6 +21,12 @@ class LogTestCase(BaseTestCase):
|
||||||
ch.value = json.dumps({"value": "alice@example.org", "up": True, "down": True})
|
ch.value = json.dumps({"value": "alice@example.org", "up": True, "down": True})
|
||||||
ch.save()
|
ch.save()
|
||||||
|
|
||||||
|
f = Flip(owner=self.check)
|
||||||
|
f.created = now() - td(hours=1)
|
||||||
|
f.old_status = "new"
|
||||||
|
f.new_status = "down"
|
||||||
|
f.save()
|
||||||
|
|
||||||
n = Notification(owner=self.check)
|
n = Notification(owner=self.check)
|
||||||
n.created = now() - td(hours=1)
|
n.created = now() - td(hours=1)
|
||||||
n.channel = ch
|
n.channel = ch
|
||||||
|
@ -36,7 +42,7 @@ class LogTestCase(BaseTestCase):
|
||||||
|
|
||||||
def url(self, **kwargs):
|
def url(self, **kwargs):
|
||||||
params = {}
|
params = {}
|
||||||
for key in ("success", "fail", "start", "log", "ign", "notification"):
|
for key in ("success", "fail", "start", "log", "ign", "notification", "flip"):
|
||||||
if kwargs.get(key, True):
|
if kwargs.get(key, True):
|
||||||
params[key] = "on"
|
params[key] = "on"
|
||||||
for key in ("u", "end"):
|
for key in ("u", "end"):
|
||||||
|
@ -48,8 +54,9 @@ class LogTestCase(BaseTestCase):
|
||||||
def test_it_works(self) -> None:
|
def test_it_works(self) -> None:
|
||||||
self.client.login(username="alice@example.org", password="password")
|
self.client.login(username="alice@example.org", password="password")
|
||||||
r = self.client.get(self.url())
|
r = self.client.get(self.url())
|
||||||
self.assertContains(r, "hello world")
|
self.assertContains(r, "hello world", status_code=200)
|
||||||
self.assertContains(r, "Sent email to alice@example.org", status_code=200)
|
self.assertContains(r, "Sent email to alice@example.org")
|
||||||
|
self.assertContains(r, "new ➔ down")
|
||||||
|
|
||||||
def test_team_access_works(self) -> None:
|
def test_team_access_works(self) -> None:
|
||||||
# Logging in as bob, not alice. Bob has team access so this
|
# Logging in as bob, not alice. Bob has team access so this
|
||||||
|
@ -128,3 +135,9 @@ class LogTestCase(BaseTestCase):
|
||||||
r = self.client.get(self.url(notification=False))
|
r = self.client.get(self.url(notification=False))
|
||||||
self.assertContains(r, "hello world")
|
self.assertContains(r, "hello world")
|
||||||
self.assertNotContains(r, "Sent email to alice@example.org", status_code=200)
|
self.assertNotContains(r, "Sent email to alice@example.org", status_code=200)
|
||||||
|
|
||||||
|
def test_it_filters_flip(self) -> None:
|
||||||
|
self.client.login(username="alice@example.org", password="password")
|
||||||
|
r = self.client.get(self.url(flip=False))
|
||||||
|
self.assertContains(r, "hello world")
|
||||||
|
self.assertNotContains(r, "new ➔ down")
|
||||||
|
|
|
@ -51,6 +51,7 @@ from hc.api.models import (
|
||||||
MAX_DURATION,
|
MAX_DURATION,
|
||||||
Channel,
|
Channel,
|
||||||
Check,
|
Check,
|
||||||
|
Flip,
|
||||||
Notification,
|
Notification,
|
||||||
Ping,
|
Ping,
|
||||||
TokenBucket,
|
TokenBucket,
|
||||||
|
@ -847,7 +848,7 @@ def _get_events(
|
||||||
start: datetime,
|
start: datetime,
|
||||||
end: datetime,
|
end: datetime,
|
||||||
kinds: tuple[str, ...] | None = None,
|
kinds: tuple[str, ...] | None = None,
|
||||||
) -> list[Notification | Ping]:
|
) -> list[Notification | Ping | Flip]:
|
||||||
# Sorting by "n" instead of "id" is important here. Both give the same
|
# Sorting by "n" instead of "id" is important here. Both give the same
|
||||||
# query results, but sorting by "id" can cause postgres to pick
|
# query results, but sorting by "id" can cause postgres to pick
|
||||||
# api_ping.id index (slow if the api_ping table is big). Sorting by
|
# api_ping.id index (slow if the api_ping table is big). Sorting by
|
||||||
|
@ -892,13 +893,19 @@ def _get_events(
|
||||||
ping.duration = None
|
ping.duration = None
|
||||||
|
|
||||||
alerts: list[Notification] = []
|
alerts: list[Notification] = []
|
||||||
if kinds is None or "notification" in kinds:
|
if kinds and "notification" in kinds:
|
||||||
aq = check.notification_set.order_by("-created")
|
aq = check.notification_set.order_by("-created")
|
||||||
aq = aq.filter(created__gte=start, created__lte=end, check_status="down")
|
aq = aq.filter(created__gte=start, created__lte=end, check_status="down")
|
||||||
aq = aq.select_related("channel")
|
aq = aq.select_related("channel")
|
||||||
alerts = list(aq[:page_limit])
|
alerts = list(aq[:page_limit])
|
||||||
|
|
||||||
events = pings + alerts
|
flips: list[Flip] = []
|
||||||
|
if kinds is None or "flip" in kinds:
|
||||||
|
fq = check.flip_set.order_by("-created")
|
||||||
|
fq = fq.filter(created__gte=start, created__lte=end)
|
||||||
|
flips = list(fq[:page_limit])
|
||||||
|
|
||||||
|
events = pings + alerts + flips
|
||||||
events.sort(key=lambda el: el.created, reverse=True)
|
events.sort(key=lambda el: el.created, reverse=True)
|
||||||
return events[:page_limit]
|
return events[:page_limit]
|
||||||
|
|
||||||
|
|
|
@ -82,9 +82,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
#log tr.missing td {
|
#log tr.missing td {
|
||||||
color: #d9534f;
|
color: #a94442;
|
||||||
background: var(--log-missing-bg);
|
}
|
||||||
border-bottom: 1px solid var(--log-missing-border);
|
|
||||||
|
#log tr.flip {
|
||||||
|
background: var(--log-flip-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
#log tr.missing td:nth-child(4) {
|
#log tr.missing td:nth-child(4) {
|
||||||
|
@ -105,3 +107,6 @@
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#filters hr {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
|
@ -40,8 +40,7 @@
|
||||||
--label-start-color: #117a3f;
|
--label-start-color: #117a3f;
|
||||||
--link-color: #0091EA;
|
--link-color: #0091EA;
|
||||||
--link-hover-color: #00629e;
|
--link-hover-color: #00629e;
|
||||||
--log-missing-bg: #fff3f2;
|
--log-flip-bg: #eee;
|
||||||
--log-missing-border: #fbe2e0;
|
|
||||||
--modal-content-bg: #fff;
|
--modal-content-bg: #fff;
|
||||||
--nav-link-hover-bg: #eee;
|
--nav-link-hover-bg: #eee;
|
||||||
--panel-bg: #fff;
|
--panel-bg: #fff;
|
||||||
|
@ -123,8 +122,7 @@ body.dark {
|
||||||
--label-start-color: #e0e0e2;
|
--label-start-color: #e0e0e2;
|
||||||
--link-color: hsl(202.8, 100%, 55%);
|
--link-color: hsl(202.8, 100%, 55%);
|
||||||
--link-hover-color: hsl(202.8, 100%, 65%);
|
--link-hover-color: hsl(202.8, 100%, 65%);
|
||||||
--log-missing-bg: #2d1e21;
|
--log-flip-bg: #383840;
|
||||||
--log-missing-border: #403033;
|
|
||||||
--modal-content-bg: #1f1f22;
|
--modal-content-bg: #1f1f22;
|
||||||
--nav-link-hover-bg: #383840;
|
--nav-link-hover-bg: #383840;
|
||||||
--panel-bg: hsl(240, 6%, 16%);
|
--panel-bg: hsl(240, 6%, 16%);
|
||||||
|
|
|
@ -84,7 +84,7 @@
|
||||||
<br>
|
<br>
|
||||||
<label>Event types</label>
|
<label>Event types</label>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label><input type="checkbox" name="success" autocomplete="off" checked> OK</label>
|
<label><input type="checkbox" name="success" autocomplete="off" checked> Success</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label><input type="checkbox" name="fail" autocomplete="off" checked> Failure</label>
|
<label><input type="checkbox" name="fail" autocomplete="off" checked> Failure</label>
|
||||||
|
@ -96,10 +96,14 @@
|
||||||
<label><input type="checkbox" name="log" autocomplete="off" checked> Log</label>
|
<label><input type="checkbox" name="log" autocomplete="off" checked> Log</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label><input type="checkbox" name="ign" autocomplete="off" checked> Ignored</label>
|
<label><input type="checkbox" name="ign" autocomplete="off" checked> Ignored ping</label>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="checkbox">
|
||||||
|
<label><input type="checkbox" name="flip" autocomplete="off" checked> Status change</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label><input type="checkbox" name="notification" autocomplete="off" checked> Notification</label>
|
<label><input type="checkbox" name="notification" autocomplete="off"> Downtime alert</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -24,8 +24,14 @@
|
||||||
{% elif event.check_status %}
|
{% elif event.check_status %}
|
||||||
<tr class="missing" data-dt="{{ event.created|timestamp }}">
|
<tr class="missing" data-dt="{{ event.created|timestamp }}">
|
||||||
<td><span class="ic-missing"></span></td>
|
<td><span class="ic-missing"></span></td>
|
||||||
<td></td>
|
<td></td><td></td>
|
||||||
<td></td>
|
|
||||||
<td colspan="2">{% include "front/event_summary.html" %}</td>
|
<td colspan="2">{% include "front/event_summary.html" %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% elif event.new_status %}
|
||||||
|
<tr class="flip" data-dt="{{ event.created|timestamp }}">
|
||||||
|
<td></td><td></td><td></td>
|
||||||
|
<td colspan="2">
|
||||||
|
Status: <strong>{{ event.old_status }} ➔ {{ event.new_status }}</strong>.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
|
Loading…
Add table
Reference in a new issue