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.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))

View File

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

View File

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

View File

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

View File

@ -24,7 +24,6 @@
</div>
</div>
</form>
</form>
{% endblock %}
{% load compress %}

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,
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(

View File

@ -30,3 +30,4 @@ requests==0.14.2
six==1.5.1
twilio==3.4.1
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