From f5bd94fc2cbc20d1eedcd7ce3b7a1616a8d7a7c0 Mon Sep 17 00:00:00 2001 From: Jens Nistler Date: Sun, 4 Sep 2016 01:26:23 +0200 Subject: [PATCH] add monitoring and example app --- .gitignore | 3 +- README.md | 40 +++++ README.rst | 2 - Vagrantfile | 4 +- django_monitoring/__init__.py | 0 django_monitoring/admin.py | 13 ++ django_monitoring/apps.py | 17 ++ django_monitoring/base.py | 133 ++++++++++++++ django_monitoring/common/__init__.py | 0 django_monitoring/common/views.py | 21 +++ django_monitoring/forms.py | 58 ++++++ .../locale/de/LC_MESSAGES/django.mo | Bin 0 -> 1631 bytes .../locale/de/LC_MESSAGES/django.po | 98 ++++++++++ django_monitoring/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../commands/monitoring_run_checks.py | 14 ++ django_monitoring/migrations/0001_initial.py | 45 +++++ .../migrations/0002_auto_20160903_2149.py | 25 +++ django_monitoring/migrations/__init__.py | 0 django_monitoring/models.py | 74 ++++++++ django_monitoring/monitoring.py | 91 ++++++++++ django_monitoring/querysets.py | 37 ++++ django_monitoring/tasks.py | 39 ++++ .../templates/django_monitoring/base.html | 60 +++++++ .../django_monitoring/dashboard.html | 75 ++++++++ .../templates/django_monitoring/detail.html | 61 +++++++ .../templates/django_monitoring/form.html | 28 +++ django_monitoring/tests/__init__.py | 0 django_monitoring/urls.py | 13 ++ django_monitoring/views.py | 158 +++++++++++++++++ example/__init__.py | 2 + example/admin.py | 10 ++ example/apps.py | 9 + example/celery.py | 18 ++ example/checks.py | 48 +++++ example/fixtures/example.json | 44 +++++ example/locale/de/LC_MESSAGES/django.mo | Bin 0 -> 1348 bytes example/locale/de/LC_MESSAGES/django.po | 51 ++++++ example/migrations/0001_initial.py | 27 +++ example/migrations/__init__.py | 0 example/models.py | 8 + example/settings.py | 167 ++++++++++++++++++ .../checks/user_has_enough_balance.html | 20 +++ example/urls.py | 22 +++ example/wsgi.py | 16 ++ manage.py | 10 ++ requirements.txt | 8 + setup.py | 24 ++- vagrant/salt/roots/salt/essentials.sls | 5 + vagrant/salt/roots/salt/files/bashrc | 5 - vagrant/salt/roots/salt/python.sls | 11 +- vagrant/salt/roots/salt/rabbitmq.sls | 8 + vagrant/salt/roots/salt/top.sls | 4 +- 53 files changed, 1607 insertions(+), 19 deletions(-) create mode 100644 README.md delete mode 100644 README.rst create mode 100644 django_monitoring/__init__.py create mode 100644 django_monitoring/admin.py create mode 100644 django_monitoring/apps.py create mode 100644 django_monitoring/base.py create mode 100644 django_monitoring/common/__init__.py create mode 100644 django_monitoring/common/views.py create mode 100644 django_monitoring/forms.py create mode 100644 django_monitoring/locale/de/LC_MESSAGES/django.mo create mode 100644 django_monitoring/locale/de/LC_MESSAGES/django.po create mode 100644 django_monitoring/management/__init__.py create mode 100644 django_monitoring/management/commands/__init__.py create mode 100644 django_monitoring/management/commands/monitoring_run_checks.py create mode 100644 django_monitoring/migrations/0001_initial.py create mode 100644 django_monitoring/migrations/0002_auto_20160903_2149.py create mode 100644 django_monitoring/migrations/__init__.py create mode 100644 django_monitoring/models.py create mode 100644 django_monitoring/monitoring.py create mode 100644 django_monitoring/querysets.py create mode 100644 django_monitoring/tasks.py create mode 100644 django_monitoring/templates/django_monitoring/base.html create mode 100644 django_monitoring/templates/django_monitoring/dashboard.html create mode 100644 django_monitoring/templates/django_monitoring/detail.html create mode 100644 django_monitoring/templates/django_monitoring/form.html create mode 100644 django_monitoring/tests/__init__.py create mode 100644 django_monitoring/urls.py create mode 100644 django_monitoring/views.py create mode 100644 example/__init__.py create mode 100644 example/admin.py create mode 100644 example/apps.py create mode 100644 example/celery.py create mode 100644 example/checks.py create mode 100644 example/fixtures/example.json create mode 100644 example/locale/de/LC_MESSAGES/django.mo create mode 100644 example/locale/de/LC_MESSAGES/django.po create mode 100644 example/migrations/0001_initial.py create mode 100644 example/migrations/__init__.py create mode 100644 example/models.py create mode 100644 example/settings.py create mode 100644 example/templates/example/checks/user_has_enough_balance.html create mode 100644 example/urls.py create mode 100644 example/wsgi.py create mode 100755 manage.py create mode 100644 vagrant/salt/roots/salt/essentials.sls create mode 100644 vagrant/salt/roots/salt/rabbitmq.sls diff --git a/.gitignore b/.gitignore index 4767318..40b405c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.pyc .idea/ -.vagrant/ \ No newline at end of file +.vagrant/ +.pycharm_helpers/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..c605e4b --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +Django Monitoring +================= +Extensive documentation will be provided soon. + +Improve django_monitoring +------------------------- + +We've included an example app to show how django_monitoring works and to make it easy to improve it. +Start by launching the included vagrant machine: +```bash +vagrant up +``` + +Then setup the example app environment: +```bash +./manage.py migrate +./manage.py loaddata example +``` +The installed superuser is "example" with password "monitoring". + +Run the development webserver: +```bash +./manage.py runserver 0.0.0.0:8000 +``` + +Login on the admin interface and open http://dm.dev:8000/ afterwards. +You'll be prompted with an empty dashboard. That's because we didn't run any checks yet. +Let's enqueue an update: +```bash +./manage.py monitoring_run_checks +``` + +Now we need to start a celery worker to handle the updates: +```bash +celery worker -A example -l DEBUG -Q django_monitoring +``` + +You will see some failed check now after you refreshed the dashboard view. + +![Django monitoring dashboard](http://static.jensnistler.de/django_monitoring.png "Django monitoring dashboard") diff --git a/README.rst b/README.rst deleted file mode 100644 index 68cfc56..0000000 --- a/README.rst +++ /dev/null @@ -1,2 +0,0 @@ -Django Monitoring -================= diff --git a/Vagrantfile b/Vagrantfile index b1f163c..5104d22 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -31,7 +31,9 @@ Vagrant.configure(2) do |config| salt.masterless = true salt.minion_config = "vagrant/salt/minion.conf" salt.run_highstate = true - salt.verbose = true salt.bootstrap_options = "-c /tmp/ -P" + salt.verbose = true + salt.colorize = true + salt.log_level = "info" end end diff --git a/django_monitoring/__init__.py b/django_monitoring/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_monitoring/admin.py b/django_monitoring/admin.py new file mode 100644 index 0000000..0adfe54 --- /dev/null +++ b/django_monitoring/admin.py @@ -0,0 +1,13 @@ +# -*- coding: UTF-8 -*- +from __future__ import unicode_literals + +from django.contrib import admin + +from django_monitoring.models import Check + + +@admin.register(Check) +class CheckAdmin(admin.ModelAdmin): + list_display = ('slug', 'identifier', 'status') + search_fields = ('slug', 'identifier', 'payload_description') + list_filter = ('status', ) diff --git a/django_monitoring/apps.py b/django_monitoring/apps.py new file mode 100644 index 0000000..0fcceac --- /dev/null +++ b/django_monitoring/apps.py @@ -0,0 +1,17 @@ +# -*- coding: UTF-8 -*- +from __future__ import unicode_literals, print_function + +from django.apps import AppConfig +from django.contrib import admin +from django_monitoring.monitoring import monitor + + +class DjangoMonitoringConfig(AppConfig): + name = 'django_monitoring' + verbose_name = "DjangoMonitoring" + + def ready(self): + super(DjangoMonitoringConfig, self).ready() + + monitor.autodiscover_checks() + admin.autodiscover() diff --git a/django_monitoring/base.py b/django_monitoring/base.py new file mode 100644 index 0000000..a274ce1 --- /dev/null +++ b/django_monitoring/base.py @@ -0,0 +1,133 @@ +# -*- coding: UTF-8 -*- +from __future__ import unicode_literals +import logging + +from django import forms + +from django_monitoring import models +from django_monitoring.tasks import django_monitoring_enqueue +from django_monitoring.models import Check +from django_monitoring.monitoring import monitor + +logger = logging.getLogger(__name__) + + +class BaseCheckForm(forms.Form): + def save(self, instance): + instance.config = self.cleaned_data + instance.save(update_fields=['config']) + return instance + + +class BaseCheck(object): + + """ + Any check should inherits from `BaseCheck` and should implements `.generate(self)` + and `.check(self, payload)` methods. + + Optionally, you can implements `.get_assigned_user(self, payload)` (resp. `.get_assigned_group(self, payload)`) + to define to which user (resp. group) the system had to assign the check result. + """ + + config_form = None + title = '' + + def __init__(self): + self.slug = monitor.get_slug(self.__module__, self.__class__.__name__) + + def run(self): + django_monitoring_enqueue.apply_async(kwargs=dict(check_slug=self.slug), queue='django_monitoring') + + def handle(self, payload): + # check result + unacknowledge = False + try: + check_result = Check.objects.get( + slug=self.slug, identifier=self.get_identifier(payload)) + old_status = check_result.status + except Check.DoesNotExist: + check_result = None + status = self.check(payload) + if check_result and old_status > Check.STATUS.ok and status == Check.STATUS.ok: + unacknowledge = True + self.save(payload, status, + unacknowledge=unacknowledge) + + def get_config(self, payload): + try: + check_result = Check.objects.get(slug=self.slug, identifier=self.get_identifier(payload)) + + # check has a configuration + if check_result.config: + return check_result.config + except Check.DoesNotExist: + pass + + # get default config from form initial values + form = self.get_form_class()() + return {name: field.initial for name, field in form.fields.items()} + + def get_form(self, payload): + return self.get_form_class()(**self.get_config(payload)) + + def get_form_class(self): + return self.config_form + + def save(self, payload, result, unacknowledge=False): + defaults = { + 'status': result, + 'assigned_to_user': self.get_assigned_user(payload, result), + 'assigned_to_group': self.get_assigned_group(payload, result), + 'payload_description': self.get_payload_description(payload) + } + + # save the check + dataset, created = Check.objects.get_or_create( + slug=self.slug, identifier=self.get_identifier(payload), + defaults=defaults) + + # update existing dataset + if not created: + for (key, value) in defaults.items(): + setattr(dataset, key, value) + if unacknowledge: + dataset.acknowledge_by = None + dataset.acknowledge_at = None + dataset.acknowledge_until = None + dataset.save() + + def generate(self): + """ + yield items to run check for + """ + raise NotImplementedError(".generate() should be overridden") + + def check(self, payload): + """ + :param payload: the payload to run the check for + :return: + """ + raise NotImplementedError(".check() should be overridden") + + def get_identifier(self, payload): + raise NotImplementedError(".get_identifier() should be overridden") + + def get_payload_description(self, payload): + return str(payload) + + def get_assigned_user(self, payload, result): + return None + + def get_assigned_group(self, payload, result): + return None + + def get_context_data(self, payload): + return dict() + + def get_title(self): + return self.title + + def get_template_name(self): + if hasattr(self, 'template_name'): + return self.template_name + return None diff --git a/django_monitoring/common/__init__.py b/django_monitoring/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_monitoring/common/views.py b/django_monitoring/common/views.py new file mode 100644 index 0000000..d27d854 --- /dev/null +++ b/django_monitoring/common/views.py @@ -0,0 +1,21 @@ +# -*- coding: UTF-8 -*- +from django.views.generic.edit import FormMixin +from django.views.generic.list import ListView + + +class FilteredListView(FormMixin, ListView): + def get_form_kwargs(self): + return { + 'initial': self.get_initial(), + 'prefix': self.get_prefix(), + 'data': self.request.GET or None + } + + def get(self, request, *args, **kwargs): + self.object_list = self.get_queryset() + + form = self.get_form(self.get_form_class()) + self.object_list = form.filter_queryset(request, self.object_list) + + context = self.get_context_data(form=form, object_list=self.object_list) + return self.render_to_response(context) diff --git a/django_monitoring/forms.py b/django_monitoring/forms.py new file mode 100644 index 0000000..6e9fc23 --- /dev/null +++ b/django_monitoring/forms.py @@ -0,0 +1,58 @@ +# -*- coding: UTF-8 -*- +from django import forms +from django.contrib.auth import get_user_model +from django.utils.translation import ugettext_lazy as _ +from model_utils.choices import Choices + +from django_monitoring.models import Check + + +class ResultFilterForm(forms.Form): + STATUS_CHOICES = Choices((0, 'all', _('All')), (1, 'failed', _('Failed'))) + + user = forms.ModelChoiceField(queryset=get_user_model().objects.all().order_by('last_name', 'first_name'), + label=_('User'), required=False) + status = forms.TypedChoiceField(coerce=int, choices=STATUS_CHOICES, label=_('Status'), + initial=STATUS_CHOICES.failed) + + def __init__(self, user, group_filter=None, **kwargs): + super(ResultFilterForm, self).__init__(**kwargs) + + if not group_filter: + group_filter = dict() + + self.fields['user'].initial = user + self.fields['user'].queryset = self.fields['user'].queryset.filter(**group_filter) + + def filter_queryset(self, request, queryset): + # default values if form has not been submitted + if not self.is_bound: + return queryset.failed().unacknowledged().for_user(request.user) + # form has been submitted with invalid values + if not self.is_valid(): + return queryset.none() + + if self.cleaned_data['user']: + queryset = queryset.for_user(self.cleaned_data['user']) + if self.cleaned_data['status']: + if self.cleaned_data['status'] == self.STATUS_CHOICES.failed: + queryset = queryset.failed().unacknowledged() + + return queryset.distinct() + + +class AcknowledgeForm(forms.ModelForm): + days = forms.IntegerField(label=_('Days to acknowledge')) + + class Meta: + model = Check + fields = ['days', 'acknowledged_reason'] + + def __init__(self, user, **kwargs): + self.user = user + super(AcknowledgeForm, self).__init__(**kwargs) + + def save(self, commit=True): + self.instance.acknowledge(user=self.user, days=self.cleaned_data.get('days'), + reason=self.cleaned_data['acknowledged_reason'], commit=commit) + return self.instance diff --git a/django_monitoring/locale/de/LC_MESSAGES/django.mo b/django_monitoring/locale/de/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..bd8dd1cfb2d3801ded215571a6440105ea26daac GIT binary patch literal 1631 zcmZva&u<$=6vqcBPz;6A@H2okIaF#1TO1=*!A?*&_BvRxW6M8+-~fzw$LopLv)0UP zN^*fa7dUX>h7kV&SHz{_R*3^QkT`Qd;)1v#_&$5vI8mQ6{_J~s^M1|y{mO-}7~~b4 zS8@Kvxr%f5A^bo-eweW*K>=O?r{FW-XF0#f?cabD<9qNC@MrKb@E7nB_-oETz>CoT z0aw8PK=NC`CE_Yr0M|gu`vwRXwh5Ad1th;V_&9g~UI0JL?O|>|%V>wxJ2<*K+3ZQQk?Z%FM*V23#9%7km_%N^kg)%mvBhv zKE8&X%uft zFx4X!v+5j~5l$g%)hMSzk+7OLw%ny$ELB+-3gxo8N}M#T5lLhnDSWolC`%-_NfxvA z9_xtXBoz^lqz#Rl%>#EAU3KNiNIPadaU@yK371(m&@>3m1{R^-6Gkf?v*-{-rXNjH ztz2rz(lP0w44v1AyaQ=0sx9-5U)}R}1AY*6dyV#fp(|&pabC;DD)M%+*n0i6%nO}% z->YJ-REbx^K+3#S+_>o#-}Z`c@lvT=y18D2z|rf$Tcm&FAm7%dN)x z2krf!&=l5reIv9@1j;M(`k>k5`+h6nTS3cjG;jH}S~uwR-hr!*GZ9O#FU7RXoBsap zfJ)wKHg6^BMrz_yAC%f^w1U(AHf|IPcp!&Kd;Q}X>UQ!I_vS1SN>_L|7RE}qJ?PiH z%~L-rYb1>q=rD~ixia4zDp%+vc;v*Zr)Fx)T+imJ-7Z!5;&c0&=4;!0<9emQ{LBs$ zsqw=1KQkTM*kn2)ODq@vc3e!)>PNJ?%ML?@1j}|8jZa)A5^M*eXx5w2b*k_F9uMV2XzdV8XYK@nX`2!70o(jP Ddrg7P literal 0 HcmV?d00001 diff --git a/django_monitoring/locale/de/LC_MESSAGES/django.po b/django_monitoring/locale/de/LC_MESSAGES/django.po new file mode 100644 index 0000000..42fe1dc --- /dev/null +++ b/django_monitoring/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,98 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-09-03 22:26+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "All" +msgstr "Alle" + +msgid "Failed" +msgstr "Fehler" + +msgid "User" +msgstr "Benutzer" + +msgid "Status" +msgstr "Status" + +msgid "Days to acknowledge" +msgstr "Ausblenden für Anzahl Tage" + +msgid "Unknown" +msgstr "Unbekannt" + +msgid "OK" +msgstr "OK" + +msgid "Warning" +msgstr "Warnung" + +msgid "Critical" +msgstr "Kritisch" + +msgid "Module slug" +msgstr "Modul-Bezeichner" + +msgid "Identifier" +msgstr "Identifizierer" + +msgid "Configuration" +msgstr "Konfiguration" + +msgid "Payload description" +msgstr "Beschreibung des Gegenstandes" + +msgid "Acknowledged by" +msgstr "Ausgeblendet von" + +msgid "Acknowledged at" +msgstr "Ausgeblendet am" + +msgid "Acknowledged until" +msgstr "Ausgeblendet bis" + +msgid "Acknowledge reason" +msgstr "Ausblendungsgrund" + +msgid "django_monitoring" +msgstr "django_monitoring" + +msgid "Filter" +msgstr "Filter" + +#, python-format +msgid "Acknowledged until %(date)s" +msgstr "Ausgeblendet bis %(date)s" + +msgid "Acknowledge" +msgstr "Ausblenden" + +msgid "Change config" +msgstr "Konfiguration ändern" + +msgid "Refresh" +msgstr "Aktualisieren" + +msgid "Reason" +msgstr "Grund" + +msgid "Cancel" +msgstr "Abbrechen" + +msgid "Save" +msgstr "Speichern" diff --git a/django_monitoring/management/__init__.py b/django_monitoring/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_monitoring/management/commands/__init__.py b/django_monitoring/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_monitoring/management/commands/monitoring_run_checks.py b/django_monitoring/management/commands/monitoring_run_checks.py new file mode 100644 index 0000000..8daf656 --- /dev/null +++ b/django_monitoring/management/commands/monitoring_run_checks.py @@ -0,0 +1,14 @@ +# -*- coding: UTF-8 -*- +import logging + +from django.core.management.base import BaseCommand + +from django_monitoring.monitoring import monitor + +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + def handle(self, *args, **options): + for check in monitor.get_all_registered_checks(): + check().run() diff --git a/django_monitoring/migrations/0001_initial.py b/django_monitoring/migrations/0001_initial.py new file mode 100644 index 0000000..ecd1fe1 --- /dev/null +++ b/django_monitoring/migrations/0001_initial.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.8 on 2016-09-03 17:44 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import django_extensions.db.fields.json +import model_utils.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0007_alter_validators_add_error_messages'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Check', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('slug', models.TextField(verbose_name='Module slug')), + ('identifier', models.CharField(max_length=256, verbose_name='Identifier')), + ('status', models.IntegerField(choices=[(0, 'Unknown'), (1, 'OK'), (2, 'Warning'), (3, 'Danger')], default=0, verbose_name='Status')), + ('config', django_extensions.db.fields.json.JSONField(blank=True, default=dict, verbose_name='Configuration')), + ('payload_description', models.TextField(verbose_name='Payload description')), + ('acknowledged_at', models.DateTimeField(null=True, verbose_name='Acknowledged at')), + ('acknowledged_until', models.DateTimeField(null=True, verbose_name='Acknowledged until')), + ('acknowledged_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='acknowledged_by', to=settings.AUTH_USER_MODEL, verbose_name='Acknowledged by')), + ('assigned_to_group', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='auth.Group')), + ('assigned_to_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='assigned_to_user', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AlterUniqueTogether( + name='check', + unique_together=set([('slug', 'identifier')]), + ), + ] diff --git a/django_monitoring/migrations/0002_auto_20160903_2149.py b/django_monitoring/migrations/0002_auto_20160903_2149.py new file mode 100644 index 0000000..303b353 --- /dev/null +++ b/django_monitoring/migrations/0002_auto_20160903_2149.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.8 on 2016-09-03 21:49 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_monitoring', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='check', + name='acknowledged_reason', + field=models.TextField(blank=True, verbose_name='Acknowledged until'), + ), + migrations.AlterField( + model_name='check', + name='status', + field=models.IntegerField(choices=[(0, 'Unknown'), (1, 'OK'), (2, 'Warning'), (3, 'Critical')], default=0, verbose_name='Status'), + ), + ] diff --git a/django_monitoring/migrations/__init__.py b/django_monitoring/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_monitoring/models.py b/django_monitoring/models.py new file mode 100644 index 0000000..f71e69b --- /dev/null +++ b/django_monitoring/models.py @@ -0,0 +1,74 @@ +# -*- coding: UTF-8 -*- +from __future__ import unicode_literals + +from dateutil import relativedelta +from django.utils import timezone +from django.conf import settings +from django.db import models +from django.utils.encoding import python_2_unicode_compatible +from django.utils.translation import ugettext_lazy as _ +from django_extensions.db.fields.json import JSONField +from model_utils.choices import Choices +from model_utils.models import TimeStampedModel + +from .monitoring import monitor +from .querysets import CheckQuerySet + + +class AlreadyAcknowledged(Exception): + pass + + +@python_2_unicode_compatible +class Check(TimeStampedModel): + STATUS = Choices((0, 'unknown', _('Unknown')), + (1, 'ok', _('OK')), + (2, 'warning', _('Warning')), + (3, 'danger', _('Critical'))) + + slug = models.TextField(verbose_name=_('Module slug')) + identifier = models.CharField(max_length=256, verbose_name=_('Identifier')) + + status = models.IntegerField(choices=STATUS, + default=STATUS.unknown, verbose_name=_('Status')) + config = JSONField(blank=True, verbose_name=_('Configuration')) + + payload_description = models.TextField(verbose_name=_('Payload description')) + + acknowledged_by = models.ForeignKey(to=settings.AUTH_USER_MODEL, null=True, + verbose_name=_('Acknowledged by'), + related_name='acknowledged_by') + acknowledged_at = models.DateTimeField(null=True, verbose_name=_('Acknowledged at')) + acknowledged_until = models.DateTimeField(null=True, verbose_name=_('Acknowledged until')) + acknowledged_reason = models.TextField(blank=True, verbose_name=_('Acknowledge reason')) + + assigned_to_user = models.ForeignKey(to=settings.AUTH_USER_MODEL, null=True, + related_name='assigned_to_user') + assigned_to_group = models.ForeignKey(to='auth.Group', null=True) + + objects = CheckQuerySet.as_manager() + + class Meta: + unique_together = ('slug', 'identifier') + + def acknowledge(self, user, days, reason=None, commit=True): + if self.status in (self.STATUS.warning, self.STATUS.danger) and self.is_acknowledged(): + raise AlreadyAcknowledged() + self.acknowledged_at = timezone.now() + self.acknowledged_by = user + self.acknowledged_until = timezone.now() + relativedelta.relativedelta(days=days) + self.acknowledged_reason = reason or '' + if commit: + self.save(update_fields=['acknowledged_at', 'acknowledged_by', 'acknowledged_until', 'acknowledged_reason']) + + def is_acknowledged(self): + return self.acknowledged_until and self.acknowledged_until >= timezone.now() + + def __str__(self): + return self.slug + + def get_check_instance(self): + return monitor.get_check_class(self.slug)() + + def get_payload(self): + return self.get_check_instance().get_payload(self.identifier) diff --git a/django_monitoring/monitoring.py b/django_monitoring/monitoring.py new file mode 100644 index 0000000..3cce9e0 --- /dev/null +++ b/django_monitoring/monitoring.py @@ -0,0 +1,91 @@ +# -*- coding: UTF-8 -*- +from __future__ import unicode_literals + +import logging + +from collections import defaultdict + +from django.utils.module_loading import autodiscover_modules +from django.db.models import signals + +logger = logging.getLogger(__name__) + + +class MonitoringHandler(object): + def __init__(self): + self._registered_checks = {} + self._related_models = defaultdict(list) + + def autodiscover_checks(self, module_name='checks'): + autodiscover_modules(module_name) + + def register(self, check_class): + slug = self.get_slug(check_class.__module__, check_class.__name__) + self._registered_checks[slug] = check_class + check = check_class() + if hasattr(check, 'trigger_update'): + for method_name, model in check.trigger_update.items(): + if not hasattr(check, 'get_%s_payload' % method_name): + logger.warning('Update trigger defined without implementing .get_*_payload()') + continue + + model_uid = make_model_uid(model) + if model_uid in self._related_models: + signals.post_save.connect(run_checks, sender=model) + self._related_models[model_uid].append(check_class) + + return check_class + + def get_all_registered_checks(self): + return self._registered_checks.values() + + @property + def checks(self): + for slug in self._registered_checks.keys(): + yield slug + + def get_check_class(self, slug): + if slug in self._registered_checks: + return self._registered_checks[slug] + return None + + def get_checks_for_model(self, model): + model_uid = make_model_uid(model) + if model_uid in self._related_models: + return self._related_models[model_uid] + return None + + def get_slug(self, module, class_name): + return u'{}.{}'.format(module, class_name) + + +monitor = MonitoringHandler() + + +def make_model_uid(model): + """ + Returns an uid that will identify the given model class. + + :param model: model class + :return: uid (string) + """ + return "%s.%s" % (model._meta.app_label, model.__name__) + + +def run_checks(sender, instance, created, raw, using, **kwargs): + """ + Re-execute checks related to the given sender model, only for the updated instance. + + :param sender: model + :param kwargs: + """ + from django_monitoring.tasks import django_monitoring_run + checks = monitor.get_checks_for_model(sender) or [] + for check_class in checks: + check = check_class() + payload = check.get_payload(instance) + if not payload: + continue + django_monitoring_run().apply( + kwargs=dict(check_slug=check.slug, identifier=check.get_identifier(payload)), + queue='django_monitoring') diff --git a/django_monitoring/querysets.py b/django_monitoring/querysets.py new file mode 100644 index 0000000..085e008 --- /dev/null +++ b/django_monitoring/querysets.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models +from django.db.models.aggregates import Count +from django.db.models.expressions import Case, When, Value +from django.db.models.query_utils import Q +from django.utils import timezone + + +class CheckQuerySet(models.QuerySet): + def for_user(self, user): + return self.filter(Q(assigned_to_group__isnull=True) | Q(assigned_to_group__in=user.groups.all()), + Q(assigned_to_user__isnull=True) | Q(assigned_to_user=user)) + + def failed(self): + return self.exclude(status__in=(self.model.STATUS.unknown, self.model.STATUS.ok)) + + def ok(self): + return self.filter(status=self.model.STATUS.ok) + + def unknown(self): + return self.filter(status=self.model.STATUS.unknown) + + def unacknowledged(self): + return self.exclude(acknowledged_until__gt=timezone.now()) + + def with_status_name(self): + case = Case(output_field=models.CharField()) + for status_value in self.model.STATUS._db_values: + case.cases.append( + When(status=status_value, then=Value(str(self.model.STATUS._display_map[status_value]))), + ) + return self.annotate(status_name=case) + + def get_stats(self): + return self.values('status').annotate(amount=Count('id')).with_status_name() diff --git a/django_monitoring/tasks.py b/django_monitoring/tasks.py new file mode 100644 index 0000000..21b0552 --- /dev/null +++ b/django_monitoring/tasks.py @@ -0,0 +1,39 @@ +# -*- coding: UTF-8 -*- +from __future__ import unicode_literals + +from celery import shared_task +from celery.utils.log import get_task_logger +from celery.schedules import crontab +from celery.task.base import PeriodicTask + + +from django_monitoring.monitoring import monitor + +logger = get_task_logger(__name__) + + +@shared_task +def django_monitoring_enqueue(check_slug, *args, **kwargs): + logger.debug('enqueuing checks for %s', check_slug) + check = monitor.get_check_class(check_slug)() + + for payload in check.generate(): + identifier = check.get_identifier(payload) + django_monitoring_run.apply_async(kwargs=dict(check_slug=check_slug, identifier=identifier), + queue='django_monitoring') + + +@shared_task +def django_monitoring_run(check_slug, identifier, *args, **kwargs): + logger.debug('running check %s for identifier %s', check_slug, identifier) + check = monitor.get_check_class(check_slug)() + payload = check.get_payload(identifier) + return check.handle(payload) + + +class MonitoringScheduler(PeriodicTask): + run_every = crontab(minute=0, hour=0) + + def run(self, *args, **kwargs): + for check_slug in monitor.checks: + django_monitoring_enqueue.apply_async(kwargs=dict(check_slug=check_slug), queue='django_monitoring') diff --git a/django_monitoring/templates/django_monitoring/base.html b/django_monitoring/templates/django_monitoring/base.html new file mode 100644 index 0000000..c03cad0 --- /dev/null +++ b/django_monitoring/templates/django_monitoring/base.html @@ -0,0 +1,60 @@ +{% load i18n %} + + + + + + {% trans "django_monitoring" %} + + + + + +
+ {% if messages %} + {% for message in messages %} +
+ + {% if 'debug' in message.tags %} + {% trans 'DEBUG' %} + {% elif 'info' in message.tags %} + {% trans 'Info' %} + {% elif 'success' in message.tags %} + {% trans 'Success' %} + {% elif 'error' in message.tags %} + {% trans 'Error' %} + {% else %} + {% trans 'Warning' %} + {% endif %} + + {% if 'safe' in message.tags %} + {{ message|safe }} + {% else %} + {{ message }} + {% endif %} +
+ {% endfor %} + {% endif %} + + {% block django_monitoring %}{% endblock %} +
+ + diff --git a/django_monitoring/templates/django_monitoring/dashboard.html b/django_monitoring/templates/django_monitoring/dashboard.html new file mode 100644 index 0000000..ee62657 --- /dev/null +++ b/django_monitoring/templates/django_monitoring/dashboard.html @@ -0,0 +1,75 @@ +{% extends "django_monitoring/base.html" %} +{% load i18n %} +{% load bootstrap3 %} + +{% block django_monitoring %} +
+
+
+
+

