Skip to content

Commit

Permalink
Merge pull request #15 from mwarkentin/settings_checkers
Browse files Browse the repository at this point in the history
Allow custom checkers via the WATCHMAN_CHECKS setting
  • Loading branch information
mwarkentin committed Sep 4, 2014
2 parents bbbd8c6 + 2d41c4b commit 2bc24c5
Show file tree
Hide file tree
Showing 10 changed files with 222 additions and 27 deletions.
4 changes: 2 additions & 2 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ Credits
Development Lead
----------------

* Michael Warkentin <[email protected]>
* Michael Warkentin <[email protected]> - https://github.com/mwarkentin

Contributors
------------

None yet. Why not be the first?
* Keryn Knight - https://github.com/kezabelle
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ coverage:
open htmlcov/index.html

docs:
rm -f docs/django-watchman.rst
rm -f docs/watchman.rst
rm -f docs/modules.rst
sphinx-apidoc -o docs/ django-watchman
sphinx-apidoc -o docs/ watchman
$(MAKE) -C docs clean
$(MAKE) -C docs html
open docs/_build/html/index.html
Expand Down
36 changes: 36 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,39 @@ The value of this setting will be the **GET** parameter that you must pass in::
WATCHMAN_TOKEN_NAME = 'custom-token-name'

GET http://127.0.0.1:8000/watchman/?custom-token-name=:token

Custom checks
*************

django-watchman allows you to customize the checks which are run by modifying
the ``WATCHMAN_CHECKS`` setting. In ``settings.py``::

WATCHMAN_CHECKS = (
'module.path.to.callable',
'another.module.path.to.callable',
)

Checks now have the same contract as context processors: they consume a
``request`` and return a ``dict`` whose keys are applied to the JSON response::

def my_check(request):
return {'x': 1}

In the absence of any checks, a 404 is thrown, which is then handled by the
``json_view`` decorator.

Run a subset of available checks
********************************

A subset of checks may be run, by passing ``?check=module.path.to.callable&check=...``
in the request URL. Only the callables given in querystring, which are in the
``WATCHMAN_CHECKS`` should be run, eg::

curl -XGET http://127.0.0.1:8080/watchman/?check=watchman.views.caches_status

Default checks
--------------

By default, django-watchman will run checks against your databases
(``watchman.views.databases_status``) and caches (``watchman.views.caches_status``).
These will function even if you haven't configured the respective settings.
7 changes: 7 additions & 0 deletions docs/modules.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
watchman
========

.. toctree::
:maxdepth: 4

watchman
70 changes: 70 additions & 0 deletions docs/watchman.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
watchman package
================

Submodules
----------

watchman.checks module
----------------------

.. automodule:: watchman.checks
:members:
:undoc-members:
:show-inheritance:

watchman.decorators module
--------------------------

.. automodule:: watchman.decorators
:members:
:undoc-members:
:show-inheritance:

watchman.models module
----------------------

.. automodule:: watchman.models
:members:
:undoc-members:
:show-inheritance:

watchman.settings module
------------------------

.. automodule:: watchman.settings
:members:
:undoc-members:
:show-inheritance:

watchman.urls module
--------------------

.. automodule:: watchman.urls
:members:
:undoc-members:
:show-inheritance:

watchman.utils module
---------------------

.. automodule:: watchman.utils
:members:
:undoc-members:
:show-inheritance:

watchman.views module
---------------------

.. automodule:: watchman.views
:members:
:undoc-members:
:show-inheritance:


Module contents
---------------

