Merge branch 'checkreports' of github.com:futurecolors/cabot into futurecolors-checkreports
@ -1,5 +1,6 @@
|
||||
from django import template
|
||||
from django.conf import settings
|
||||
from datetime import timedelta
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@ -7,3 +8,9 @@ register = template.Library()
|
||||
@register.simple_tag
|
||||
def jenkins_human_url(jobname):
|
||||
return '{}job/{}/'.format(settings.JENKINS_API, jobname)
|
||||
|
||||
|
||||
@register.filter(name='format_timedelta')
|
||||
def format_timedelta(delta):
|
||||
# Getting rid of microseconds.
|
||||
return str(timedelta(days=delta.days, seconds=delta.seconds))
|
||||
|
@ -1,20 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import requests
|
||||
from cabotapp.alert import _send_hipchat_alert
|
||||
from django.utils import timezone
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.contrib.auth.models import User
|
||||
from django.test.client import Client
|
||||
from cabotapp.models import (
|
||||
StatusCheck, GraphiteStatusCheck, JenkinsStatusCheck,
|
||||
GraphiteStatusCheck, JenkinsStatusCheck,
|
||||
HttpStatusCheck, Service, StatusCheckResult)
|
||||
from cabotapp.views import StatusCheckReportForm
|
||||
from mock import Mock, patch
|
||||
from twilio import rest
|
||||
from django.core import mail
|
||||
from datetime import timedelta
|
||||
from datetime import timedelta, date
|
||||
import json
|
||||
import os
|
||||
|
||||
@ -310,3 +309,17 @@ class TestWebInterface(LocalTestCase):
|
||||
reloaded = Service.objects.get(id=self.service.id)
|
||||
# Still the same
|
||||
self.assertEqual(reloaded.hackpad_id, snippet_link)
|
||||
|
||||
def test_checks_report(self):
|
||||
form = StatusCheckReportForm({
|
||||
'service': self.service.id,
|
||||
'checks': [self.graphite_check.id],
|
||||
'date_from': date.today() - timedelta(days=1),
|
||||
'date_to': date.today(),
|
||||
})
|
||||
self.assertTrue(form.is_valid())
|
||||
checks = form.get_report()
|
||||
self.assertEqual(len(checks), 1)
|
||||
check = checks[0]
|
||||
self.assertEqual(len(check.problems), 1)
|
||||
self.assertEqual(check.success_rate, 50)
|
||||
|
@ -1,5 +1,6 @@
|
||||
from django.template import RequestContext, loader
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, date
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
from django.core.urlresolvers import reverse_lazy
|
||||
from django.conf import settings
|
||||
@ -7,19 +8,20 @@ from models import (
|
||||
StatusCheck, GraphiteStatusCheck, JenkinsStatusCheck, HttpStatusCheck,
|
||||
StatusCheckResult, UserProfile, Service, Shift, get_duty_officers)
|
||||
from tasks import run_status_check as _run_status_check
|
||||
from tasks import update_service as _update_service
|
||||
from tasks import run_all_checks as _run_all_checks
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.generic import DetailView, CreateView, UpdateView, ListView, DeleteView
|
||||
from django.views.generic import (
|
||||
DetailView, CreateView, UpdateView, ListView, DeleteView, TemplateView)
|
||||
from django import forms
|
||||
from .graphite import get_data, get_matching_metrics
|
||||
from .alert import telephone_alert_twiml_callback
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils import timezone
|
||||
from django.utils.timezone import utc
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from itertools import groupby, dropwhile, izip_longest
|
||||
import requests
|
||||
import json
|
||||
import re
|
||||
@ -238,6 +240,42 @@ class ServiceForm(forms.ModelForm):
|
||||
raise ValidationError('Please specify a valid JS snippet link')
|
||||
|
||||
|
||||
class StatusCheckReportForm(forms.Form):
|
||||
service = forms.ModelChoiceField(
|
||||
queryset=Service.objects.all(),
|
||||
widget=forms.HiddenInput
|
||||
)
|
||||
checks = forms.ModelMultipleChoiceField(
|
||||
queryset=StatusCheck.objects.all(),
|
||||
widget=forms.SelectMultiple(
|
||||
attrs={
|
||||
'data-rel': 'chosen',
|
||||
'style': 'width: 70%',
|
||||
},
|
||||
)
|
||||
)
|
||||
date_from = forms.DateField(label='From', widget=forms.DateInput(attrs={'class': 'datepicker'}))
|
||||
date_to = forms.DateField(label='To', widget=forms.DateInput(attrs={'class': 'datepicker'}))
|
||||
|
||||
def get_report(self):
|
||||
checks = self.cleaned_data['checks']
|
||||
for check in checks:
|
||||
# Group results of the check by status (failed alternating with succeeded),
|
||||
# take time of the first one in each group (starting from a failed group),
|
||||
# split them into pairs and form the list of problems.
|
||||
results = check.statuscheckresult_set.filter(
|
||||
time__gte=self.cleaned_data['date_from'],
|
||||
time__lt=self.cleaned_data['date_to'] + timedelta(days=1)
|
||||
).order_by('time')
|
||||
groups = dropwhile(lambda item: item[0], groupby(results, key=lambda r: r.succeeded))
|
||||
times = [next(group).time for succeeded, group in groups]
|
||||
pairs = izip_longest(*([iter(times)] * 2), fillvalue=timezone.now())
|
||||
check.problems = [(start, end - start) for start, end in pairs]
|
||||
if results:
|
||||
check.success_rate = results.filter(succeeded=True).count() / float(len(results)) * 100
|
||||
return checks
|
||||
|
||||
|
||||
class CheckCreateView(LoginRequiredMixin, CreateView):
|
||||
template_name = 'cabotapp/statuscheck_form.html'
|
||||
|
||||
@ -366,6 +404,17 @@ class ServiceDetailView(LoginRequiredMixin, DetailView):
|
||||
model = Service
|
||||
context_object_name = 'service'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(ServiceDetailView, self).get_context_data(**kwargs)
|
||||
date_from = date.today() - relativedelta(day=1)
|
||||
context['report_form'] = StatusCheckReportForm(initial={
|
||||
'checks': self.object.status_checks.all(),
|
||||
'service': self.object,
|
||||
'date_from': date_from,
|
||||
'date_to': date_from + relativedelta(months=1) - relativedelta(days=1)
|
||||
})
|
||||
return context
|
||||
|
||||
|
||||
class ServiceCreateView(LoginRequiredMixin, CreateView):
|
||||
model = Service
|
||||
@ -400,6 +449,15 @@ class ShiftListView(LoginRequiredMixin, ListView):
|
||||
deleted=False).order_by('start')
|
||||
|
||||
|
||||
class StatusCheckReportView(LoginRequiredMixin, TemplateView):
|
||||
template_name = 'cabotapp/statuscheck_report.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
form = StatusCheckReportForm(self.request.GET)
|
||||
if form.is_valid():
|
||||
return {'checks': form.get_report(), 'service': form.cleaned_data['service']}
|
||||
|
||||
|
||||
# Misc JSON api and other stuff
|
||||
|
||||
def twiml_callback(request, service_id):
|
||||
|
@ -70,6 +70,47 @@
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="col-xs-1"><h3><i class="fa fa-table"></i></h3></div>
|
||||
<div class="col-xs-11">
|
||||
<h3>Status check report</h3>
|
||||
</div>
|
||||
<div class="col-xs-12">
|
||||
<form action="{% url checks-report %}" method="get">
|
||||
<div class="form-group">
|
||||
<div class="col-xs-12">
|
||||
{{ report_form.service }}
|
||||
<label class="col-xs-2 control-label">{{ report_form.checks.label_tag }}</label>
|
||||
<div class="col-xs-10">{{ report_form.checks }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-xs-12">
|
||||
<label class="col-xs-2 control-label">{{ report_form.date_from.label_tag }}</label>
|
||||
<div class="col-xs-10">{{ report_form.date_from }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-xs-12">
|
||||
<label class="col-xs-2 control-label">{{ report_form.date_to.label_tag }}</label>
|
||||
<div class="col-xs-10">{{ report_form.date_to }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12">
|
||||
<div class="form-group">
|
||||
<div class="col-xs-6 col-xs-offset-2">
|
||||
<button type="submit" class="btn btn-primary">Get report</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="col-xs-1"><h3><i class="fa fa-exclamation-triangle"></i></h3></div>
|
||||
@ -156,6 +197,16 @@ drawRickshaw = (data, labels, events = []) ->
|
||||
annotator.add evt.time, evt.message
|
||||
annotator.update()
|
||||
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
$(function(){
|
||||
$(':input.datepicker').datepicker({
|
||||
dateFormat: 'yy-mm-dd',
|
||||
buttonImage: '{{ STATIC_URL }}theme/img/calendar.gif',
|
||||
buttonImageOnly: true,
|
||||
showOn: 'button'
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endcompress %}
|
||||
{% endblock js %}
|
||||
{% endblock js %}
|
||||
|
@ -21,9 +21,8 @@
|
||||
<a class="btn btn-danger" href="{% url delete-service form.instance.id %}">Delete service</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
|
49
app/templates/cabotapp/statuscheck_report.html
Normal file
@ -0,0 +1,49 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% load extra %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="col-xs-1"><h2><i class="fa fa-gears"></i></h2></div>
|
||||
<div class="col-xs-5"><h2><span class="break"></span>{{ service.name }}</h2></div>
|
||||
</div>
|
||||
</div>
|
||||
{% if not checks %}
|
||||
No checks
|
||||
{% else %}
|
||||
{% for check in checks %}
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="col-xs-1"><h3><i class="glyphicon glyphicon-{% if check.polymorphic_ctype.model == 'graphitestatuscheck' %}signal{% elif check.polymorphic_ctype.model == 'httpstatuscheck' %}arrow-up{% elif check.polymorphic_ctype.model == 'jenkinsstatuscheck' %}ok{% endif %}"></i></h3></div>
|
||||
<div class="col-xs-11"><h3>{{ check.name }}</h3></div>
|
||||
</div>
|
||||
</div>
|
||||
{% if check.success_rate != None %}
|
||||
<h4>Success rate: {{ check.success_rate|floatformat:"2" }}%.</h4>
|
||||
{% endif %}
|
||||
{% if check.problems %}
|
||||
<table class="table bootstrap-datatable datatable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Duration</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for start_time, duration in check.problems %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ start_time }}
|
||||
</td>
|
||||
<td>
|
||||
{{ duration|format_timedelta }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endblock content %}
|
@ -5,7 +5,7 @@ from cabotapp.views import (
|
||||
HttpCheckCreateView, HttpCheckUpdateView,
|
||||
JenkinsCheckCreateView, JenkinsCheckUpdateView,
|
||||
StatusCheckDeleteView, StatusCheckListView, StatusCheckDetailView,
|
||||
StatusCheckResultDetailView)
|
||||
StatusCheckResultDetailView, StatusCheckReportView)
|
||||
from cabotapp.views import (ServiceListView, ServiceDetailView,
|
||||
ServiceUpdateView, ServiceCreateView, ServiceDeleteView,
|
||||
UserProfileUpdateView, ShiftListView, subscriptions)
|
||||
@ -43,7 +43,7 @@ urlpatterns = patterns('',
|
||||
url(r'^service/(?P<pk>\d+)/',
|
||||
view=ServiceDetailView.as_view(), name='service'),
|
||||
|
||||
url(r'^checks/', view=StatusCheckListView.as_view(),
|
||||
url(r'^checks/$', view=StatusCheckListView.as_view(),
|
||||
name='checks'),
|
||||
url(r'^check/run/(?P<pk>\d+)/',
|
||||
view=run_status_check, name='run-check'),
|
||||
@ -52,6 +52,8 @@ urlpatterns = patterns('',
|
||||
), name='delete-check'),
|
||||
url(r'^check/(?P<pk>\d+)/',
|
||||
view=StatusCheckDetailView.as_view(), name='check'),
|
||||
url(r'^checks/report/$',
|
||||
view=StatusCheckReportView.as_view(), name='checks-report'),
|
||||
|
||||
url(r'^graphitecheck/create/',
|
||||
view=GraphiteCheckCreateView.as_view(
|
||||
|
@ -29,4 +29,5 @@ redis==2.9.0
|
||||
requests==0.14.2
|
||||
six==1.5.1
|
||||
twilio==3.4.1
|
||||
wsgiref==0.1.2
|
||||
wsgiref==0.1.2
|
||||
python-dateutil==2.1
|
||||
|
BIN
static/theme/img/animated-overlay.gif
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
static/theme/img/calendar.gif
Normal file
After Width: | Height: | Size: 269 B |
BIN
static/theme/img/ui-bg_flat_0_aaaaaa_40x100.png
Normal file
After Width: | Height: | Size: 180 B |
BIN
static/theme/img/ui-bg_flat_75_ffffff_40x100.png
Normal file
After Width: | Height: | Size: 178 B |
BIN
static/theme/img/ui-bg_glass_55_fbf9ee_1x400.png
Normal file
After Width: | Height: | Size: 120 B |
BIN
static/theme/img/ui-bg_glass_65_ffffff_1x400.png
Normal file
After Width: | Height: | Size: 105 B |
BIN
static/theme/img/ui-bg_glass_75_dadada_1x400.png
Normal file
After Width: | Height: | Size: 111 B |
BIN
static/theme/img/ui-bg_glass_75_e6e6e6_1x400.png
Normal file
After Width: | Height: | Size: 110 B |
BIN
static/theme/img/ui-bg_glass_95_fef1ec_1x400.png
Normal file
After Width: | Height: | Size: 119 B |
BIN
static/theme/img/ui-bg_highlight-soft_75_cccccc_1x100.png
Normal file
After Width: | Height: | Size: 101 B |
BIN
static/theme/img/ui-icons_222222_256x240.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
static/theme/img/ui-icons_2e83ff_256x240.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
static/theme/img/ui-icons_454545_256x240.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
static/theme/img/ui-icons_888888_256x240.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
static/theme/img/ui-icons_cd0a0a_256x240.png
Normal file
After Width: | Height: | Size: 4.3 KiB |