{% trans "Filter" %}

+
+
+
+ {% bootstrap_form form layout='horizontal' %} + {% buttons %} +
+ {% trans "Filter" as trans_filter %} + {% bootstrap_button trans_filter button_type="submit" button_class="btn-primary" %} +
+ {% endbuttons %} +
+
+
+
+
+ + {% regroup results by get_status_display as status_list %} +
+
+ {% widthratio 12 status_list|length 1 as size %} + {% for status in status_list %} + {% for status_definition in view.queryset.get_stats %} + {% if status.grouper == status_definition.status_name %} +
+
+ {{ status_definition.status_name }}
+

{{ status_definition.amount }}

+
+
+ {% endif %} + {% endfor %} + {% endfor %} +
+
+ +
+
+ {% for status in status_list %} +

{{ status.grouper }}

+ + + {% for result in status.list %} + + + + + + {% endfor %} +
+ {{ result.get_status_display }} + {% if result.is_acknowledged %} +
+ {% blocktrans with date=result.acknowledged_until|date %}Acknowledged until {{ date }}{% endblocktrans %} + {% endif %} +
+ + {{ result.get_check_instance.get_title }} + + + {{ result.payload_description }} +
+ {% endfor %} +
+
+{% endblock %} diff --git a/django_monitoring/templates/django_monitoring/detail.html b/django_monitoring/templates/django_monitoring/detail.html new file mode 100644 index 0000000..10c023b --- /dev/null +++ b/django_monitoring/templates/django_monitoring/detail.html @@ -0,0 +1,61 @@ +{% extends "django_monitoring/base.html" %} +{% load i18n %} + +{% block django_monitoring %} +
+
+
+
+
+

