Skip to content

Commit

Permalink
[CHANGE] Remove old Prometheus v1 rule format #148
Browse files Browse the repository at this point in the history
  • Loading branch information
kfdm authored Jul 3, 2019
2 parents 3ecff37 + 75237d4 commit 4a3af99
Show file tree
Hide file tree
Showing 8 changed files with 19 additions and 185 deletions.
8 changes: 1 addition & 7 deletions promgen/management/commands/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

import logging

from django.conf import settings
from django.core.management.base import BaseCommand
from promgen import prometheus, tasks

Expand All @@ -13,10 +12,6 @@
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument('--reload', action='store_true', help='Trigger Prometheus Reload')
parser.add_argument(
'--format', type=int, dest='version',
default=settings.PROMGEN['prometheus'].get('version', 1),
help='Prometheus rule format. Defaults to promgen.yml version (%(default)s)')
parser.add_argument(
'out',
nargs='?',
Expand All @@ -28,10 +23,9 @@ def handle(self, **kwargs):
tasks.write_rules(
path=kwargs['out'],
reload=kwargs['reload'],
version=kwargs['version']
)
else:
# Since we're already working with utf8 encoded data, we can skip
# the newline ending here
self.stdout.ending = None
self.stdout.write(prometheus.render_rules(version=kwargs['version']))
self.stdout.write(prometheus.render_rules())
103 changes: 5 additions & 98 deletions promgen/prometheus.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import datetime
import json
import logging
import re
import subprocess
import tempfile
from urllib.parse import urljoin
Expand All @@ -16,7 +15,6 @@
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db.models import prefetch_related_objects
from django.template.loader import render_to_string
from django.utils import timezone
from promgen import models, util

Expand Down Expand Up @@ -55,7 +53,7 @@ def check_rules(rules):
raise ValidationError(rendered.decode('utf8') + e.output.decode('utf8'))


def render_rules(rules=None, version=None):
def render_rules(rules=None):
'''
Render rules in a format that Prometheus understands
Expand All @@ -71,8 +69,6 @@ def render_rules(rules=None, version=None):
'''
if rules is None:
rules = models.Rule.objects.filter(enabled=True)
if version is None:
version = settings.PROMGEN['prometheus'].get('version', 1)

prefetch_related_objects(
rules,
Expand All @@ -84,11 +80,6 @@ def render_rules(rules=None, version=None):
'rulelabel_set',
)

# V1 format is a custom format which we render through django templates
# See promgen/tests/examples/import.rule
if version == 1:
return render_to_string('promgen/prometheus.rule', {'rules': rules}).encode('utf-8')

# V2 format is a yaml dictionary which we build and then render
# See promgen/tests/examples/import.rule.yml
rule_list = collections.defaultdict(list)
Expand Down Expand Up @@ -186,6 +177,10 @@ def import_rules_v2(config, content_object=None):
This assumes a dictonary in the 2.x rule format.
See promgen/tests/examples/import.rule.yml for an example
'''
# If not already a dictionary, try to load as YAML
if not isinstance(config, dict):
config = yaml.safe_load(config)

counters = collections.defaultdict(int)
for group in config['groups']:
for r in group['rules']:
Expand Down Expand Up @@ -223,94 +218,6 @@ def import_rules_v2(config, content_object=None):
return dict(counters)


def import_rules_v1(config, content_object=None):
'''
Parse text and extract Prometheus rules
This assumes text in the 1.x rule format.
See promgen/tests/examples/import.rule for an example
'''
# Attemps to match the pattern name="value" for Prometheus labels and annotations
RULE_MATCH = re.compile('((?P<key>\w+)\s*=\s*\"(?P<value>.*?)\")')
counters = collections.defaultdict(int)

def parse_prom(text):
if not text:
return {}
converted = {}
for match, key, value in RULE_MATCH.findall(text.strip().strip('{}')):
converted[key] = value
return converted

tokens = {}
rules = []
for line in config.split('\n'):
line = line.strip()
if not line:
continue
if line.startswith('#'):
continue

keyword, data = line.split(' ', 1)

if keyword != 'ALERT':
tokens[keyword] = data
continue

if keyword == 'ALERT' and 'ALERT' not in tokens:
tokens[keyword] = data
continue

rules.append(tokens)
# Start building our next rule
tokens = {keyword: data}
# Make sure we keep our last token after parsing all lines
rules.append(tokens)

for tokens in rules:
labels = parse_prom(tokens.get('LABELS'))
annotations = parse_prom(tokens.get('ANNOTATIONS'))

# Check our labels to see if we have a project or service
# label set and if not, default it to a global rule
if content_object:
obj = content_object
elif 'project' in labels:
obj = models.Project.objects.get(name=labels['project'])
elif 'service' in labels:
obj = models.Service.objects.get(name=labels['service'])
else:
obj = models.Site.objects.get_current()

rule, created = models.Rule.objects.get_or_create(
name=tokens['ALERT'],
defaults={
'clause': tokens['IF'],
'duration': tokens['FOR'],
'obj': obj,
}
)

if created:
counters['Rules'] += 1
for k, v in labels.items():
rule.add_label(k, v)
for k, v in annotations.items():
rule.add_annotation(k, v)

return dict(counters)


def import_rules(config, content_object=None):
try:
data = yaml.safe_load(config)
except Exception as e:
logger.debug('If we fail to parse yaml, then assume it is v1 format')
return import_rules_v1(config, content_object)
else:
return import_rules_v2(data, content_object)


def import_config(config, replace_shard=None):
counters = collections.defaultdict(list)
skipped = collections.defaultdict(list)
Expand Down
12 changes: 3 additions & 9 deletions promgen/rest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from django.conf import settings
from django.http import HttpResponse
from promgen import filters, models, prometheus, serializers
from rest_framework import viewsets
Expand All @@ -22,15 +21,10 @@ def services(self, request, name):

class SharedViewSet:
def format(self, rules=None, name='promgen'):
version = settings.PROMGEN['prometheus'].get('version', 1)
content = prometheus.render_rules(rules, version=version)
content = prometheus.render_rules(rules)
response = HttpResponse(content)
if version == 1:
response['Content-Type'] = 'text/plain; charset=utf-8'
response['Content-Disposition'] = 'attachment; filename=%s.rule' % name
else:
response['Content-Type'] = 'application/x-yaml'
response['Content-Disposition'] = 'attachment; filename=%s.rule.yml' % name
response['Content-Type'] = 'application/x-yaml'
response['Content-Disposition'] = 'attachment; filename=%s.rule.yml' % name
return response


Expand Down
4 changes: 2 additions & 2 deletions promgen/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,12 @@ def write_config(path=None, reload=True, chmod=0o644):


@shared_task
def write_rules(path=None, reload=True, chmod=0o644, version=None):
def write_rules(path=None, reload=True, chmod=0o644):
if path is None:
path = settings.PROMGEN["prometheus"]["rules"]
with atomic_write(path, mode="wb", overwrite=True) as fp:
# Set mode on our temporary file before we write and move it
os.chmod(fp.name, chmod)
fp.write(prometheus.render_rules(version=version))
fp.write(prometheus.render_rules())
if reload:
reload_prometheus()
7 changes: 0 additions & 7 deletions promgen/templates/promgen/prometheus.rule

This file was deleted.

18 changes: 0 additions & 18 deletions promgen/tests/examples/import.rule

This file was deleted.

36 changes: 3 additions & 33 deletions promgen/tests/test_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,7 @@
from django.test import override_settings
from django.urls import reverse

_RULES = '''
ALERT RuleName
IF up==0
FOR 1s
LABELS {severity="severe"}
ANNOTATIONS {rule="http://example.com/rule/%d/edit", summary="Test case"}
'''.lstrip().encode('utf-8')

_RULE_NEW = '''
_RULE_V2 = '''
groups:
- name: example.com
rules:
Expand Down Expand Up @@ -56,15 +46,10 @@ def setUp(self, mock_signal):
models.RuleLabel.objects.create(name='severity', value='severe', rule=self.rule)
models.RuleAnnotation.objects.create(name='summary', value='Test case', rule=self.rule)

@mock.patch('django.dispatch.dispatcher.Signal.send')
def test_write_old(self, mock_post):
result = prometheus.render_rules(version=1)
self.assertEqual(result, _RULES % self.rule.id)

@mock.patch('django.dispatch.dispatcher.Signal.send')
def test_write_new(self, mock_post):
result = prometheus.render_rules(version=2)
self.assertEqual(result, _RULE_NEW % self.rule.id)
result = prometheus.render_rules()
self.assertEqual(result, _RULE_V2 % self.rule.id)

@mock.patch('django.dispatch.dispatcher.Signal.send')
def test_copy(self, mock_post):
Expand All @@ -77,21 +62,6 @@ def test_copy(self, mock_post):
self.assertEqual(models.RuleLabel.objects.count(), 3, 'Copied rule has exiting labels + service label')
self.assertEqual(models.RuleAnnotation.objects.count(), 2)

@mock.patch('django.dispatch.dispatcher.Signal.send')
def test_import_v1(self, mock_post):
self.user.user_permissions.add(
Permission.objects.get(codename='change_rule'),
Permission.objects.get(codename='change_site', content_type__app_label='sites'),
)
self.client.post(reverse('rule-import'), {
'rules': PromgenTest.data('examples', 'import.rule')
})

# Includes count of our setUp rule + imported rules
self.assertEqual(models.Rule.objects.count(), 3, 'Missing Rule')
self.assertEqual(models.RuleLabel.objects.count(), 4, 'Missing labels')
self.assertEqual(models.RuleAnnotation.objects.count(), 7, 'Missing annotations')

@mock.patch('django.dispatch.dispatcher.Signal.send')
def test_import_v2(self, mock_post):
self.user.user_permissions.add(
Expand Down
16 changes: 5 additions & 11 deletions promgen/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -863,7 +863,7 @@ def post(self, request, content_type, object_id):
service = get_object_or_404(models.Service, id=self.kwargs['pk'])
if importform.is_valid():
data = importform.clean()
counters = prometheus.import_rules(data['rules'], service)
counters = prometheus.import_rules_v2(data['rules'], service)
messages.info(request, 'Imported %s' % counters)
return HttpResponseRedirect(service.get_absolute_url())

Expand Down Expand Up @@ -996,15 +996,10 @@ def post(self, request):

class _ExportRules(View):
def format(self, rules=None, name='promgen'):
version = settings.PROMGEN['prometheus'].get('version', 1)
content = prometheus.render_rules(rules, version=version)
content = prometheus.render_rules(rules)
response = HttpResponse(content)
if version == 1:
response['Content-Type'] = 'text/plain; charset=utf-8'
response['Content-Disposition'] = 'attachment; filename=%s.rule' % name
else:
response['Content-Type'] = 'application/x-yaml'
response['Content-Disposition'] = 'attachment; filename=%s.rule.yml' % name
response['Content-Type'] = 'application/x-yaml'
response['Content-Disposition'] = 'attachment; filename=%s.rule.yml' % name
return response


Expand Down Expand Up @@ -1152,7 +1147,6 @@ class RuleImport(PromgenPermissionMixin, FormView):
permission_required = ('sites.change_site', 'promgen.change_rule')
permisison_denied_message = 'User lacks permission to import'


def form_valid(self, form):
data = form.clean()
if data.get('file_field'):
Expand All @@ -1164,7 +1158,7 @@ def form_valid(self, form):
return self.form_invalid(form)

try:
counters = prometheus.import_rules(rules)
counters = prometheus.import_rules_v2(rules)
messages.info(self.request, 'Imported %s' % counters)
return redirect('rule-import')
except:
Expand Down

0 comments on commit 4a3af99

Please sign in to comment.