Merge branch 'checkreports' of github.com:futurecolors/cabot into futurecolors-checkreports

This commit is contained in:
David Buxton 2014-03-03 12:13:00 +00:00
commit 1228f3cb54
23 changed files with 194 additions and 14 deletions

View File

@ -1,5 +1,6 @@
from django import template from django import template
from django.conf import settings from django.conf import settings
from datetime import timedelta
register = template.Library() register = template.Library()
@ -7,3 +8,9 @@ register = template.Library()
@register.simple_tag @register.simple_tag
def jenkins_human_url(jobname): def jenkins_human_url(jobname):
return '{}job/{}/'.format(settings.JENKINS_API, 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))

View File

@ -1,20 +1,19 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import requests import requests
from cabotapp.alert import _send_hipchat_alert
from django.utils import timezone from django.utils import timezone
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import TestCase from django.test import TestCase
from django.core.exceptions import ValidationError
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test.client import Client from django.test.client import Client
from cabotapp.models import ( from cabotapp.models import (
StatusCheck, GraphiteStatusCheck, JenkinsStatusCheck, GraphiteStatusCheck, JenkinsStatusCheck,
HttpStatusCheck, Service, StatusCheckResult) HttpStatusCheck, Service, StatusCheckResult)
from cabotapp.views import StatusCheckReportForm
from mock import Mock, patch from mock import Mock, patch
from twilio import rest from twilio import rest
from django.core import mail from django.core import mail
from datetime import timedelta from datetime import timedelta, date
import json import json
import os import os
@ -310,3 +309,17 @@ class TestWebInterface(LocalTestCase):
reloaded = Service.objects.get(id=self.service.id) reloaded = Service.objects.get(id=self.service.id)
# Still the same # Still the same
self.assertEqual(reloaded.hackpad_id, snippet_link) 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)

View File

@ -1,5 +1,6 @@
from django.template import RequestContext, loader 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.http import HttpResponse, HttpResponseRedirect
from django.core.urlresolvers import reverse_lazy from django.core.urlresolvers import reverse_lazy
from django.conf import settings from django.conf import settings
@ -7,19 +8,20 @@ from models import (
StatusCheck, GraphiteStatusCheck, JenkinsStatusCheck, HttpStatusCheck, StatusCheck, GraphiteStatusCheck, JenkinsStatusCheck, HttpStatusCheck,
StatusCheckResult, UserProfile, Service, Shift, get_duty_officers) StatusCheckResult, UserProfile, Service, Shift, get_duty_officers)
from tasks import run_status_check as _run_status_check 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.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator 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 django import forms
from .graphite import get_data, get_matching_metrics from .graphite import get_data, get_matching_metrics
from .alert import telephone_alert_twiml_callback from .alert import telephone_alert_twiml_callback
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils import timezone
from django.utils.timezone import utc from django.utils.timezone import utc
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from itertools import groupby, dropwhile, izip_longest
import requests import requests
import json import json
import re import re
@ -238,6 +240,42 @@ class ServiceForm(forms.ModelForm):
raise ValidationError('Please specify a valid JS snippet link') 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): class CheckCreateView(LoginRequiredMixin, CreateView):
template_name = 'cabotapp/statuscheck_form.html' template_name = 'cabotapp/statuscheck_form.html'
@ -366,6 +404,17 @@ class ServiceDetailView(LoginRequiredMixin, DetailView):
model = Service model = Service
context_object_name = '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): class ServiceCreateView(LoginRequiredMixin, CreateView):
model = Service model = Service
@ -400,6 +449,15 @@ class ShiftListView(LoginRequiredMixin, ListView):
deleted=False).order_by('start') 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 # Misc JSON api and other stuff
def twiml_callback(request, service_id): def twiml_callback(request, service_id):

View File

@ -70,6 +70,47 @@
<hr> <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="row">
<div class="col-xs-12"> <div class="col-xs-12">
<div class="col-xs-1"><h3><i class="fa fa-exclamation-triangle"></i></h3></div> <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.add evt.time, evt.message
annotator.update() 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> </script>
{% endcompress %} {% endcompress %}
{% endblock js %} {% endblock js %}

View File

@ -21,9 +21,8 @@
<a class="btn btn-danger" href="{% url delete-service form.instance.id %}">Delete service</a> <a class="btn btn-danger" href="{% url delete-service form.instance.id %}">Delete service</a>
</div> </div>
{% endif %} {% endif %}
</div>
</div> </div>
</form> </div>
</form> </form>
{% endblock %} {% endblock %}

View 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 %}

View File

@ -5,7 +5,7 @@ from cabotapp.views import (
HttpCheckCreateView, HttpCheckUpdateView, HttpCheckCreateView, HttpCheckUpdateView,
JenkinsCheckCreateView, JenkinsCheckUpdateView, JenkinsCheckCreateView, JenkinsCheckUpdateView,
StatusCheckDeleteView, StatusCheckListView, StatusCheckDetailView, StatusCheckDeleteView, StatusCheckListView, StatusCheckDetailView,
StatusCheckResultDetailView) StatusCheckResultDetailView, StatusCheckReportView)
from cabotapp.views import (ServiceListView, ServiceDetailView, from cabotapp.views import (ServiceListView, ServiceDetailView,
ServiceUpdateView, ServiceCreateView, ServiceDeleteView, ServiceUpdateView, ServiceCreateView, ServiceDeleteView,
UserProfileUpdateView, ShiftListView, subscriptions) UserProfileUpdateView, ShiftListView, subscriptions)
@ -43,7 +43,7 @@ urlpatterns = patterns('',
url(r'^service/(?P<pk>\d+)/', url(r'^service/(?P<pk>\d+)/',
view=ServiceDetailView.as_view(), name='service'), view=ServiceDetailView.as_view(), name='service'),
url(r'^checks/', view=StatusCheckListView.as_view(), url(r'^checks/$', view=StatusCheckListView.as_view(),
name='checks'), name='checks'),
url(r'^check/run/(?P<pk>\d+)/', url(r'^check/run/(?P<pk>\d+)/',
view=run_status_check, name='run-check'), view=run_status_check, name='run-check'),
@ -52,6 +52,8 @@ urlpatterns = patterns('',
), name='delete-check'), ), name='delete-check'),
url(r'^check/(?P<pk>\d+)/', url(r'^check/(?P<pk>\d+)/',
view=StatusCheckDetailView.as_view(), name='check'), view=StatusCheckDetailView.as_view(), name='check'),
url(r'^checks/report/$',
view=StatusCheckReportView.as_view(), name='checks-report'),
url(r'^graphitecheck/create/', url(r'^graphitecheck/create/',
view=GraphiteCheckCreateView.as_view( view=GraphiteCheckCreateView.as_view(

View File

@ -29,4 +29,5 @@ redis==2.9.0
requests==0.14.2 requests==0.14.2
six==1.5.1 six==1.5.1
twilio==3.4.1 twilio==3.4.1
wsgiref==0.1.2 wsgiref==0.1.2
python-dateutil==2.1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB