0
0
Fork 0
mirror of https://github.com/healthchecks/healthchecks.git synced 2025-04-07 06:05:34 +00:00

Add date filters in the Log page

This commit is contained in:
Pēteris Caune 2022-09-09 14:16:17 +03:00
parent 1b7bdea9e9
commit 37bbe5a9c7
No known key found for this signature in database
GPG key ID: E28D7679E9A9EDE2
10 changed files with 209 additions and 45 deletions

View file

@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
- Add support for EMAIL_USE_SSL environment variable (#685) - Add support for EMAIL_USE_SSL environment variable (#685)
- Switch from requests to pycurl - Switch from requests to pycurl
- Implement documentation search - Implement documentation search
- Add date filters in the Log page
### Bug Fixes ### Bug Fixes
- Fix the handling of TooManyRedirects exceptions - Fix the handling of TooManyRedirects exceptions

View file

@ -1,4 +1,4 @@
from datetime import timedelta as td from datetime import datetime, timedelta as td, timezone
import json import json
import re import re
from urllib.parse import quote, urlencode from urllib.parse import quote, urlencode
@ -337,3 +337,14 @@ class AddGotifyForm(forms.Form):
class SearchForm(forms.Form): class SearchForm(forms.Form):
q = forms.RegexField(regex=r"^[0-9a-zA-Z\s]{3,100}$") q = forms.RegexField(regex=r"^[0-9a-zA-Z\s]{3,100}$")
class SeekForm(forms.Form):
start = forms.IntegerField()
end = forms.IntegerField()
def clean_start(self):
return datetime.fromtimestamp(self.cleaned_data["start"], tz=timezone.utc)
def clean_end(self):
return datetime.fromtimestamp(self.cleaned_data["end"], tz=timezone.utc)

View file

@ -680,9 +680,26 @@ def remove_check(request, code):
return redirect("hc-checks", project.code) return redirect("hc-checks", project.code)
def _get_events(check, limit): def _get_min(check):
pings = Ping.objects.filter(owner=check).order_by("-id")[:limit] min_n = check.n_pings - check.project.owner_profile.ping_log_limit
pings = list(pings) ping = check.ping_set.filter(n__gt=min_n).first()
dt = ping.created if ping else now()
return (dt - td(hours=24)).replace(minute=0, second=0)
def _get_total(check, start, end):
ping_log_limit = check.project.owner_profile.ping_log_limit
pings = check.ping_set.filter(created__gte=start, created__lte=end)
return min(pings.count(), ping_log_limit)
def _get_events(check, page_limit, start=None, end=None):
min_n = check.n_pings - check.project.owner_profile.ping_log_limit
pings = check.ping_set.filter(n__gt=min_n).order_by("-id")
if start and end:
pings = pings.filter(created__gte=start, created__lte=end)
pings = list(pings[:page_limit])
last_start = None last_start = None
for ping in reversed(pings): for ping in reversed(pings):
@ -694,12 +711,15 @@ def _get_events(check, limit):
if delta < MAX_DELTA: if delta < MAX_DELTA:
setattr(ping, "delta", delta) setattr(ping, "delta", delta)
alerts = [] alerts = Notification.objects.select_related("channel")
if len(pings): alerts = alerts.filter(owner=check, check_status="down")
if start and end:
alerts = alerts.filter(created__gte=start, created__lte=end)
elif len(pings):
cutoff = pings[-1].created cutoff = pings[-1].created
alerts = Notification.objects.select_related("channel").filter( alerts = alerts.filter(created__gt=cutoff)
owner=check, check_status="down", created__gt=cutoff else:
) alerts = []
events = pings + list(alerts) events = pings + list(alerts)
events.sort(key=lambda el: el.created, reverse=True) events.sort(key=lambda el: el.created, reverse=True)
@ -710,13 +730,26 @@ def _get_events(check, limit):
def log(request, code): def log(request, code):
check, rw = _get_check_for_user(request, code) check, rw = _get_check_for_user(request, code)
limit = check.project.owner_profile.ping_log_limit form = forms.SeekForm(request.GET)
smin, smax = _get_min(check), now()
if form.is_valid():
start = form.cleaned_data["start"]
end = form.cleaned_data["end"]
else:
start = smin
end = smax
ctx = { ctx = {
"page": "log",
"project": check.project, "project": check.project,
"check": check, "check": check,
"events": _get_events(check, limit), "min": smin,
"limit": limit, "max": smax,
"show_limit_notice": check.n_pings > limit and settings.USE_PAYMENTS, "start": start,
"end": end,
"events": _get_events(check, 1000, start=start, end=end),
"num_total": _get_total(check, start, end),
} }
return render(request, "front/log.html", ctx) return render(request, "front/log.html", ctx)

View file

@ -75,3 +75,21 @@
#log tr.missing td:nth-child(4) { #log tr.missing td:nth-child(4) {
font-size: 12px; font-size: 12px;
} }
#log-slider {
margin-bottom: 60px;
}
#log-slider .noUi-value {
font-size: 12px;
padding-top: 5px;
}
#log-slider .noUi-connect {
background-color: var(--link-color);
}
#slider-from-formatted,
#slider-to-formatted {
font-weight: bold;
}

