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
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


+ 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.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)


+ 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.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)


+ 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.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)


+ 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):
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

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

@ -20,7 +20,12 @@ Hello,<br />
{% endif %}
<br />
{% include "emails/summary-html.html" %}
{% if nag %}
{% include "emails/summary-html.html" %}
{% else %}
{% include "emails/summary-downtimes-html.html" %}
{% endif %}
{% if nag %}
<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