.. automodule:: watchman
:members:
:undoc-members:
:show-inheritance:
41 changes: 33 additions & 8 deletions tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"""

from __future__ import unicode_literals

from django.test.client import RequestFactory
import json
import unittest

Expand All @@ -23,30 +23,55 @@ class TestWatchman(unittest.TestCase):
def setUp(self):
pass

@patch('watchman.views.check_databases')
@patch('watchman.checks._check_databases')
def test_response_content_type_json(self, patched_check_databases):
patched_check_databases.return_value = []
response = views.status('')
request = RequestFactory().get('/')
response = views.status(request)
self.assertEqual(response['Content-Type'], 'application/json')

@patch('watchman.views.check_databases')
@patch('watchman.checks._check_databases')
def test_response_contains_expected_checks(self, patched_check_databases):
expected_checks = ['caches', 'databases']
patched_check_databases.return_value = []
response = views.status('')
request = RequestFactory().get('/')
response = views.status(request)
content = json.loads(response.content)
self.assertItemsEqual(expected_checks, content.keys())

def test_check_database_handles_exception(self):
response = checks.check_database('foo')
response = checks._check_database('foo')
self.assertFalse(response['foo']['ok'])
self.assertEqual(response['foo']['error'], "The connection foo doesn't exist")

def test_check_cache_handles_exception(self):
expected_error = "Could not find backend 'foo': Could not find backend 'foo': foo doesn't look like a module path"
response = checks.check_cache('foo')
response = checks._check_cache('foo')
self.assertFalse(response['foo']['ok'])
self.assertEqual(response['foo']['error'], expected_error)
self.assertIn(response['foo']['error'], expected_error)

@patch('watchman.checks._check_databases')
def test_response_only_single_check(self, patched_check_databases):
patched_check_databases.return_value = []
request = RequestFactory().get('/', data={
'check': 'watchman.checks.databases_status',
})
response = views.status(request)
content = json.loads(response.content)
self.assertEqual(response.status_code, 200)
self.assertItemsEqual({'databases': []}, content)

@patch('watchman.checks._check_databases')
def test_response_404_when_none_specified(self, patched_check_databases):
patched_check_databases.return_value = []
request = RequestFactory().get('/', data={
'check': '',
})
response = views.status(request)
content = json.loads(response.content)
self.assertEqual(response.status_code, 404)
self.assertItemsEqual({'message': 'No checks found', 'error': 404},
content)

def tearDown(self):
pass
22 changes: 15 additions & 7 deletions watchman/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@

import traceback
import uuid

from django.conf import settings
from django.core.cache import get_cache
from django.db import connections


def check_databases(databases):
return [check_database(database) for database in databases]
def _check_databases(databases):
return [_check_database(database) for database in databases]


def check_database(database):
def _check_database(database):
try:
connections[database].introspection.table_names()
response = {database: {"ok": True}}
Expand All @@ -28,11 +28,11 @@ def check_database(database):
return response


def check_caches(caches):
return [check_cache(cache) for cache in caches]
def _check_caches(caches):
return [_check_cache(cache) for cache in caches]


def check_cache(cache_name):
def _check_cache(cache_name):
key = str(uuid.uuid4())
value = str(uuid.uuid4())
try:
Expand All @@ -50,3 +50,11 @@ def check_cache(cache_name):
},
}
return response


def caches_status(request):
return {"caches": _check_caches(settings.CACHES)}


def databases_status(request):
return {'databases': _check_databases(settings.DATABASES)}
8 changes: 7 additions & 1 deletion watchman/settings.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
from django.conf import settings


# TODO: these should not be module level.
WATCHMAN_TOKEN = getattr(settings, 'WATCHMAN_TOKEN', None)
WATCHMAN_TOKEN_NAME = getattr(settings, 'WATCHMAN_TOKEN_NAME', 'watchman-token')
DEFAULT_CHECKS = (
'watchman.checks.caches_status',
'watchman.checks.databases_status',
)

WATCHMAN_CHECKS = getattr(settings, 'WATCHMAN_CHECKS', DEFAULT_CHECKS)
36 changes: 36 additions & 0 deletions watchman/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
try: # try for Django 1.7+ first.
from django.utils.module_loading import import_string
except ImportError: # < Django 1.7
try:
from django.utils.module_loading import import_by_path as import_string
except ImportError: # < Django 1.5.3 (including 1.4 LTS)
import sys
from django.utils import six
from django.utils.importlib import import_module
from django.core.exceptions import ImproperlyConfigured

def import_string(dotted_path, error_prefix=''):
try:
module_path, class_name = dotted_path.rsplit('.', 1)
except ValueError:
raise ImproperlyConfigured("%s%s doesn't look like a module path" % (
error_prefix, dotted_path))
try:
module = import_module(module_path)
except ImportError as e:
msg = '%sError importing module %s: "%s"' % (
error_prefix, module_path, e)
six.reraise(ImproperlyConfigured, ImproperlyConfigured(msg),
sys.exc_info()[2])
try:
attr = getattr(module, class_name)
except AttributeError:
raise ImproperlyConfigured('%sModule "%s" does not define a "%s" attribute/class' % (
error_prefix, module_path, class_name))
return attr


def get_checks(paths_to_checks):
for python_path in paths_to_checks:
yield import_string(python_path)
21 changes: 14 additions & 7 deletions watchman/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,26 @@

from __future__ import unicode_literals

from django.conf import settings
from django.http import Http404

from jsonview.decorators import json_view

from watchman.checks import check_caches, check_databases
from watchman.utils import get_checks
from watchman.settings import WATCHMAN_CHECKS
from watchman.decorators import token_required


@token_required
@json_view
def status(request):
response = {
"caches": check_caches(settings.CACHES),
"databases": check_databases(settings.DATABASES),
}
response = {}
available_checks = frozenset(WATCHMAN_CHECKS)
# allow for asking for only a subset back.
if len(request.GET) > 0 and 'check' in request.GET:
possible_filters = frozenset(request.GET.getlist('check'))
available_checks &= possible_filters
for func in get_checks(paths_to_checks=available_checks):
if callable(func):
response.update(func(request))
if len(response) == 0:
raise Http404('No checks found')
return response

0 comments on commit 2bc24c5

Please sign in to comment.