Browse Source

Experimental: show the number of outages and total downtime in monthly reports. (#104)

pull/272/head
Pēteris Caune 5 years ago
parent
commit
b74e56a273
No known key found for this signature in database GPG Key ID: E28D7679E9A9EDE2
7 changed files with 177 additions and 3 deletions
  1. +6
    -0
      CHANGELOG.md
  2. +2
    -0
      hc/accounts/models.py
  3. +34
    -0
      hc/api/models.py
  4. +6
    -1
      hc/front/templatetags/hc_extras.py
  5. +39
    -1
      hc/lib/date.py
  6. +6
    -1
      templates/emails/report-body-html.html
  7. +84
    -0
      templates/emails/summary-downtimes-html.html

+ 6
- 0
CHANGELOG.md View File

@ -1,6 +1,12 @@
# 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.
## Unreleased
### Improvements
- Show the number of outages and total downtime in monthly reports. (#104)
## 1.8.0 - 2019-07-08 ## 1.8.0 - 2019-07-08
### Improvements ### Improvements


+ 2
- 0
hc/accounts/models.py View File

@ -12,6 +12,7 @@ from django.db.models import Count, Q
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from hc.lib import emails from hc.lib import emails
from hc.lib.date import month_boundaries
NO_NAG = timedelta() NO_NAG = timedelta()
@ -176,6 +177,7 @@ class Profile(models.Model):
"nag": nag, "nag": nag,
"nag_period": self.nag_period.total_seconds(), "nag_period": self.nag_period.total_seconds(),
"num_down": num_down, "num_down": num_down,
"month_boundaries": month_boundaries(),
} }
emails.report(self.user.email, ctx, headers) emails.report(self.user.email, ctx, headers)


+ 34
- 0
hc/api/models.py View File

@ -13,6 +13,7 @@ from django.utils import timezone
from hc.accounts.models import Project from hc.accounts.models import Project
from hc.api import transports from hc.api import transports
from hc.lib import emails from hc.lib import emails
from hc.lib.date import month_boundaries
import pytz import pytz
STATUSES = (("up", "Up"), ("down", "Down"), ("new", "New"), ("paused", "Paused")) STATUSES = (("up", "Up"), ("down", "Down"), ("new", "New"), ("paused", "Paused"))
@ -245,6 +246,39 @@ class Check(models.Model):
ping.body = body[:10000] ping.body = body[:10000]
ping.save() 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): class Ping(models.Model):
id = models.BigAutoField(primary_key=True) id = models.BigAutoField(primary_key=True)


+ 6
- 1
hc/front/templatetags/hc_extras.py View File

@ -5,7 +5,7 @@ from django.conf import settings
from django.utils.html import escape from django.utils.html import escape
from django.utils.safestring import mark_safe 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() register = template.Library()
@ -15,6 +15,11 @@ def hc_duration(td):
return format_duration(td) return format_duration(td)
@register.filter
def hc_approx_duration(td):
return format_approx_duration(td)
@register.filter @register.filter
def hms(td): def hms(td):
return format_hms(td) return format_hms(td)


+ 39
- 1
hc/lib/date.py View File

