diff --git a/CHANGELOG.md b/CHANGELOG.md index b01d25f0..e93c2b2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Changelog All notable changes to this project will be documented in this file. +## Unreleased + +### Improvements +- Show the number of outages and total downtime in monthly reports. (#104) + + ## 1.8.0 - 2019-07-08 ### Improvements diff --git a/hc/accounts/models.py b/hc/accounts/models.py index db35b2b5..37ed5cd7 100644 --- a/hc/accounts/models.py +++ b/hc/accounts/models.py @@ -12,6 +12,7 @@ from django.db.models import Count, Q from django.urls import reverse from django.utils import timezone from hc.lib import emails +from hc.lib.date import month_boundaries NO_NAG = timedelta() @@ -176,6 +177,7 @@ class Profile(models.Model): "nag": nag, "nag_period": self.nag_period.total_seconds(), "num_down": num_down, + "month_boundaries": month_boundaries(), } emails.report(self.user.email, ctx, headers) diff --git a/hc/api/models.py b/hc/api/models.py index ca92fdd8..52824ca0 100644 --- a/hc/api/models.py +++ b/hc/api/models.py @@ -13,6 +13,7 @@ from django.utils import timezone from hc.accounts.models import Project from hc.api import transports from hc.lib import emails +from hc.lib.date import month_boundaries import pytz STATUSES = (("up", "Up"), ("down", "Down"), ("new", "New"), ("paused", "Paused")) @@ -245,6 +246,39 @@ class Check(models.Model): ping.body = body[:10000] ping.save() + def outages_by_month(self, months=2): + now = timezone.now() + + totals = {} + events = [] + for boundary in month_boundaries(months=months): + totals[(boundary.year, boundary.month)] = [boundary, 0, 0] + events.append((boundary, "---")) + + flips = self.flip_set.filter(created__gt=now - td(days=32 * months)) + for flip in flips: + events.append((flip.created, flip.old_status)) + + events.sort(reverse=True) + + needle, status = now, self.status + for dt, old_status in events: + 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 + + needle = dt + if old_status != "---": + status = old_status + + flattened = list(totals.values()) + flattened.sort(reverse=True) + return flattened + class Ping(models.Model): id = models.BigAutoField(primary_key=True) diff --git a/hc/front/templatetags/hc_extras.py b/hc/front/templatetags/hc_extras.py index bff76b5d..aaadc79f 100644 --- a/hc/front/templatetags/hc_extras.py +++ b/hc/front/templatetags/hc_extras.py @@ -5,7 +5,7 @@ from django.conf import settings from django.utils.html import escape from django.utils.safestring import mark_safe -from hc.lib.date import format_duration, format_hms +from hc.lib.date import format_duration, format_approx_duration, format_hms register = template.Library() @@ -15,6 +15,11 @@ def hc_duration(td): return format_duration(td) +@register.filter +def hc_approx_duration(td): + return format_approx_duration(td) + + @register.filter def hms(td): return format_hms(td) diff --git a/hc/lib/date.py b/hc/lib/date.py index a358a167..6c9ef704 100644 --- a/hc/lib/date.py +++ b/hc/lib/date.py @@ -1,3 +1,7 @@ +from datetime import datetime as dt +from django.utils import timezone + + class Unit(object): def __init__(self, name, nsecs): self.name = name @@ -5,6 +9,7 @@ class Unit(object): self.nsecs = nsecs +SECOND = Unit("second", 1) MINUTE = Unit("minute", 60) HOUR = Unit("hour", MINUTE.nsecs * 60) DAY = Unit("day", HOUR.nsecs * 24) @@ -13,6 +18,7 @@ WEEK = Unit("week", DAY.nsecs * 7) def format_duration(td): remaining_seconds = int(td.total_seconds()) + result = [] for unit in (WEEK, DAY, HOUR, MINUTE): @@ -30,7 +36,11 @@ def format_duration(td): def format_hms(td): - total_seconds = int(td.total_seconds()) + if isinstance(td, int): + total_seconds = td + else: + total_seconds = int(td.total_seconds()) + result = [] mins, secs = divmod(total_seconds, 60) @@ -45,3 +55,31 @@ def format_hms(td): result.append("%s sec" % secs) return " ".join(result) + + +def format_approx_duration(v): + for unit in (DAY, HOUR, MINUTE, SECOND): + if v >= unit.nsecs: + vv = v // unit.nsecs + if vv == 1: + return "1 %s" % unit.name + else: + return "%d %s" % (vv, unit.plural) + + return "" + + +def month_boundaries(months=2): + result = [] + + now = timezone.now() + y, m = now.year, now.month + for x in range(0, months): + result.append(dt(y, m, 1, tzinfo=timezone.utc)) + + m -= 1 + if m == 0: + m = 12 + y = y - 1 + + return result diff --git a/templates/emails/report-body-html.html b/templates/emails/report-body-html.html index da214c7a..d61bbf75 100644 --- a/templates/emails/report-body-html.html +++ b/templates/emails/report-body-html.html @@ -20,7 +20,12 @@ Hello,
{% endif %}
-{% include "emails/summary-html.html" %} + +{% if nag %} + {% include "emails/summary-html.html" %} +{% else %} + {% include "emails/summary-downtimes-html.html" %} +{% endif %} {% if nag %} Too many notifications? diff --git a/templates/emails/summary-downtimes-html.html b/templates/emails/summary-downtimes-html.html new file mode 100644 index 00000000..160fcf33 --- /dev/null +++ b/templates/emails/summary-downtimes-html.html @@ -0,0 +1,84 @@ +{% load humanize hc_extras %} +{% regroup checks by project as groups %} + + {% for group in groups %} + + + {% for dt in month_boundaries %} + + {% endfor %} + + {% for check in group.list|sortchecks:sort %} + + + + {% for boundary, seconds, count in check.outages_by_month %} + {% if count %} + + {% else %} + + {% endif %} + {% endfor %} + + {% endfor %} + {% endfor %} +
+ {{ group.grouper|mangle_link }} + + {{ dt|date:"N Y"}} +
+ + + {% if check.get_status == "new" %} + + {% elif check.get_status == "paused" %} + + {% elif check.get_status == "grace" %} + + {% elif check.get_status == "up" %} + + {% elif check.get_status == "started" %} + + {% elif check.get_status == "down" %} + + {% endif %} + +
NEWPAUSEDLATEUPSTARTEDDOWN
+
+ {% if check.name %} + {% if check.name|length > 20 %} + {{ check.name|mangle_link }} + {% else %} + {{ check.name|mangle_link }} + {% endif %} + {% else %} + unnamed + {% endif %} + {% if check.tags %} +
+ + + {% for tag in check.tags_list %} + + {% endfor %} + +
+ + + + +
+ {{ tag|mangle_link }} +
+
+ {% endif %} +
+ {{ count }} outage{{ count|pluralize }} + +
+ ({{ seconds|hc_approx_duration }} total) +
+
+ All good! +
+
\ No newline at end of file