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
|
# 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
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
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