forked from GithubBackups/healthchecks
Experimental: show the number of outages and total downtime in monthly reports. (#104)
This commit is contained in:
parent
e174e1ef4c
commit
b74e56a273
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
templates/emails/summary-downtimes-html.html
Normal file
84
templates/emails/summary-downtimes-html.html
Normal 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…
x
Reference in New Issue
Block a user