{{ result.get_status_display }} - {{ result.get_check_instance.get_title }}

+
+
+
+
+
+

{{ result.payload_description }}

+
+ +
+ {% if result.is_acknowledged %} +
+
+
+ {% blocktrans with date=result.acknowledged_until|date %}Acknowledged until {{ date }}{% endblocktrans %} +
+ {% trans "Reason" %}: {{ result.acknowledged_reason }} +
+
+ {% endif %} +
+
+
+ {% block django_monitoring_check %}{% endblock %} +
+
+
+
+
+
+ +
+
+

+ + {{ result.get_check_instance.get_title }} + +

+

+ +

+ +
+
+ {% block check_content %}{% endblock %} +
+
+{% endblock %} diff --git a/django_monitoring/templates/django_monitoring/form.html b/django_monitoring/templates/django_monitoring/form.html new file mode 100644 index 0000000..cd25354 --- /dev/null +++ b/django_monitoring/templates/django_monitoring/form.html @@ -0,0 +1,28 @@ +{% extends "django_monitoring/base.html" %} +{% load i18n %} +{% load bootstrap3 %} + +{% block django_monitoring %} +
+
+
+
+

{% trans "Filter" %}

+
+
+
+ {% csrf_token %} + {% bootstrap_form form layout='horizontal' %} + {% buttons %} +
+ {% bootstrap_button action button_type="submit" button_class="btn-primary" %} +   + {% trans "Cancel" %} +
+ {% endbuttons %} +
+
+
+
+
+{% endblock %} diff --git a/django_monitoring/tests/__init__.py b/django_monitoring/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_monitoring/urls.py b/django_monitoring/urls.py new file mode 100644 index 0000000..89b16b8 --- /dev/null +++ b/django_monitoring/urls.py @@ -0,0 +1,13 @@ +# -*- coding: UTF-8 -*- +from django.conf.urls import url + +from django_monitoring import views + +urlpatterns = [ + url(r'^$', views.DashboardView.as_view(), name='django_monitoring_index'), + url(r'^result/(?P\d+)/$', views.ResultView.as_view(), name='django_monitoring_result'), + url(r'^result/(?P\d+)/acknowledge/$', views.ResultAcknowledgeView.as_view(), + name='django_monitoring_result_acknowledge'), + url(r'^result/(?P\d+)/config/$', views.ResultConfigView.as_view(), name='django_monitoring_result_config'), + url(r'^result/(?P\d+)/refresh/$', views.ResultRefreshView.as_view(), name='django_monitoring_result_refresh'), +] diff --git a/django_monitoring/views.py b/django_monitoring/views.py new file mode 100644 index 0000000..4248270 --- /dev/null +++ b/django_monitoring/views.py @@ -0,0 +1,158 @@ +# -*- coding: UTF-8 -*- +import logging + +from braces.views import PermissionRequiredMixin, LoginRequiredMixin +from django.core.urlresolvers import reverse_lazy +from django.utils.translation import gettext_lazy as _ +from django.views.generic.base import RedirectView +from django.views.generic.detail import DetailView, SingleObjectMixin +from django.views.generic.edit import UpdateView, FormView +from django.contrib import messages + +from django_monitoring import forms +from django_monitoring.common.views import FilteredListView +from django_monitoring.models import Check +from django_monitoring.tasks import django_monitoring_run + +logger = logging.getLogger(__name__) + + +class DashboardView(LoginRequiredMixin, PermissionRequiredMixin, FilteredListView): + form_class = forms.ResultFilterForm + permission_required = 'django_monitoring.view' + template_name = 'django_monitoring/dashboard.html' + context_object_name = 'results' + + def get_form_kwargs(self): + kwargs = super(DashboardView, self).get_form_kwargs() + kwargs.update(dict(user=self.request.user)) + return kwargs + + def get_queryset(self): + if self.queryset is None: + self.queryset = Check.objects.all().order_by('-status') + return self.queryset + + def get_context_data(self, **kwargs): + ctx = super(DashboardView, self).get_context_data(**kwargs) + ctx.update(dict(check=Check)) + return ctx + + +class ResultView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): + permission_required = 'django_monitoring.view' + model = Check + template_name = 'django_monitoring/detail.html' + + def __init__(self, *args, **kwargs): + super(ResultView, self).__init__(*args, **kwargs) + self.check_instance = None + + def get_check_instance(self): + if not self.check_instance: + self.check_instance = self.object.get_check_instance() + return self.check_instance + + def get_context_data(self, **kwargs): + kwargs.update(result=self.object) + ctx = super(ResultView, self).get_context_data(**kwargs) + ctx.update(self.get_check_instance().get_context_data(self.object)) + return ctx + + def get_template_names(self): + template_names = super(ResultView, self).get_template_names() + check_template = self.get_check_instance().get_template_name() + if check_template: + return [check_template] + template_names + return template_names + + +class ResultAcknowledgeView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): + permission_required = 'django_monitoring.acknowledge' + form_class = forms.AcknowledgeForm + model = Check + template_name = 'django_monitoring/form.html' + + def get_form_kwargs(self): + kwargs = super(ResultAcknowledgeView, self).get_form_kwargs() + kwargs['user'] = self.request.user + return kwargs + + def get_success_url(self): + return reverse_lazy('django_monitoring_index') + + def get_context_data(self, **kwargs): + ctx = super(ResultAcknowledgeView, self).get_context_data(**kwargs) + ctx.update(dict(action=_('Acknowledge'))) + return ctx + + def form_valid(self, form): + response = super(ResultAcknowledgeView, self).form_valid(form) + messages.add_message(self.request, messages.INFO, _('Check has been successfully acknowledged')) + return response + + +class ResultConfigView(LoginRequiredMixin, PermissionRequiredMixin, SingleObjectMixin, FormView): + permission_required = 'django_monitoring.config' + model = Check + template_name = 'django_monitoring/form.html' + + def __init__(self, **kwargs): + self.object = None + super(ResultConfigView, self).__init__(**kwargs) + + def get(self, request, *args, **kwargs): + self.object = self.get_object() + return super(ResultConfigView, self).get(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + return super(ResultConfigView, self).post(request, *args, **kwargs) + + def get_form_class(self): + return self.object.get_check_instance().get_form_class() + + def get_form_kwargs(self): + kwargs = super(ResultConfigView, self).get_form_kwargs() + if self.object.config: + kwargs.update(dict(initial=self.object.config)) + return kwargs + + def get_success_url(self): + return reverse_lazy('django_monitoring_result', kwargs=dict(pk=self.object.pk)) + + def get_context_data(self, **kwargs): + ctx = super(ResultConfigView, self).get_context_data(**kwargs) + ctx.update(dict(action=_('Save'))) + return ctx + + def form_valid(self, form): + form.save(instance=self.object) + check = self.object.get_check_instance() + + django_monitoring_run.apply(kwargs=dict(check_slug=check.slug, identifier=check.get_identifier(self.object)), + queue='django_monitoring') + return super(ResultConfigView, self).form_valid(form) + + +class ResultRefreshView(LoginRequiredMixin, PermissionRequiredMixin, SingleObjectMixin, RedirectView): + permission_required = 'django_monitoring.refresh' + permanent = False + model = Check + + def __init__(self, **kwargs): + super(ResultRefreshView, self).__init__(**kwargs) + self.kwargs = dict() + self.object = None + + def get(self, request, *args, **kwargs): + self.object = self.get_object() + response = super(ResultRefreshView, self).get(request, *args, **kwargs) + check = self.object.get_check_instance() + django_monitoring_run.apply(kwargs=dict(check_slug=check.slug, identifier=check.get_identifier(self.object)), + queue='django_monitoring') + messages.add_message(request, messages.INFO, _('Result has been refreshed')) + return response + + def get_redirect_url(self, *args, **kwargs): + return reverse_lazy('django_monitoring_result', kwargs=dict(pk=self.object.pk)) diff --git a/example/__init__.py b/example/__init__.py new file mode 100644 index 0000000..6368c8e --- /dev/null +++ b/example/__init__.py @@ -0,0 +1,2 @@ +from __future__ import absolute_import +from .celery import app as celery_app # noqa diff --git a/example/admin.py b/example/admin.py new file mode 100644 index 0000000..a76aa93 --- /dev/null +++ b/example/admin.py @@ -0,0 +1,10 @@ +# -*- coding: UTF-8 -*- +from django.contrib import admin + +from example import models + + +@admin.register(models.Wallet) +class WalletAdmin(admin.ModelAdmin): + list_display = ('id', 'user', 'balance') + search_fields = ('user__username', ) diff --git a/example/apps.py b/example/apps.py new file mode 100644 index 0000000..f8056de --- /dev/null +++ b/example/apps.py @@ -0,0 +1,9 @@ +# -*- coding: UTF-8 -*- +from __future__ import unicode_literals, print_function + +from django.apps import AppConfig + + +class ExampleConfig(AppConfig): + name = 'example' + verbose_name = 'Example' diff --git a/example/celery.py b/example/celery.py new file mode 100644 index 0000000..2a357d6 --- /dev/null +++ b/example/celery.py @@ -0,0 +1,18 @@ +# -*- coding: UTF-8 -*- +from __future__ import absolute_import + +import os + +from celery import Celery + +# set the default Django settings module for the 'celery' program. +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'example.settings') + +from django.conf import settings # noqa + +app = Celery('example') + +# Using a string here means the worker will not have to +# pickle the object when using Windows. +app.config_from_object('django.conf:settings') +app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) diff --git a/example/checks.py b/example/checks.py new file mode 100644 index 0000000..218646b --- /dev/null +++ b/example/checks.py @@ -0,0 +1,48 @@ +# -*- coding: UTF-8 -*- +from __future__ import unicode_literals, print_function + + +from django.utils.translation import ugettext as _ +from django_monitoring.models import Check +from django_monitoring.monitoring import monitor +from django_monitoring.base import BaseCheck, BaseCheckForm +from django import forms + +from example import models + + +class UserHasEnoughBalanceConfig(BaseCheckForm): + danger = forms.IntegerField(initial=0, label=_('Balance critical')) + warning = forms.IntegerField(initial=100, label=_('Balance warning')) + + +@monitor.register +class UserHasEnoughBalance(BaseCheck): + config_form = UserHasEnoughBalanceConfig + title = _('User balance') + template_name = 'example/checks/user_has_enough_balance.html' + trigger_update = dict(wallet=models.Wallet) + + def generate(self): + for payload in models.Wallet.objects.all(): + yield payload + + def check(self, payload): + config = self.get_config(payload) + if payload.balance < config['danger']: + return Check.STATUS.danger + if payload.balance < config['warning']: + return Check.STATUS.warning + return Check.STATUS.ok + + def get_identifier(self, payload): + return payload.pk + + def get_payload(self, identifier): + return models.Wallet.objects.get(pk=identifier) + + def get_payload_description(self, payload): + return payload.user.username + + def get_wallet_payload(self, instance): + return instance diff --git a/example/fixtures/example.json b/example/fixtures/example.json new file mode 100644 index 0000000..7a4cbaf --- /dev/null +++ b/example/fixtures/example.json @@ -0,0 +1,44 @@ +[ + { + "model": "auth.user", + "pk": 1, + "fields": { + "password": "pbkdf2_sha256$24000$K7FELS7xYuVP$I8/i4Ay0DBb6LKaXDnimyyorpbT6C0RtN5aBQxfmtuQ=", + "last_login": "2016-09-03T23:17:38Z", + "is_superuser": true, + "username": "example", + "first_name": "", + "last_name": "", + "email": "", + "is_staff": true, + "is_active": true, + "date_joined": "2016-09-03T13:14:46Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "example.wallet", + "pk": 1, + "fields": { + "user": 1, + "balance": "1000.00" + } + }, + { + "model": "example.wallet", + "pk": 2, + "fields": { + "user": 1, + "balance": "50.00" + } + }, + { + "model": "example.wallet", + "pk": 3, + "fields": { + "user": 1, + "balance": "-100.00" + } + } +] diff --git a/example/locale/de/LC_MESSAGES/django.mo b/example/locale/de/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..fab8b6edc0fe1d0d3e911ab7cae86eda9f6634c8 GIT binary patch literal 1348 zcma)5&5j#I5N;qLV1a-DCpZ)r6twEW>qyb!%|`28&o0*bPqsG_xwU6%$BoC`R{zA= z9P$=9aOcPk2_7JC!2!V=@Cbb4b=HX#AzG^Wy1T0DtH1g4_KoKP>wU5bV=sY+IREsH5T6780DcDi z2S~u1H-z{E_%-kua1Zzd_&e}ZVEv{LaI=mRAl|D!Snooh5^yhID&TItZf#Js%909e z^Mq1kV=<$JHGE0dP?)iHG*zWC87JcCYD1aRL1(JGzE~+|v?;`KbdF7U`GT|;|lEBHX#^+S7n)v@U~K(a#mT;k5yoVIV@Rurp%gv+sem+ z^kTuehQ7S~PZAeR6)Kf>^=3!Pbv)3{X1E|ON~aeac`%N1w%RF$GUR9vHKxyjbv{i+b#Nj(Cl?~9u4|wt*d;H6Q_(X zRj{r}C+FQR^;^A^?x($0r+e6Hw@2xC`~aq=h^k;YVYO&dx79yAuR^}x?H-mov98$K zAkiF3*RKOTp#^GW1p_J2X_p8bwIn{?ANNfWl!4Bz~v^I0`rTb4^*&; z1)Xw att, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-09-03 22:26+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Balance critical" +msgstr "Kritischer Kontostand" + +msgid "Balance warning" +msgstr "Niedriger Kontostand" + +msgid "User balance" +msgstr "Benutzer-Kontostand" + +msgid "" +"The balance of the user is critical. Their account was deactivated. Please " +"contact them immediately." +msgstr "" +"Der Kontostand des Benutzers ist kritisch. Sein Zugang wurde gesperrt. Bitte " +"kontaktiere den Benutzer so schnell wie möglich." + +msgid "" +"The balance of the user is running low. Please contact them and ask them to " +"add more money to their balance." +msgstr "" +"Der Kontostand des Benutzers ist sehr gering. Bitte kontaktiere ihn und " +"bitte darum mehr Geld auf das Konto aufzubuchen." + +msgid "All good. Enough money on their balance." +msgstr "Alles in Ordnung. Es ist genug Geld auf dem Konto des Benutzers." + +msgid "Status unknown." +msgstr "Status unbekannt." + +msgid "Current balance is" +msgstr "Der aktuelle Kontostand beträgt" diff --git a/example/migrations/0001_initial.py b/example/migrations/0001_initial.py new file mode 100644 index 0000000..75a5238 --- /dev/null +++ b/example/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.8 on 2016-09-03 14:46 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Wallet', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('balance', models.DecimalField(decimal_places=2, max_digits=9)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/example/migrations/__init__.py b/example/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/example/models.py b/example/models.py new file mode 100644 index 0000000..bb01e78 --- /dev/null +++ b/example/models.py @@ -0,0 +1,8 @@ +# -*- coding: UTF-8 -*- +from django.conf import settings +from django.db import models + + +class Wallet(models.Model): + user = models.ForeignKey(settings.AUTH_USER_MODEL) + balance = models.DecimalField(max_digits=9, decimal_places=2) diff --git a/example/settings.py b/example/settings.py new file mode 100644 index 0000000..c1f2fda --- /dev/null +++ b/example/settings.py @@ -0,0 +1,167 @@ +""" +Django settings for example project. + +Generated by 'django-admin startproject' using Django 1.9.8. + +For more information on this file, see +https://docs.djangoproject.com/en/1.9/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.9/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = '&kxa67(_phgs@5&8=!x(ix(l%w1nmkh&n#1%^5pm&wm^ij)4(6' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'bootstrap3', + 'django_monitoring.apps.DjangoMonitoringConfig', + 'example.apps.ExampleConfig', +] + +MIDDLEWARE_CLASSES = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'example.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'example.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.9/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': 'vagrant', + 'USER': 'vagrant', + 'PASSWORD': 'vagrant', + 'HOST': '127.0.0.1', + 'PORT': '5432', + }, +} + + +# Password validation +# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': True, + 'formatters': { + 'verbose': { + 'format': '%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)d %(funcName)s %(process)d ' + '%(thread)d %(message)s', + }, + }, + 'handlers': { + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'verbose', + }, + }, + 'loggers': { + '': { + 'handlers': ['console'], + 'level': 'DEBUG', + }, + 'django': { + 'handlers': ['console'], + 'level': 'INFO', + }, + 'celery': { + 'handlers': ['console'], + 'level': 'DEBUG', + }, + }, +} + + +# Internationalization +# https://docs.djangoproject.com/en/1.9/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.9/howto/static-files/ + +STATIC_URL = '/static/' + +BROKER_URL = 'amqp://' + +# django-braces settings +LOGIN_URL = '/admin/' + +BOOTSTRAP3 = {'horizontal_label_class': 'col-md-2', 'horizontal_field_class': 'col-md-10', 'success_css_class': ''} diff --git a/example/templates/example/checks/user_has_enough_balance.html b/example/templates/example/checks/user_has_enough_balance.html new file mode 100644 index 0000000..0a1384d --- /dev/null +++ b/example/templates/example/checks/user_has_enough_balance.html @@ -0,0 +1,20 @@ +{% extends "django_monitoring/detail.html" %} +{% load i18n %} + +{% block django_monitoring_check %} +

