0
0
Fork 0
mirror of https://github.com/healthchecks/healthchecks.git synced 2025-04-03 04:15:29 +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)
- Switch from requests to pycurl
- Implement documentation search
- Add date filters in the Log page
### Bug Fixes
- 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 re
from urllib.parse import quote, urlencode
@ -337,3 +337,14 @@ class AddGotifyForm(forms.Form):
class SearchForm(forms.Form):
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)
def _get_events(check, limit):
pings = Ping.objects.filter(owner=check).order_by("-id")[:limit]
pings = list(pings)
def _get_min(check):
min_n = check.n_pings - check.project.owner_profile.ping_log_limit
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
for ping in reversed(pings):
@ -694,12 +711,15 @@ def _get_events(check, limit):
if delta < MAX_DELTA:
setattr(ping, "delta", delta)
alerts = []
if len(pings):
alerts = Notification.objects.select_related("channel")
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
alerts = Notification.objects.select_related("channel").filter(
owner=check, check_status="down", created__gt=cutoff
)
alerts = alerts.filter(created__gt=cutoff)
else:
alerts = []
events = pings + list(alerts)
events.sort(key=lambda el: el.created, reverse=True)
@ -710,13 +730,26 @@ def _get_events(check, limit):
def log(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 = {
"page": "log",
"project": check.project,
"check": check,
"events": _get_events(check, limit),
"limit": limit,
"show_limit_notice": check.n_pings > limit and settings.USE_PAYMENTS,
"min": smin,
"max": smax,
"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)

View file

@ -75,3 +75,21 @@
#log tr.missing td:nth-child(4) {
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;
}
#period-slider.noUi-connect {
#period-slider .noUi-connect {
background: #22bc66;
}
@ -140,13 +140,13 @@
margin: 20px 50px 110px 50px;
}
#grace-slider.noUi-connect {
#grace-slider .noUi-connect {
background: #f0ad4e;
}
#period-slider .noUi-value, #grace-slider .noUi-value {
width: 60px;
margin-left: -30px;
font-size: 12px;
padding-top: 5px;
}
.update-timeout-terms {

View file

@ -1,9 +1,5 @@
body.dark .noUi-background {
background-color: #3c3939;
box-shadow: none;
}
body.dark .noUi-target {
background-color: #3c3939;
border-color: #515151;
box-shadow: none;
}
@ -25,3 +21,7 @@ body.dark .noUi-marker {
body.dark .noUi-pips {
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-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}
.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}

View file

@ -1,4 +1,96 @@
$(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() {
var n = $("td", this).first().text();
var tmpl = $("#log").data("url").slice(0, -2);
@ -7,10 +99,12 @@ $(function () {
});
function switchDateFormat(format) {
document.querySelectorAll("#log tr").forEach(function(row) {
var dt = moment.unix(row.dataset.dt).utc();
format == "local" ? dt.local() : dt.tz(format);
dateFormat = format;
slider.noUiSlider.updateOptions({}, true);
updateSliderPreview();
document.querySelectorAll("#log tr").forEach(function(row) {
var dt = fromUnix(row.dataset.dt);
row.children[1].textContent = dt.format("MMM D");
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>
</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 %}
<div class="table-responsive">
<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 %}
</table>
</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 %}
</div>
</div>
@ -75,6 +86,7 @@
{% compress js %}
<script src="{% static 'js/jquery-3.6.0.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-timezone-with-data-10-year-range.min.js' %}"></script>
<script src="{% static 'js/purify.min.js' %}"></script>