diff --git a/promgen/forms.py b/promgen/forms.py index 327034177..219da168f 100644 --- a/promgen/forms.py +++ b/promgen/forms.py @@ -4,7 +4,7 @@ import datetime from django import forms - +from dateutil import parser from promgen import models, plugins @@ -37,31 +37,30 @@ class ImportRuleForm(forms.Form): class SilenceForm(forms.Form): def validate_datetime(value): try: - datetime.datetime.strptime(value, '%Y-%m-%d %H:%M') + parser.parse(value) except: raise forms.ValidationError('Invalid timestamp') - next = forms.CharField(required=False) duration = forms.CharField(required=False) - start = forms.CharField(required=False, validators=[validate_datetime]) - stop = forms.CharField(required=False, validators=[validate_datetime]) + startsAt = forms.CharField(required=False, validators=[validate_datetime]) + endsAt = forms.CharField(required=False, validators=[validate_datetime]) comment = forms.CharField(required=False) - created_by = forms.CharField(required=False) + createdBy = forms.CharField(required=False) def clean_comment(self): if self.cleaned_data['comment']: return self.cleaned_data['comment'] return "Silenced from Promgen" - def clean_created_by(self): - if self.cleaned_data['created_by']: - return self.cleaned_data['created_by'] + def clean_createdBy(self): + if self.cleaned_data['createdBy']: + return self.cleaned_data['createdBy'] return "Promgen" def clean(self): duration = self.data.get('duration') - start = self.data.get('start') - stop = self.data.get('stop') + start = self.data.get('startsAt') + stop = self.data.get('endsAt') if duration: # No further validation is required if only duration is set @@ -69,7 +68,7 @@ def clean(self): if not all([start, stop]): raise forms.ValidationError('Both start and end are required') - elif datetime.datetime.strptime(start, '%Y-%m-%d %H:%M') > datetime.datetime.strptime(stop, '%Y-%m-%d %H:%M'): + elif parser.parse(start) > parser.parse(stop): raise forms.ValidationError('Start time and end time is mismatch') diff --git a/promgen/prometheus.py b/promgen/prometheus.py index 8989279e8..20722b81f 100644 --- a/promgen/prometheus.py +++ b/promgen/prometheus.py @@ -433,6 +433,7 @@ def silence(labels, duration=None, **kwargs): else: raise Exception('Unknown time modifier') kwargs['endsAt'] = end.isoformat() + kwargs.pop('startsAt', False) else: local_timezone = pytz.timezone(settings.PROMGEN.get('timezone', 'UTC')) for key in ['startsAt', 'endsAt']: @@ -448,4 +449,7 @@ def silence(labels, duration=None, **kwargs): logger.debug('Sending silence for %s', kwargs) url = urljoin(settings.PROMGEN['alertmanager']['url'], '/api/v1/silences') - util.post(url, json=kwargs).raise_for_status() + response = util.post(url, json=kwargs) + response.raise_for_status() + return response + diff --git a/promgen/proxy.py b/promgen/proxy.py index 6781f3d5a..50276865d 100644 --- a/promgen/proxy.py +++ b/promgen/proxy.py @@ -2,12 +2,18 @@ # These sources are released under the terms of the MIT license: see LICENSE import concurrent.futures +import json import logging +from urllib.parse import urljoin -from django.http import JsonResponse +import requests +from dateutil import parser +from django.conf import settings +from django.http import HttpResponse, JsonResponse +from django.template import defaultfilters from django.views.generic import View from django.views.generic.base import TemplateView -from promgen import models, util +from promgen import forms, models, prometheus, util from requests.exceptions import HTTPError logger = logging.getLogger(__name__) @@ -174,3 +180,70 @@ def get(self, request): return JsonResponse( {"status": "success", "data": {"resultType": resultType, "result": data}} ) + + +class ProxyAlerts(View): + def get(self, request): + alerts = [] + try: + url = urljoin(settings.PROMGEN["alertmanager"]["url"], "/api/v1/alerts") + response = util.get(url) + except requests.exceptions.ConnectionError: + logger.error("Error connecting to %s", url) + return JsonResponse({}) + + data = response.json().get("data", []) + if data is None: + # Return an empty alert-all if there are no active alerts from AM + return JsonResponse({}) + + for alert in data: + alert.setdefault("annotations", {}) + # Humanize dates for frontend + for key in ["startsAt", "endsAt"]: + if key in alert: + alert[key] = parser.parse(alert[key]) + # Convert any links to for frontend + for k, v in alert["annotations"].items(): + alert["annotations"][k] = defaultfilters.urlize(v) + alerts.append(alert) + return JsonResponse({"data": data}, safe=False) + + +class ProxySilences(View): + def get(self, request): + try: + url = urljoin(settings.PROMGEN["alertmanager"]["url"], "/api/v1/silences") + response = util.get(url, params={"silenced": False}) + except requests.exceptions.ConnectionError: + logger.error("Error connecting to %s", url) + return JsonResponse({}) + else: + return HttpResponse(response.content, content_type="application/json") + + def post(self, request): + body = json.loads(request.body.decode("utf-8")) + body.setdefault("comment", "Silenced from Promgen") + body.setdefault("createdBy", request.user.email) + + form = forms.SilenceForm(body) + if not form.is_valid(): + return JsonResponse({"status": form.errors}, status=400) + + response = prometheus.silence(body.pop("labels"), **form.cleaned_data) + + return HttpResponse( + response.text, status=response.status_code, content_type="application/json" + ) + + +class ProxyDeleteSilence(View): + def delete(self, request, silence_id): + url = urljoin( + settings.PROMGEN["alertmanager"]["url"], "/api/v1/silence/%s" % silence_id + ) + response = util.delete(url) + return HttpResponse( + response.text, status=response.status_code, content_type="application/json" + ) + diff --git a/promgen/static/css/promgen.css b/promgen/static/css/promgen.css index 599ffece6..6b1c8b26b 100644 --- a/promgen/static/css/promgen.css +++ b/promgen/static/css/promgen.css @@ -23,3 +23,11 @@ a[rel]:after { grid-gap: 10px; grid-template-columns: repeat(auto-fit, minmax(400px,1fr)); } + +[v-cloak] { + display: none; +} + +.dl-horizontal { + word-wrap: break-word; +} diff --git a/promgen/static/js/promgen.js b/promgen/static/js/promgen.js index 28a97b288..4f955fbca 100644 --- a/promgen/static/js/promgen.js +++ b/promgen/static/js/promgen.js @@ -3,68 +3,15 @@ # These sources are released under the terms of the MIT license: see LICENSE */ -function silence_tag() { - var labels = this.dataset; - var form = $('#silence-form'); - for (var label in labels) { - var value = labels[label]; - console.debug('Adding %s %s', label, value); - - form.find('a.' + label).remove(); - - input = $(''); - input.val(value).attr('name', 'label.' + label); - - span = $('').addClass(label); - span.attr('onclick', 'this.parentNode.removeChild(this); return false;'); - span.text(label + ':' + value); - span.append(input); - span.append($('')); - - form.find('div.labels').append(span); - } - - form.show(); -} - function update_page(data) { for (var key in data) { console.log("Replacing %s", key); var ele = $(data[key]); - ele.find("a.promgen-silence").click(silence_tag); $(key).replaceWith(ele); } } - -function label(key, value) { - var tmpl = document.querySelector('template.label'); - var ele = document.importNode(tmpl.content, true); - var a = ele.querySelector('a'); - a.text = key + ':' + value; - a.dataset[key] = value; - return ele; -} - -function row(href, text) { - var tmpl = document.querySelector('template.alertrow'); - var ele = document.importNode(tmpl.content, true); - var a = ele.querySelector('td a'); - a.href = href; - a.text = text; - return ele; -} - -function annotation(dt, dd) { - var tmpl = document.querySelector('template.annotation'); - var ele = document.importNode(tmpl.content, true); - ele.querySelector('dt').textContent = dt; - ele.querySelector('dd').innerHTML = dd; - return ele; -} - $(document).ready(function() { - $("a.promgen-silence").click(silence_tag); $('[data-toggle="popover"]').popover(); $('[data-toggle="tooltip"]').tooltip(); @@ -89,51 +36,6 @@ $(document).ready(function() { }) }) - $.ajax("/ajax/alert").done(function(alerts){ - var btn = document.getElementById('alert-load'); - var panel = document.getElementById('alert-all'); - - for (var alert_id in alerts) { - var alert = alerts[alert_id]; - var r = row(alert.generatorURL, alert.startsAt); - var labels = annotation('labels', ''); - for (var k in alert.labels) { - var l = label(k, alert.labels[k]); - labels.querySelector('dd').appendChild(l); - } - r.querySelector('dl').appendChild(labels); - - for (var k in alert.annotations) { - var a = annotation(k, alert.annotations[k]); - r.querySelector('dl').appendChild(a); - } - - for (var k in alert.labels) { - var sel = '.promgen-alert[data-'+k+'^="'+alert.labels[k].split(':')[0]+'"]'; - console.debug('Searching for', sel); - var target = document.querySelector('.promgen-alert[data-'+k+'^="'+alert.labels[k].split(':')[0]+'"]'); - if (target) { - target.querySelector('table').appendChild(r.cloneNode(true)); - target.style.display = 'block'; - } - } - panel.querySelector('table').appendChild(r); - } - - if (alerts.length > 0) { - btn.classList.remove('btn-default'); - btn.classList.add('btn-danger'); - btn.text = 'Alerts ' + alerts.length; - } - document.querySelectorAll('a.promgen-silence').forEach(function(ele){ - ele.onclick = silence_tag; - }); - }); - - $.post("/ajax/silence", { - 'referer': window.location.toString() - }).done(update_page); - $('[data-source]').click(function() { var btn = $(this); var query = btn.data('source') === 'self' ? btn.text() : $(btn.data('source')).val(); @@ -178,39 +80,6 @@ $(document).ready(function() { } }); -/* Disable pending refactoring - $("select[data-ajax]").each(function(index) { - var ele = $(this); - var tgt = $(ele.data('target')); - - $.get(ele.data('ajax')).done(function(data){ - data.data.forEach(function(item){ - var option = $('