+ {% if result.status == result.STATUS.danger %} + {% trans "The balance of the user is critical. Their account was deactivated. Please contact them immediately." %} + {% elif result.status == result.STATUS.warning %} + {% trans "The balance of the user is running low. Please contact them and ask them to add more money to their balance." %} + {% elif result.status == result.STATUS.ok %} + {% trans "All good. Enough money on their balance." %} + {% else %} + {% trans "Status unknown." %} + {% endif %} +

+ +

+ {% trans "Current balance is" %}: {{ result.get_payload.balance|floatformat:2 }} +

+{% endblock %} diff --git a/example/urls.py b/example/urls.py new file mode 100644 index 0000000..1600fed --- /dev/null +++ b/example/urls.py @@ -0,0 +1,22 @@ +"""example URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.9/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.conf.urls import url, include + 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) +""" +from django.conf.urls import url, include +from django.contrib import admin + +urlpatterns = [ + url(r'^admin/', admin.site.urls), + url(r'^', include('django_monitoring.urls')), +] diff --git a/example/wsgi.py b/example/wsgi.py new file mode 100644 index 0000000..fd6d782 --- /dev/null +++ b/example/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for example project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") + +application = get_wsgi_application() diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..2605e37 --- /dev/null +++ b/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/requirements.txt b/requirements.txt index e69de29..fc8b414 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1,8 @@ +celery==3.1.23 +Django==1.9.8 +django-bootstrap3==7.0.1 +django-braces==1.9.0 +django-extensions==1.7.3 +django-model-utils==2.5.2 +psycopg2==2.6.2 +python-dateutil==2.5.3 diff --git a/setup.py b/setup.py index 6062d55..31f9751 100644 --- a/setup.py +++ b/setup.py @@ -1,18 +1,36 @@ +# -*- coding: UTF-8 -*- from setuptools import setup setup( name='django-monitoring', packages=['django_monitoring'], - version='0.0.0', - description='Django monitoring to make awesome checks', + version='0.1.0', + description='Django monitoring runs automated data checks in your Django installation', author='Jens Nistler', author_email='opensource@jensnistler.de', install_requires=[ + 'celery>=3.1.23', + 'Django>=1.9', + 'django-braces>=1.9.0', + 'django-extensions>=1.6.7', + 'django-model-utils>=2.5', + 'python-dateutil>=2.5.3', ], license='MIT', url='https://github.com/RegioHelden/django-monitoring', download_url='', keywords=['django', 'monitoring', 'check', 'checks'], - classifiers=[] + classifiers=[ + "Programming Language :: Python", + "Programming Language :: Python :: 2", + "Development Status :: 4 - Beta", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Topic :: Software Development", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: System :: Monitoring", + ] ) diff --git a/vagrant/salt/roots/salt/essentials.sls b/vagrant/salt/roots/salt/essentials.sls new file mode 100644 index 0000000..efc7388 --- /dev/null +++ b/vagrant/salt/roots/salt/essentials.sls @@ -0,0 +1,5 @@ +essentials: + pkg: + - installed + - names: + - gettext diff --git a/vagrant/salt/roots/salt/files/bashrc b/vagrant/salt/roots/salt/files/bashrc index 52f982c..ad6358d 100644 --- a/vagrant/salt/roots/salt/files/bashrc +++ b/vagrant/salt/roots/salt/files/bashrc @@ -4,10 +4,5 @@ alias ls='ls -al --color=auto' # activate virtual env . /home/vagrant/virtualenv/bin/activate -# django configuration -export DATABASE_URL="psql://vagrant:vagrant@127.0.0.1:5432/vagrant" -export DEBUG=True -export SECRET_KEY="fsdffuT*T*%B&*BT^R7rv4E%E^%R^" - # cd to vagrant dir cd /vagrant diff --git a/vagrant/salt/roots/salt/python.sls b/vagrant/salt/roots/salt/python.sls index a8013e0..18911d0 100644 --- a/vagrant/salt/roots/salt/python.sls +++ b/vagrant/salt/roots/salt/python.sls @@ -1,23 +1,22 @@ -python3: +python: pkg: - installed - names: - - python3-dev - - python3 + - python-dev + - python python-pip: pkg: - installed - names: - python-pip - - python3-pip - require: - - pkg: python3 + - pkg: python virtualenv: pip: - installed - - bin_env: '/usr/bin/pip3' + - bin_env: '/usr/bin/pip' - require: - pkg: python-pip diff --git a/vagrant/salt/roots/salt/rabbitmq.sls b/vagrant/salt/roots/salt/rabbitmq.sls new file mode 100644 index 0000000..6cbadcb --- /dev/null +++ b/vagrant/salt/roots/salt/rabbitmq.sls @@ -0,0 +1,8 @@ +rabbitmq-server: + pkg: + - installed + service: + - running + - enable: True + - require: + - pkg: rabbitmq-server diff --git a/vagrant/salt/roots/salt/top.sls b/vagrant/salt/roots/salt/top.sls index 2a35256..5d18961 100644 --- a/vagrant/salt/roots/salt/top.sls +++ b/vagrant/salt/roots/salt/top.sls @@ -1,6 +1,8 @@ base: '*': + - essentials - user - locale - postgresql - - python \ No newline at end of file + - python + - rabbitmq