mirror of
https://github.com/healthchecks/healthchecks.git
synced 2025-04-10 15:37:30 +00:00
Add DowntimeRecord.no_data field
This commit is contained in:
parent
58d7c8cc55
commit
2a0ae809a7
4 changed files with 37 additions and 25 deletions
hc/api
templates
|
@ -133,27 +133,34 @@ class CheckDict(TypedDict, total=False):
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class DowntimeRecord:
|
class DowntimeRecord:
|
||||||
boundary: datetime
|
boundary: datetime # The start of this time interval (timezone-aware)
|
||||||
tz: str
|
tz: str # For calculating total seconds in a month
|
||||||
duration: td
|
no_data: bool # True if the check did not yet exist in this time interval
|
||||||
count: int | None
|
duration: td # Total downtime in this time interval
|
||||||
|
count: int # The number of downtime events in this time interval
|
||||||
|
|
||||||
def monthly_uptime(self) -> float:
|
def monthly_uptime(self) -> float:
|
||||||
# NB: this method assumes monthly boundaries.
|
# NB: this method assumes monthly boundaries.
|
||||||
# It will yield incorrect results for weekly boundaries
|
# It will yield incorrect results for weekly boundaries
|
||||||
|
|
||||||
max_seconds = seconds_in_month(self.boundary.date(), self.tz)
|
max_seconds = seconds_in_month(self.boundary.date(), self.tz)
|
||||||
up_seconds = max_seconds - self.duration.total_seconds()
|
up_seconds = max_seconds - self.duration.total_seconds()
|
||||||
return up_seconds / max_seconds
|
return up_seconds / max_seconds
|
||||||
|
|
||||||
|
|
||||||
class DowntimeSummary(object):
|
class DowntimeSummary(object):
|
||||||
def __init__(self, boundaries: list[datetime], tz: str) -> None:
|
def __init__(self, boundaries: list[datetime], tz: str, created: datetime) -> None:
|
||||||
"""
|
"""
|
||||||
`boundaries` are timezone-aware datetimes of the first days of time intervals
|
`boundaries` is a list of timezone-aware datetimes of the starts of time
|
||||||
(months or weeks), and should be pre-sorted in descending order.
|
intervals (months or weeks), and should be pre-sorted in descending order.
|
||||||
"""
|
"""
|
||||||
self.records = [DowntimeRecord(b, tz, td(), 0) for b in boundaries]
|
self.records = []
|
||||||
|
prev_boundary = None
|
||||||
|
for b in boundaries:
|
||||||
|
# If the check was created *after* the start of the previous time
|
||||||
|
# interval then the check did not yet exist during this time interval:
|
||||||
|
no_data = prev_boundary and created > prev_boundary
|
||||||
|
self.records.append(DowntimeRecord(b, tz, no_data, td(), 0))
|
||||||
|
prev_boundary = b
|
||||||
|
|
||||||
def add(self, when: datetime, duration: td) -> None:
|
def add(self, when: datetime, duration: td) -> None:
|
||||||
for record in self.records:
|
for record in self.records:
|
||||||
|
@ -513,7 +520,7 @@ class Check(models.Model):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
summary = DowntimeSummary(boundaries, tz)
|
summary = DowntimeSummary(boundaries, tz, self.created)
|
||||||
|
|
||||||
# A list of flips and time interval boundaries
|
# A list of flips and time interval boundaries
|
||||||
events = [(b, "---") for b in boundaries]
|
events = [(b, "---") for b in boundaries]
|
||||||
|
@ -535,14 +542,6 @@ class Check(models.Model):
|
||||||
if prev_status != "---":
|
if prev_status != "---":
|
||||||
status = prev_status
|
status = prev_status
|
||||||
|
|
||||||
# Set count to None for intervals when the check didn't exist yet
|
|
||||||
prev_boundary = None
|
|
||||||
for record in summary.records:
|
|
||||||
if prev_boundary and self.created > prev_boundary:
|
|
||||||
record.count = None
|
|
||||||
|
|
||||||
prev_boundary = record.boundary
|
|
||||||
|
|
||||||
return summary.records
|
return summary.records
|
||||||
|
|
||||||
def downtimes(self, months: int, tz: str) -> list[DowntimeRecord]:
|
def downtimes(self, months: int, tz: str) -> list[DowntimeRecord]:
|
||||||
|
|
|
@ -188,16 +188,22 @@ class CheckModelTestCase(BaseTestCase):
|
||||||
|
|
||||||
# Jan. 2020
|
# Jan. 2020
|
||||||
self.assertEqual(jan.boundary.strftime("%m-%Y"), "01-2020")
|
self.assertEqual(jan.boundary.strftime("%m-%Y"), "01-2020")
|
||||||
|
self.assertEqual(jan.tz, "UTC")
|
||||||
|
self.assertFalse(jan.no_data)
|
||||||
self.assertEqual(jan.duration, td())
|
self.assertEqual(jan.duration, td())
|
||||||
self.assertEqual(jan.count, 0)
|
self.assertEqual(jan.count, 0)
|
||||||
|
|
||||||
# Dec. 2019
|
# Dec. 2019
|
||||||
self.assertEqual(dec.boundary.strftime("%m-%Y"), "12-2019")
|
self.assertEqual(dec.boundary.strftime("%m-%Y"), "12-2019")
|
||||||
|
self.assertEqual(jan.tz, "UTC")
|
||||||
|
self.assertFalse(jan.no_data)
|
||||||
self.assertEqual(dec.duration, td())
|
self.assertEqual(dec.duration, td())
|
||||||
self.assertEqual(dec.count, 0)
|
self.assertEqual(dec.count, 0)
|
||||||
|
|
||||||
# Nov. 2019
|
# Nov. 2019
|
||||||
self.assertEqual(nov.boundary.strftime("%m-%Y"), "11-2019")
|
self.assertEqual(nov.boundary.strftime("%m-%Y"), "11-2019")
|
||||||
|
self.assertEqual(jan.tz, "UTC")
|
||||||
|
self.assertFalse(jan.no_data)
|
||||||
self.assertEqual(nov.duration, td())
|
self.assertEqual(nov.duration, td())
|
||||||
self.assertEqual(nov.count, 0)
|
self.assertEqual(nov.count, 0)
|
||||||
|
|
||||||
|
@ -272,16 +278,19 @@ class CheckModelTestCase(BaseTestCase):
|
||||||
jan, dec, nov = r
|
jan, dec, nov = r
|
||||||
|
|
||||||
self.assertEqual(jan.boundary.isoformat(), "2020-01-01T00:00:00+00:00")
|
self.assertEqual(jan.boundary.isoformat(), "2020-01-01T00:00:00+00:00")
|
||||||
|
self.assertFalse(jan.no_data)
|
||||||
self.assertEqual(jan.duration, td(days=14))
|
self.assertEqual(jan.duration, td(days=14))
|
||||||
self.assertEqual(jan.monthly_uptime(), (31 - 14) / 31)
|
self.assertEqual(jan.monthly_uptime(), (31 - 14) / 31)
|
||||||
self.assertEqual(jan.count, 1)
|
self.assertEqual(jan.count, 1)
|
||||||
|
|
||||||
self.assertEqual(dec.boundary.isoformat(), "2019-12-01T00:00:00+00:00")
|
self.assertEqual(dec.boundary.isoformat(), "2019-12-01T00:00:00+00:00")
|
||||||
|
self.assertFalse(dec.no_data)
|
||||||
self.assertEqual(dec.duration, td(days=31))
|
self.assertEqual(dec.duration, td(days=31))
|
||||||
self.assertEqual(dec.monthly_uptime(), 0.0)
|
self.assertEqual(dec.monthly_uptime(), 0.0)
|
||||||
self.assertEqual(dec.count, 1)
|
self.assertEqual(dec.count, 1)
|
||||||
|
|
||||||
self.assertEqual(nov.boundary.isoformat(), "2019-11-01T00:00:00+00:00")
|
self.assertEqual(nov.boundary.isoformat(), "2019-11-01T00:00:00+00:00")
|
||||||
|
self.assertFalse(nov.no_data)
|
||||||
self.assertEqual(nov.duration, td(days=16))
|
self.assertEqual(nov.duration, td(days=16))
|
||||||
self.assertEqual(nov.monthly_uptime(), 14 / 30)
|
self.assertEqual(nov.monthly_uptime(), 14 / 30)
|
||||||
self.assertEqual(nov.count, 1)
|
self.assertEqual(nov.count, 1)
|
||||||
|
@ -304,6 +313,8 @@ class CheckModelTestCase(BaseTestCase):
|
||||||
jan, dec = r
|
jan, dec = r
|
||||||
|
|
||||||
self.assertEqual(jan.boundary.isoformat(), "2020-01-01T00:00:00+02:00")
|
self.assertEqual(jan.boundary.isoformat(), "2020-01-01T00:00:00+02:00")
|
||||||
|
self.assertEqual(jan.tz, "Europe/Riga")
|
||||||
|
self.assertFalse(jan.no_data)
|
||||||
self.assertEqual(jan.duration, td(days=14, hours=1))
|
self.assertEqual(jan.duration, td(days=14, hours=1))
|
||||||
total_hours = 31 * 24
|
total_hours = 31 * 24
|
||||||
up_hours = total_hours - 14 * 24 - 1
|
up_hours = total_hours - 14 * 24 - 1
|
||||||
|
@ -311,6 +322,8 @@ class CheckModelTestCase(BaseTestCase):
|
||||||
self.assertEqual(jan.count, 1)
|
self.assertEqual(jan.count, 1)
|
||||||
|
|
||||||
self.assertEqual(dec.boundary.isoformat(), "2019-12-01T00:00:00+02:00")
|
self.assertEqual(dec.boundary.isoformat(), "2019-12-01T00:00:00+02:00")
|
||||||
|
self.assertEqual(dec.tz, "Europe/Riga")
|
||||||
|
self.assertFalse(dec.no_data)
|
||||||
self.assertEqual(dec.duration, td())
|
self.assertEqual(dec.duration, td())
|
||||||
self.assertEqual(dec.count, 0)
|
self.assertEqual(dec.count, 0)
|
||||||
|
|
||||||
|
@ -324,13 +337,13 @@ class CheckModelTestCase(BaseTestCase):
|
||||||
jan, dec, nov = check.downtimes(3, "UTC")
|
jan, dec, nov = check.downtimes(3, "UTC")
|
||||||
|
|
||||||
# Jan. 2020
|
# Jan. 2020
|
||||||
self.assertEqual(jan.count, 0)
|
self.assertFalse(jan.no_data)
|
||||||
|
|
||||||
# Dec. 2019
|
# Dec. 2019
|
||||||
self.assertIsNone(dec.count)
|
self.assertTrue(dec.no_data)
|
||||||
|
|
||||||
# Nov. 2019
|
# Nov. 2019
|
||||||
self.assertIsNone(nov.count)
|
self.assertTrue(nov.no_data)
|
||||||
|
|
||||||
@override_settings(S3_BUCKET=None)
|
@override_settings(S3_BUCKET=None)
|
||||||
def test_it_prunes(self) -> None:
|
def test_it_prunes(self) -> None:
|
||||||
|
|
|
@ -73,7 +73,7 @@
|
||||||
</td>
|
</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td style="border-top: 1px solid #EDEFF2; padding: 16px 8px; font-family: Helvetica, Arial, sans-serif; color: #9BA2AB;">
|
<td style="border-top: 1px solid #EDEFF2; padding: 16px 8px; font-family: Helvetica, Arial, sans-serif; color: #9BA2AB;">
|
||||||
{% if d.count is None %}
|
{% if d.no_data %}
|
||||||
{% comment %} The check didn't exist yet {% endcomment %}
|
{% comment %} The check didn't exist yet {% endcomment %}
|
||||||
{% else %}
|
{% else %}
|
||||||
All good!
|
All good!
|
||||||
|
|
|
@ -5,11 +5,11 @@
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ d.boundary|date:"N Y"}}</th>
|
<th>{{ d.boundary|date:"N Y"}}</th>
|
||||||
<td>
|
<td>
|
||||||
{% if d.count %}
|
{% if d.no_data %}
|
||||||
|
–
|
||||||
|
{% elif d.count %}
|
||||||
{{ d.count }} downtime{{ d.count|pluralize }}, {{ d.duration|hc_approx_duration }} total
|
{{ d.count }} downtime{{ d.count|pluralize }}, {{ d.duration|hc_approx_duration }} total
|
||||||
<span class="uptime">{{ d.monthly_uptime|pct }}% uptime</span>
|
<span class="uptime">{{ d.monthly_uptime|pct }}% uptime</span>
|
||||||
{% elif d.count is None %}
|
|
||||||
–
|
|
||||||
{% else %}
|
{% else %}
|
||||||
All good!
|
All good!
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
Loading…
Add table
Reference in a new issue