Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/bare status #114

Merged
merged 5 commits into from
Jan 31, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ Contributors
* Daniel Widerin - https://github.com/saily
* Ryan Wilson-Perkin - https://github.com/ryanwilsonperkin
* David Hoffman - https://github.com/dhoffman34
* James M. Allen - https://github.com/jamesmallen
6 changes: 6 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
History
=======

0.15.0 (Unreleased)
-------------------

* [`#114 <https://github.com/mwarkentin/django-watchman/pull/114>`_] Add "bare" status view (@jamesmallen)


0.14.0 (2018-01-09)
-------------------

Expand Down
12 changes: 12 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,17 @@ requests, you can call ping:
It will return the text ``pong`` with a 200 status code. Calling this doesn't
run any of the checks.

Bare status view
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a 0.15.0 (Unreleased) heading to the history file and add a bullet for this feature?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 657f135

****************

If you would like a "bare" status view (one that doesn't report any details,
just ``HTTP 200`` if checks pass, and ``HTTP 500`` if any checks fail), you
can use the ``bare_status`` view by putting the following into ``urls.py``::

import watchman.views
# ...
url(r'^status/?$', watchman.views.bare_status),

Django management command
*************************

Expand Down Expand Up @@ -326,3 +337,4 @@ Instructions
2. Visit watchman json endpoint in your browser: http://127.0.0.1:8000/watchman/
3. Visit watchman dashboard in your browser: http://127.0.0.1:8000/watchman/dashboard/
4. Visit watchman ping in your browser: http://127.0.0.1:8000/watchman/ping/
5. Visit watchman bare status in your browser: http://127.0.0.1:8000/watchman/bare/
3 changes: 3 additions & 0 deletions sample_project/sample_project/urls.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from django.conf.urls import include, url
from django.contrib import admin
import watchman.views


urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url(r'^watchman/', include('watchman.urls')),
url(r'^watchman/bare/', watchman.views.bare_status, name='bare_status'),
]
49 changes: 49 additions & 0 deletions tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,55 @@ def test_returns_pong(self):
self.assertEqual(response['Content-Type'], 'text/plain')


class TestBareStatus(unittest.TestCase):
def setUp(self):
# Ensure that every test executes with separate settings
reload_settings()

def test_bare_status_success(self):
request = RequestFactory().get('/')
response = views.bare_status(request)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content.decode(), '')

@patch('watchman.checks._check_databases')
@override_settings(WATCHMAN_ERROR_CODE=503)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add another test that the default error code for the bare status view will be 500? You can just copy this one without the override.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 657f135

def test_bare_status_error(self, patched_check_databases):
reload_settings()
# Fake a DB error, ensure we get our error code
patched_check_databases.return_value = [{
"foo": {
"ok": False,
"error": "Fake DB Error",
"stacktrace": "Fake DB Stack Trace",
},
}]
request = RequestFactory().get('/', data={
'check': 'watchman.checks.databases',
})
response = views.bare_status(request)
self.assertEqual(response.status_code, 503)
self.assertEqual(response.content.decode(), '')

@patch('watchman.checks._check_databases')
def test_bare_status_default_error(self, patched_check_databases):
reload_settings()
# Fake a DB error, ensure we get our error code
patched_check_databases.return_value = [{
"foo": {
"ok": False,
"error": "Fake DB Error",
"stacktrace": "Fake DB Stack Trace",
},
}]
request = RequestFactory().get('/', data={
'check': 'watchman.checks.databases',
})
response = views.bare_status(request)
self.assertEqual(response.status_code, 500)
self.assertEqual(response.content.decode(), '')


class TestEmailCheck(DjangoTestCase):
def setUp(self):
# Ensure that every test executes with separate settings
Expand Down
16 changes: 8 additions & 8 deletions watchman/templates/watchman/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ <h3 class="{% if overall_status %}text-success{% else %}text-danger{% endif %}">
</tr>
</thead>
<tbody>
{% for type in checks %}
{% for type_name, type in checks.items %}
{% for status in type.statuses %}
<tr>
<td class="{% if type.ok %}success{% else %}{% if not status.ok %}danger{% else %}warning{% endif %}{# not status.ok #}{% endif %}{# type.ok #}">{{ type.type_singular|title }}</td>
<td class="{% if type.ok %}success{% else %}{% if not status.ok %}danger{% else %}warning{% endif %}{# not status.ok #}{% endif %}{# type.ok #}">{{ type_name|title }}</td>
<td class="{% if status.ok %}success{% else %}danger{% endif %}">{{ status.name }}</td>