View file

@ -132,7 +132,7 @@
margin: 20px 50px 90px 50px; margin: 20px 50px 90px 50px;
} }
#period-slider.noUi-connect { #period-slider .noUi-connect {
background: #22bc66; background: #22bc66;
} }
@ -140,13 +140,13 @@
margin: 20px 50px 110px 50px; margin: 20px 50px 110px 50px;
} }
#grace-slider.noUi-connect { #grace-slider .noUi-connect {
background: #f0ad4e; background: #f0ad4e;
} }
#period-slider .noUi-value, #grace-slider .noUi-value { #period-slider .noUi-value, #grace-slider .noUi-value {
width: 60px; font-size: 12px;
margin-left: -30px; padding-top: 5px;
} }
.update-timeout-terms { .update-timeout-terms {

View file

@ -1,9 +1,5 @@
body.dark .noUi-background {
background-color: #3c3939;
box-shadow: none;
}
body.dark .noUi-target { body.dark .noUi-target {
background-color: #3c3939;
border-color: #515151; border-color: #515151;
box-shadow: none; box-shadow: none;
} }
@ -25,3 +21,7 @@ body.dark .noUi-marker {
body.dark .noUi-pips { body.dark .noUi-pips {
color: hsl(240, 3%, 55%); color: hsl(240, 3%, 55%);
} }
body.dark .noUi-value-sub {
color: hsl(240, 3%, 45%);
}

View file

@ -1,4 +1 @@
/*! nouislider - 8.0.2 - 2015-07-06 13:22:09 */ .noUi-target,.noUi-target *{-webkit-touch-callout:none;-webkit-tap-highlight-color:transparent;-webkit-user-select:none;-ms-touch-action:none;touch-action:none;-ms-user-select:none;-moz-user-select:none;user-select:none;-moz-box-sizing:border-box;box-sizing:border-box}.noUi-target{position:relative}.noUi-base,.noUi-connects{width:100%;height:100%;position:relative;z-index:1}.noUi-connects{overflow:hidden;z-index:0}.noUi-connect,.noUi-origin{will-change:transform;position:absolute;z-index:1;top:0;right:0;height:100%;width:100%;-ms-transform-origin:0 0;-webkit-transform-origin:0 0;-webkit-transform-style:preserve-3d;transform-origin:0 0;transform-style:flat}.noUi-txt-dir-rtl.noUi-horizontal .noUi-origin{left:0;right:auto}.noUi-vertical .noUi-origin{top:-100%;width:0}.noUi-horizontal .noUi-origin{height:0}.noUi-handle{-webkit-backface-visibility:hidden;backface-visibility:hidden;position:absolute}.noUi-touch-area{height:100%;width:100%}.noUi-state-tap .noUi-connect,.noUi-state-tap .noUi-origin{-webkit-transition:transform .3s;transition:transform .3s}.noUi-state-drag *{cursor:inherit!important}.noUi-horizontal{height:18px}.noUi-horizontal .noUi-handle{width:34px;height:28px;right:-17px;top:-6px}.noUi-vertical{width:18px}.noUi-vertical .noUi-handle{width:28px;height:34px;right:-6px;bottom:-17px}.noUi-txt-dir-rtl.noUi-horizontal .noUi-handle{left:-17px;right:auto}.noUi-target{background:#FAFAFA;border-radius:4px;border:1px solid #D3D3D3;box-shadow:inset 0 1px 1px #F0F0F0,0 3px 6px -5px #BBB}.noUi-connects{border-radius:3px}.noUi-connect{background:#3FB8AF}.noUi-draggable{cursor:ew-resize}.noUi-vertical .noUi-draggable{cursor:ns-resize}.noUi-handle{border:1px solid #D9D9D9;border-radius:3px;background:#FFF;cursor:default;box-shadow:inset 0 0 1px #FFF,inset 0 1px 7px #EBEBEB,0 3px 6px -3px #BBB}.noUi-active{box-shadow:inset 0 0 1px #FFF,inset 0 1px 7px #DDD,0 3px 6px -3px #BBB}.noUi-handle:after,.noUi-handle:before{content:"";display:block;position:absolute;height:14px;width:1px;background:#E8E7E6;left:14px;top:6px}.noUi-handle:after{left:17px}.noUi-vertical .noUi-handle:after,.noUi-vertical .noUi-handle:before{width:14px;height:1px;left:6px;top:14px}.noUi-vertical .noUi-handle:after{top:17px}[disabled] .noUi-connect{background:#B8B8B8}[disabled] .noUi-handle,[disabled].noUi-handle,[disabled].noUi-target{cursor:not-allowed}.noUi-pips,.noUi-pips *{-moz-box-sizing:border-box;box-sizing:border-box}.noUi-pips{position:absolute;color:#999}.noUi-value{position:absolute;white-space:nowrap;text-align:center}.noUi-value-sub{color:#ccc;font-size:10px}.noUi-marker{position:absolute;background:#CCC}.noUi-marker-sub{background:#AAA}.noUi-marker-large{background:#AAA}.noUi-pips-horizontal{padding:10px 0;height:80px;top:100%;left:0;width:100%}.noUi-value-horizontal{-webkit-transform:translate(-50%,50%);transform:translate(-50%,50%)}.noUi-rtl .noUi-value-horizontal{-webkit-transform:translate(50%,50%);transform:translate(50%,50%)}.noUi-marker-horizontal.noUi-marker{margin-left:-1px;width:2px;height:5px}.noUi-marker-horizontal.noUi-marker-sub{height:10px}.noUi-marker-horizontal.noUi-marker-large{height:15px}.noUi-pips-vertical{padding:0 10px;height:100%;top:0;left:100%}.noUi-value-vertical{-webkit-transform:translate(0,-50%);transform:translate(0,-50%);padding-left:25px}.noUi-rtl .noUi-value-vertical{-webkit-transform:translate(0,50%);transform:translate(0,50%)}.noUi-marker-vertical.noUi-marker{width:5px;height:2px;margin-top:-1px}.noUi-marker-vertical.noUi-marker-sub{width:10px}.noUi-marker-vertical.noUi-marker-large{width:15px}.noUi-tooltip{display:block;position:absolute;border:1px solid #D9D9D9;border-radius:3px;background:#fff;color:#000;padding:5px;text-align:center;white-space:nowrap}.noUi-horizontal .noUi-tooltip{-webkit-transform:translate(-50%,0);transform:translate(-50%,0);left:50%;bottom:120%}.noUi-vertical .noUi-tooltip{-webkit-transform:translate(0,-50%);transform:translate(0,-50%);top:50%;right:120%}.noUi-horizontal .noUi-origin>.noUi-tooltip{-webkit-transform:translate(50%,0);transform:translate(50%,0);left:auto;bottom:10px}.noUi-vertical .noUi-origin>.noUi-tooltip{-webkit-transform:translate(0,-18px);transform:translate(0,-18px);top:auto;right:28px}
.noUi-target,.noUi-target *{-webkit-touch-callout:none;-webkit-user-select:none;-ms-touch-action:none;-ms-user-select:none;-moz-user-select:none;-moz-box-sizing:border-box;box-sizing:border-box}.noUi-target{position:relative;direction:ltr}.noUi-base{width:100%;height:100%;position:relative;z-index:1}.noUi-origin{position:absolute;right:0;top:0;left:0;bottom:0}.noUi-handle{position:relative;z-index:1}.noUi-stacking .noUi-handle{z-index:10}.noUi-state-tap .noUi-origin{-webkit-transition:left .3s,top .3s;transition:left .3s,top .3s}.noUi-state-drag *{cursor:inherit!important}.noUi-base{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.noUi-horizontal{height:18px}.noUi-horizontal .noUi-handle{width:34px;height:28px;left:-17px;top:-6px}.noUi-vertical{width:18px}.noUi-vertical .noUi-handle{width:28px;height:34px;left:-6px;top:-17px}.noUi-background{background:#FAFAFA;box-shadow:inset 0 1px 1px #f0f0f0}.noUi-connect{background:#3FB8AF;box-shadow:inset 0 0 3px rgba(51,51,51,.45);-webkit-transition:background 450ms;transition:background 450ms}.noUi-origin{border-radius:2px}.noUi-target{border-radius:4px;border:1px solid #D3D3D3;box-shadow:inset 0 1px 1px #F0F0F0,0 3px 6px -5px #BBB}.noUi-target.noUi-connect{box-shadow:inset 0 0 3px rgba(51,51,51,.45),0 3px 6px -5px #BBB}.noUi-dragable{cursor:w-resize}.noUi-vertical .noUi-dragable{cursor:n-resize}.noUi-handle{border:1px solid #D9D9D9;border-radius:3px;background:#FFF;cursor:default;box-shadow:inset 0 0 1px #FFF,inset 0 1px 7px #EBEBEB,0 3px 6px -3px #BBB}.noUi-active{box-shadow:inset 0 0 1px #FFF,inset 0 1px 7px #DDD,0 3px 6px -3px #BBB}.noUi-handle:after,.noUi-handle:before{content:"";display:block;position:absolute;height:14px;width:1px;background:#E8E7E6;left:14px;top:6px}.noUi-handle:after{left:17px}.noUi-vertical .noUi-handle:after,.noUi-vertical .noUi-handle:before{width:14px;height:1px;left:6px;top:14px}.noUi-vertical .noUi-handle:after{top:17px}[disabled] .noUi-connect,[disabled].noUi-connect{background:#B8B8B8}[disabled] .noUi-handle,[disabled].noUi-origin{cursor:not-allowed}.noUi-pips,.noUi-pips *{-moz-box-sizing:border-box;box-sizing:border-box}.noUi-pips{position:absolute;font:400 12px Arial;color:#999}.noUi-value{width:40px;position:absolute;text-align:center}.noUi-value-sub{color:#ccc;font-size:10px}.noUi-marker{position:absolute;background:#CCC}.noUi-marker-large,.noUi-marker-sub{background:#AAA}.noUi-pips-horizontal{padding:10px 0;height:50px;top:100%;left:0;width:100%}.noUi-value-horizontal{margin-left:-20px;padding-top:20px}.noUi-value-horizontal.noUi-value-sub{padding-top:15px}.noUi-marker-horizontal.noUi-marker{margin-left:-1px;width:2px;height:5px}.noUi-marker-horizontal.noUi-marker-sub{height:10px}.noUi-marker-horizontal.noUi-marker-large{height:15px}.noUi-pips-vertical{padding:0 10px;height:100%;top:0;left:100%}.noUi-value-vertical{width:15px;margin-left:20px;margin-top:-5px}.noUi-marker-vertical.noUi-marker{width:5px;height:2px;margin-top:-1px}.noUi-marker-vertical.noUi-marker-sub{width:10px}.noUi-marker-vertical.noUi-marker-large{width:15px}

View file

@ -1,4 +1,96 @@
$(function () { $(function () {
var SMALL_LABEL = 2;
var BIG_LABEL = 1;
var PIP = 0;
var NO_PIP = -1;
var slider = document.getElementById("log-slider");
var smin = parseInt(slider.dataset.min);
var smax = parseInt(slider.dataset.max);
var deltaHours = (smax - smin) / 3600;
var pixelsPerHour = slider.clientWidth / deltaHours;
var pixelsPerDay = pixelsPerHour * 24;
var dayGap = Math.round(0.5 + 80 / pixelsPerDay);
var dateFormat = "local";
function fromUnix(timestamp) {
var dt = moment.unix(timestamp);
dateFormat == "local" ? dt.local() : dt.tz(dateFormat);
return dt;
}
function filterPips(value, type) {
var m = fromUnix(value);
if (m.minute() != 0)
return NO_PIP;
// Date labels on every day
if (pixelsPerDay > 60 && m.hour() == 0)
return BIG_LABEL;
// Date labels every "dayGap" days
if (m.hour() == 0 && m.dayOfYear() % dayGap == 0)
return BIG_LABEL;
// Hour labels on every hour:
if (pixelsPerHour > 40)
return SMALL_LABEL;
// Hour labels every 3 hours:
if (pixelsPerHour > 15 && m.hour() % 3 == 0)
return SMALL_LABEL;
// Hour labels every 6 hours:
if (pixelsPerHour > 5 && m.hour() % 6 == 0)
return SMALL_LABEL;
// Pip on every hour
if (pixelsPerHour > 5)
return PIP;
// Pip on every day
if (pixelsPerDay > 10 && m.hour() == 0)
return PIP;
return NO_PIP;
}
function fmt(ts) {
var pipType = filterPips(ts);
return fromUnix(ts).format(pipType == 2 ? "HH:mm" : "MMM D");
}
noUiSlider.create(slider, {
start: [parseInt(slider.dataset.start), parseInt(slider.dataset.end)],
range: {'min': smin, 'max': smax},
connect: true,
step: 3600,
pips: {
mode: "steps",
density: 3,
filter: filterPips,
format: {
to: fmt,
from: function() {}
}
}
});
function updateSliderPreview() {
var values = slider.noUiSlider.get();
$("#slider-from-formatted").text(fromUnix(values[0]).format("MMMM D, HH:mm"));
$("#slider-to-formatted").text(fromUnix(values[1]).format("MMMM D, HH:mm"));
}
updateSliderPreview();
slider.noUiSlider.on("slide", updateSliderPreview);
slider.noUiSlider.on("change", function(a, b, value) {
$("#seek-start").val(Math.round(value[0]));
$("#seek-end").val(Math.round(value[1]));
$("#seek-form").submit();
});
$("#log tr.ok").on("click", function() { $("#log tr.ok").on("click", function() {
var n = $("td", this).first().text(); var n = $("td", this).first().text();
var tmpl = $("#log").data("url").slice(0, -2); var tmpl = $("#log").data("url").slice(0, -2);
@ -7,10 +99,12 @@ $(function () {
}); });
function switchDateFormat(format) { function switchDateFormat(format) {
document.querySelectorAll("#log tr").forEach(function(row) { dateFormat = format;
var dt = moment.unix(row.dataset.dt).utc(); slider.noUiSlider.updateOptions({}, true);
format == "local" ? dt.local() : dt.tz(format); updateSliderPreview();
document.querySelectorAll("#log tr").forEach(function(row) {
var dt = fromUnix(row.dataset.dt);
row.children[1].textContent = dt.format("MMM D"); row.children[1].textContent = dt.format("MMM D");
row.children[2].textContent = dt.format("HH:mm"); row.children[2].textContent = dt.format("HH:mm");
}) })

File diff suppressed because one or more lines are too long

View file

@ -35,25 +35,36 @@
</li> </li>
</ol> </ol>
<p>Filter events from <strong id="slider-from-formatted"></strong> to <strong id="slider-to-formatted"></strong>.</p>
<div
id="log-slider"
data-min="{{ min|timestamp }}"
data-max="{{ max|timestamp }}"
data-start="{{ start|timestamp }}"
data-end="{{ end|timestamp }}">
</div>
<form id="seek-form" method="get">
<input type="hidden" name="start" id="seek-start">
<input type="hidden" name="end" id="seek-end">
</form>
{% if num_total > 1000 %}
<p class="alert alert-info">Found {{ num_total }} ping events, displaying the most recent 1000.</p>
{% elif num_total > 0 %}
<p class="alert alert-info">Found {{ num_total }} ping event{{ num_total|pluralize }}.</p>
{% elif not events and check.n_pings > 0 %}
<div class="alert alert-info">The filter parameters matched no events.</div>
{% elif check.n_pings == 0 %}
<div class="alert alert-info">Log is empty. This check has not received any pings yet.</div>
{% endif %}
{% if events %} {% if events %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table" id="log" data-url="{% url 'hc-ping-details' check.code '0' %}"> <table class="table" id="log" data-url="{% url 'hc-ping-details' check.code '0' %}">
{% for event in events %}{% include "front/log_row.html" with describe_body=True %}{% endfor %} {% for event in events %}{% include "front/log_row.html" with describe_body=True %}{% endfor %}
</table> </table>
</div> </div>
{% if show_limit_notice and limit < 1000 %}
<p class="alert alert-info">
<strong>Showing last {{ limit }} pings.</strong>
Want to see more?
<a href="{% url 'hc-pricing' %}">
Upgrade your account!
</a>
</p>
{% endif %}
{% else %}
<div class="alert alert-info">Log is empty. This check has not received any pings yet.</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@ -75,6 +86,7 @@
{% compress js %} {% compress js %}
<script src="{% static 'js/jquery-3.6.0.min.js' %}"></script> <script src="{% static 'js/jquery-3.6.0.min.js' %}"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script> <script src="{% static 'js/bootstrap.min.js' %}"></script>
<script src="{% static 'js/nouislider.min.js' %}"></script>
<script src="{% static 'js/moment.min.js' %}"></script> <script src="{% static 'js/moment.min.js' %}"></script>
<script src="{% static 'js/moment-timezone-with-data-10-year-range.min.js' %}"></script> <script src="{% static 'js/moment-timezone-with-data-10-year-range.min.js' %}"></script>
<script src="{% static 'js/purify.min.js' %}"></script> <script src="{% static 'js/purify.min.js' %}"></script>