diff --git a/hc/api/models.py b/hc/api/models.py index 52824ca0..aa5181ba 100644 --- a/hc/api/models.py +++ b/hc/api/models.py @@ -247,33 +247,40 @@ class Check(models.Model): ping.save() def outages_by_month(self, months=2): - now = timezone.now() + """ Calculate the number of outages and downtime minutes per month. + + Returns a list of (datetime, downtime_in_secs, number_of_outages) tuples. + + """ + def monthkey(dt): + return dt.year, dt.month + + # Will accumulate totals here. + # (year, month) -> [datetime, downtime_in_secs, number_of_outages] totals = {} + # Will collect flips and month boundaries here events = [] + for boundary in month_boundaries(months=months): - totals[(boundary.year, boundary.month)] = [boundary, 0, 0] + totals[monthkey(boundary)] = [boundary, 0, 0] events.append((boundary, "---")) - flips = self.flip_set.filter(created__gt=now - td(days=32 * months)) - for flip in flips: + for flip in self.flip_set.filter(created__gt=boundary): events.append((flip.created, flip.old_status)) - events.sort(reverse=True) - - needle, status = now, self.status - for dt, old_status in events: + # Iterate through flips and month boundaries in reverse order, + # and for each "down" event increase the counters in `totals`. + dt, status = timezone.now(), self.status + for prev_dt, prev_status in sorted(events, reverse=True): if status == "down": - if (dt.year, dt.month) not in totals: - break - - delta = needle - dt - totals[(dt.year, dt.month)][1] += int(delta.total_seconds()) - totals[(dt.year, dt.month)][2] += 1 + delta = dt - prev_dt + totals[monthkey(prev_dt)][1] += int(delta.total_seconds()) + totals[monthkey(prev_dt)][2] += 1 - needle = dt - if old_status != "---": - status = old_status + dt = prev_dt + if prev_status != "---": + status = prev_status flattened = list(totals.values()) flattened.sort(reverse=True) diff --git a/hc/api/tests/test_check_model.py b/hc/api/tests/test_check_model.py index fec533e5..3fea2eb8 100644 --- a/hc/api/tests/test_check_model.py +++ b/hc/api/tests/test_check_model.py @@ -1,8 +1,9 @@ from datetime import datetime, timedelta from django.utils import timezone -from hc.api.models import Check +from hc.api.models import Check, Flip from hc.test import BaseTestCase +from mock import patch class CheckModelTestCase(BaseTestCase): @@ -164,3 +165,65 @@ class CheckModelTestCase(BaseTestCase): d = check.to_dict() self.assertEqual(d["next_ping"], "2000-01-01T01:00:00+00:00") + + def test_outages_by_month_handles_no_flips(self): + check = Check.objects.create(project=self.project) + r = check.outages_by_month(10) + self.assertEqual(len(r), 10) + for dt, secs, outages in r: + self.assertEqual(secs, 0) + self.assertEqual(outages, 0) + + def test_outages_by_month_handles_currently_down_check(self): + check = Check.objects.create(project=self.project, status="down") + + r = check.outages_by_month(10) + self.assertEqual(len(r), 10) + for dt, secs, outages in r: + self.assertEqual(outages, 1) + + @patch("hc.api.models.timezone.now") + def test_outages_by_month_handles_flip_one_day_ago(self, mock_now): + mock_now.return_value = datetime(2019, 7, 19, tzinfo=timezone.utc) + + check = Check.objects.create(project=self.project, status="down") + flip = Flip(owner=check) + flip.created = datetime(2019, 7, 18, tzinfo=timezone.utc) + flip.old_status = "up" + flip.new_status = "down" + flip.save() + + r = check.outages_by_month(10) + self.assertEqual(len(r), 10) + for dt, secs, outages in r: + if dt.month == 7: + self.assertEqual(secs, 86400) + self.assertEqual(outages, 1) + else: + self.assertEqual(secs, 0) + self.assertEqual(outages, 0) + + @patch("hc.api.models.timezone.now") + def test_outages_by_month_handles_flip_two_months_ago(self, mock_now): + mock_now.return_value = datetime(2019, 7, 19, tzinfo=timezone.utc) + + check = Check.objects.create(project=self.project, status="down") + flip = Flip(owner=check) + flip.created = datetime(2019, 5, 19, tzinfo=timezone.utc) + flip.old_status = "up" + flip.new_status = "down" + flip.save() + + r = check.outages_by_month(10) + self.assertEqual(len(r), 10) + for dt, secs, outages in r: + if dt.month == 7: + self.assertEqual(outages, 1) + elif dt.month == 6: + self.assertEqual(secs, 30 * 86400) + self.assertEqual(outages, 1) + elif dt.month == 5: + self.assertEqual(outages, 1) + else: + self.assertEqual(secs, 0) + self.assertEqual(outages, 0) diff --git a/templates/emails/summary-downtimes-html.html b/templates/emails/summary-downtimes-html.html index d735d5d0..3b10b4cb 100644 --- a/templates/emails/summary-downtimes-html.html +++ b/templates/emails/summary-downtimes-html.html @@ -65,7 +65,7 @@ {% for boundary, seconds, count in check.outages_by_month %} {% if count %}