@ -1,3 +1,7 @@
from datetime import datetime as dt
from django.utils import timezone
class Unit(object): class Unit(object):
def __init__(self, name, nsecs): def __init__(self, name, nsecs):
self.name = name self.name = name
@ -5,6 +9,7 @@ class Unit(object):
self.nsecs = nsecs self.nsecs = nsecs
SECOND = Unit("second", 1)
MINUTE = Unit("minute", 60) MINUTE = Unit("minute", 60)
HOUR = Unit("hour", MINUTE.nsecs * 60) HOUR = Unit("hour", MINUTE.nsecs * 60)
DAY = Unit("day", HOUR.nsecs * 24) DAY = Unit("day", HOUR.nsecs * 24)
@ -13,6 +18,7 @@ WEEK = Unit("week", DAY.nsecs * 7)
def format_duration(td): def format_duration(td):
remaining_seconds = int(td.total_seconds()) remaining_seconds = int(td.total_seconds())
result = [] result = []
for unit in (WEEK, DAY, HOUR, MINUTE): for unit in (WEEK, DAY, HOUR, MINUTE):
@ -30,7 +36,11 @@ def format_duration(td):
def format_hms(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 = [] result = []
mins, secs = divmod(total_seconds, 60) mins, secs = divmod(total_seconds, 60)
@ -45,3 +55,31 @@ def format_hms(td):
result.append("%s sec" % secs) result.append("%s sec" % secs)
return " ".join(result) 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

+ 6
- 1
templates/emails/report-body-html.html View File

@ -20,7 +20,12 @@ Hello,<br />
{% endif %} {% endif %}
<br /> <br />
{% include "emails/summary-html.html" %}
{% if nag %}
{% include "emails/summary-html.html" %}
{% else %}
{% include "emails/summary-downtimes-html.html" %}
{% endif %}
{% if nag %} {% if nag %}
<strong>Too many notifications?</strong> <strong>Too many notifications?</strong>


+ 84
- 0
templates/emails/summary-downtimes-html.html View File

@ -0,0 +1,84 @@
{% load humanize hc_extras %}
{% regroup checks by project as groups %}
<table style="margin: 0; width: 100%; font-size: 16px;" cellpadding="0" cellspacing="0">
{% for group in groups %}
<tr>
<td colspan="2" style="font-weight: bold; padding: 32px 8px 8px 8px; color: #333;">
{{ group.grouper|mangle_link }}
</td>
{% for dt in month_boundaries %}
<td style="padding: 32px 8px 8px 8px; margin: 0; font-size: 12px; color: #9BA2AB; font-family: Helvetica, Arial, sans-serif;">
{{ dt|date:"N Y"}}
</td>
{% endfor %}
</tr>
{% for check in group.list|sortchecks:sort %}
<tr>
<td style="border-top: 1px solid #EDEFF2; padding: 16px 8px;">
<table cellpadding="0" cellspacing="0">
<tr>
{% if check.get_status == "new" %}
<td style="background: #AAA; font-family: Helvetica, Arial, sans-serif; font-weight: bold; font-size: 10px; line-height: 10px; color: white; padding: 6px; margin: 0; border-radius: 3px;">NEW</td>
{% elif check.get_status == "paused" %}
<td style="background: #AAA; font-family: Helvetica, Arial, sans-serif; font-weight: bold; font-size: 10px; line-height: 10px; color: white; padding: 6px; border-radius: 3px;">PAUSED</td>
{% elif check.get_status == "grace" %}
<td style="background: #f0ad4e; font-family: Helvetica, Arial, sans-serif; font-weight: bold; font-size: 10px; line-height: 10px; color: white; padding: 6px; border-radius: 3px;">LATE</td>
{% elif check.get_status == "up" %}
<td style="background: #5cb85c; font-family: Helvetica, Arial, sans-serif; font-weight: bold; font-size: 10px; line-height: 10px; color: white; padding: 6px; border-radius: 3px;">UP</td>
{% elif check.get_status == "started" %}
<td style="background: #5cb85c; font-family: Helvetica, Arial, sans-serif; font-weight: bold; font-size: 10px; line-height: 10px; color: white; padding: 6px; border-radius: 3px;">STARTED</td>
{% elif check.get_status == "down" %}
<td style="background: #d9534f; font-family: Helvetica, Arial, sans-serif; font-weight: bold; font-size: 10px; line-height: 10px; color: white; padding: 6px; border-radius: 3px;">DOWN</td>
{% endif %}
</tr>
</table>
</td>
<td style="border-top: 1px solid #EDEFF2; padding: 16px 8px; font-family: Helvetica, Arial, sans-serif;">
{% if check.name %}
{% if check.name|length > 20 %}
<small>{{ check.name|mangle_link }}</small>
{% else %}
{{ check.name|mangle_link }}
{% endif %}
{% else %}
<span style="color: #74787E; font-style: italic;">unnamed</span>
{% endif %}
{% if check.tags %}
<br />
<table cellpadding="0" cellspacing="0">
<tr>
{% for tag in check.tags_list %}
<td style="padding-right: 4px">
<table cellpadding="0" cellspacing="0">
<tr>
<td style="background: #eee; font-family: Helvetica, Arial, sans-serif; font-size: 10px; line-height: 10px; color: #555; padding: 4px; margin: 0; border-radius: 2px;">
{{ tag|mangle_link }}
</td>
</tr>
</table>
</td>
{% endfor %}
</tr>
</table>
{% endif %}
</td>
{% for boundary, seconds, count in check.outages_by_month %}
{% if count %}
<td style="border-top: 1px solid #EDEFF2; padding: 16px 8px; font-family: Helvetica, Arial, sans-serif;">
{{ count }} outage{{ count|pluralize }}
<span style="font-size: 12px">
<br />
({{ seconds|hc_approx_duration }} total)
</span>
</td>
{% else %}
<td style="border-top: 1px solid #EDEFF2; padding: 16px 8px; font-family: Helvetica, Arial, sans-serif; color: #999;">
All good!
</td>
{% endif %}
{% endfor %}
</tr>
{% endfor %}
{% endfor %}
</table>
<br />

Loading…
Cancel
Save