{% if status.ok %}
Expand All @@ -64,7 +64,7 @@ <h3 class="{% if overall_status %}text-success{% else %}text-danger{% endif %}">
{% trans "ERROR!" %}

<div class="btn-group btn-group-xs pull-right">
<button type="button" class="btn btn-danger" data-toggle="modal" data-target="#{{ type.type }}{% if status.name %}-{{ status.name }}{% endif %}">{% trans "Traceback" %}</button>
<button type="button" class="btn btn-danger" data-toggle="modal" data-target="#{{ type_name }}{% if status.name %}-{{ status.name }}{% endif %}">{% trans "Traceback" %}</button>
</div>
</td>
{% endif %}{# status.ok #}
Expand All @@ -75,7 +75,7 @@ <h3 class="{% if overall_status %}text-success{% else %}text-danger{% endif %}">
<tr>
<td colspan="3">{% trans "No checks indicated." %}</td>
</tr>
{% endfor %}{# for type in checks #}
{% endfor %}{# for type_name, type in checks.items #}
</tbody>
</table>
</div><!-- .col -->
Expand All @@ -84,15 +84,15 @@ <h3 class="{% if overall_status %}text-success{% else %}text-danger{% endif %}">


{% block error_content %}
{% for type in checks %}
{% for type_name, type in checks.items %}
{% for status in type.statuses %}
{% if not status.ok %}
<div class="modal fade" id="{{ type.type }}{% if status.name %}-{{ status.name }}{% endif %}" tabindex="-1" role="dialog" aria-labelledby="{{ type.type }}{% if status.name %}-{{ status.name }}{% endif %}-title">
<div class="modal fade" id="{{ type_name }}{% if status.name %}-{{ status.name }}{% endif %}" tabindex="-1" role="dialog" aria-labelledby="{{ type_name }}{% if status.name %}-{{ status.name }}{% endif %}-title">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="{{ type.type }}{% if status.name %}-{{ status.name }}{% endif %}-title">{{ type.type|title }}{% if status.name %} - {{ status.name|title }}{% endif %}</h4>
<h4 class="modal-title" id="{{ type_name }}{% if status.name %}-{{ status.name }}{% endif %}-title">{{ type_name|title }}{% if status.name %} - {{ status.name|title }}{% endif %}</h4>
</div><!-- class="modal-header" -->
<div class="modal-body">
<h4><pre>{{ status.error }}</pre></h4>
Expand All @@ -105,7 +105,7 @@ <h4><pre>{{ status.error }}</pre></h4>
</div><!-- class="modal-dialog" -->
</div><!-- class="modal fade" -->
{% endif %}{# not status.ok #}
{% endfor %}{# for type in checks #}
{% endfor %}{# for status in type.statuses #}
{% endfor %}{# for type_name, type in checks.items #}
{% endblock %}{# error_content #}
{% endblock %}{# content #}
172 changes: 81 additions & 91 deletions watchman/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,11 @@ def _deprecation_warnings():
warnings.warn("`WATCHMAN_TOKEN` setting is deprecated, use `WATCHMAN_TOKENS` instead. It will be removed in django-watchman 1.0", DeprecationWarning)


@auth
@json_view
@non_atomic_requests
def status(request):
def run_checks(request):
_deprecation_warnings()

response = {}
http_code = 200
checks = {}
ok = True

check_list, skip_list = _get_check_params(request)

Expand All @@ -55,109 +52,102 @@ def status(request):
if type(_check[_type]) == dict:
result = _check[_type]
if not result['ok']:
http_code = settings.WATCHMAN_ERROR_CODE
ok = False
elif type(_check[_type]) == list:
for entry in _check[_type]:
for result in entry:
if not entry[result]['ok']:
http_code = settings.WATCHMAN_ERROR_CODE
response.update(_check)
ok = False
checks.update(_check)

return checks, ok


if len(response) == 0:
@auth
@json_view
@non_atomic_requests
def status(request):
checks, ok = run_checks(request)

if not checks:
raise Http404(_('No checks found'))
http_code = 200 if ok else settings.WATCHMAN_ERROR_CODE
return checks, http_code, {WATCHMAN_VERSION_HEADER: __version__}


return response, http_code, {WATCHMAN_VERSION_HEADER: __version__}
@non_atomic_requests
def bare_status(request):
checks, ok = run_checks(request)
http_code = 200 if ok else settings.WATCHMAN_ERROR_CODE
return HttpResponse(status=http_code, content_type='text/plain')


def ping(request):
_deprecation_warnings()

return HttpResponse('pong', content_type='text/plain')


@auth
@non_atomic_requests
def dashboard(request):
_deprecation_warnings()

check_types = []

check_list, skip_list = _get_check_params(request)

for check in get_checks(check_list=check_list, skip_list=skip_list):
if callable(check):
_check = check()

for _type in _check:
# For other systems (eg: email, storage) _check[_type] is a
# dictionary of status
#
# Example:
# {
# 'ok': True, # Status
# }
#
# Example:
# {
# 'ok': False, # Status
# 'error': "RuntimeError",
# 'stacktrace': "...",
# }
#
# For some systems (eg: cache, database) _check[_type] is a
# list of dictionaries of dictionaries of statuses
#
# Example:
# [
# {
# 'default': { # Cache/database name
# 'ok': True, # Status
# }
# },
# {
# 'non-default': { # Cache/database name
# 'ok': False, # Status
# 'error': "RuntimeError",
# 'stacktrace': "...",
# }
# },
# ]
#
statuses = []

if type(_check[_type]) == dict:
result = _check[_type]
statuses = [{
'name': '',
'ok': result['ok'],
'error': '' if result['ok'] else result['error'],
'stacktrace': '' if result['ok'] else result['stacktrace'],
}]

type_overall_status = _check[_type]['ok']

elif type(_check[_type]) == list:
for result in _check[_type]:
for name in result:
statuses.append({
'name': name,
'ok': result[name]['ok'],
'error': '' if result[name]['ok'] else result[name]['error'],
'stacktrace': '' if result[name]['ok'] else result[name]['stacktrace'],
})

type_overall_status = all(s['ok'] for s in statuses)

check_types.append({
'type': _type,
'type_singular': _type[:-1] if _type.endswith('s') else _type,
'ok': type_overall_status,
'statuses': statuses})

overall_status = all(type_status['ok'] for type_status in check_types)
checks, overall_status = run_checks(request)

expanded_checks = {}
for key, value in checks.items():
if isinstance(value, dict):
# For some systems (eg: email, storage) value is a
# dictionary of status
#
# Example:
# {
# 'ok': True, # Status
# }
#
# Example:
# {
# 'ok': False, # Status
# 'error': "RuntimeError",
# 'stacktrace': "...",
# }
single_status = value.copy()
single_status['name'] = ''
expanded_check = {
'ok': value['ok'],
'statuses': [single_status],
}
else:
# For other systems (eg: cache, database) value is a
# list of dictionaries of dictionaries of statuses
#
# Example:
# [
# {
# 'default': { # Cache/database name
# 'ok': True, # Status
# }
# },
# {
# 'non-default': { # Cache/database name
# 'ok': False, # Status
# 'error': "RuntimeError",
# 'stacktrace': "...",
# }
# },
# ]
statuses = []
for outer_status in value:
for name, inner_status in outer_status.items():
detail = inner_status.copy()
detail['name'] = name
statuses.append(detail)

expanded_check = {
'ok': all(detail['ok'] for detail in statuses),
'statuses': statuses,
}
expanded_checks[key] = expanded_check

response = render(request, 'watchman/dashboard.html', {
'checks': check_types,
'checks': expanded_checks,
'overall_status': overall_status
})

Expand Down