Skip to content

Commit

Permalink
Merge pull request #2097 from onaio/2053-health-check-endpoint
Browse files Browse the repository at this point in the history
Add `service_health` view function
  • Loading branch information
DavisRayM committed Jun 8, 2021
2 parents af8af2a + 9ac4117 commit 4db58e4
Show file tree
Hide file tree
Showing 9 changed files with 135 additions and 5 deletions.
12 changes: 10 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ services:
# it everytime we rebuild.
- ../.onadata_db:/var/lib/postgresql/data
queue:
image: rabbitmq
image: redis:6-alpine
web:
build:
context: .
Expand All @@ -31,4 +31,12 @@ services:
environment:
- SELECTED_PYTHON=python3.6
- INITDB=false

notifications:
image: emqx/emqx:4.3.2
ports:
- "1883:1883"
- "8080:8080"
volumes:
- ../.onadata_mqtt/data:/opt/emqx/data
- ../.onadata_mqtt/etc:/opt/emqx/etc
- ../.onadata_mqtt/log:/opt/emqx/log
1 change: 1 addition & 0 deletions extras/reserved_accounts.txt
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ editor
ei
email
end
enketo
f
fag
faq
Expand Down
40 changes: 40 additions & 0 deletions onadata/apps/main/tests/test_service_health.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import json
from django.http import HttpRequest
from django.db.utils import DatabaseError
from mock import patch

from onadata.apps.main.tests.test_base import TestBase
from onadata.apps.main.views import service_health


class TestServiceHealthView(TestBase):
def test_service_health(self):
"""
Test that the `service_health` view function
works as expected:
1. Returns a 200 when secondary services are healthy
2. Returns a 500 when a secondary service is not available
"""
req = HttpRequest()
resp = service_health(req)

self.assertEqual(resp.status_code, 200)
self.assertEqual(
json.loads(resp.content.decode('utf-8')),
{
'default-Database': 'OK',
'Cache-Service': 'OK'
})

with patch('onadata.apps.main.views.XForm') as xform_mock:
xform_mock.objects.using().first.side_effect = DatabaseError(
'Some database error')
resp = service_health(req)

self.assertEqual(resp.status_code, 500)
self.assertEqual(
json.loads(resp.content.decode('utf-8')),
{
'default-Database': 'Degraded state; Some database error',
'Cache-Service': 'OK'
})
5 changes: 4 additions & 1 deletion onadata/apps/main/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,10 @@
re_path(r'^favicon\.ico',
RedirectView.as_view(url='/static/images/favicon.ico',
permanent=True)),
re_path(r'^static/(?P<path>.*)$', staticfiles_views.serve)
re_path(r'^static/(?P<path>.*)$', staticfiles_views.serve),

# Health status
re_path(r'^status$', main_views.service_health)
]

CUSTOM_URLS = getattr(settings, 'CUSTOM_MAIN_URLS', None)
Expand Down
35 changes: 35 additions & 0 deletions onadata/apps/main/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.core.cache import cache
from django.core.files.storage import default_storage, get_storage_class
from django.db import IntegrityError, OperationalError
from django.http import (HttpResponse, HttpResponseBadRequest,
Expand Down Expand Up @@ -1423,6 +1424,40 @@ def enketo_preview(request, username, id_string):
return HttpResponseRedirect(enketo_preview_url)


def service_health(request):
"""
This endpoint checks whether the various services(Database, Cache, e.t.c )
of the application are running as expected. Returns a 200 Status code if
all is well and a 500 if a service is down
"""
service_degraded = False
service_statuses = {}

# Check if Database connections are present & data is retrievable
for database in getattr(settings, 'DATABASES').keys():
try:
XForm.objects.using(database).first()
except Exception as e:
service_statuses[f'{database}-Database'] = f'Degraded state; {e}'
service_degraded = True
else:
service_statuses[f'{database}-Database'] = 'OK'

# Check if cache is accessible
try:
cache.set('ping', 'pong')
cache.delete('ping')
except Exception as e:
service_statuses['Cache-Service'] = f'Degraded state; {e}'
else:
service_statuses['Cache-Service'] = 'OK'

return HttpResponse(
json.dumps(service_statuses),
status=500 if service_degraded else 200,
content_type='application/json')


@require_GET
@login_required
def username_list(request):
Expand Down
35 changes: 33 additions & 2 deletions onadata/settings/docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,39 @@
else:
TESTING_MODE = False

CELERY_BROKER_URL = 'amqp://guest:@queue:5672//'
CELERY_TASK_ALWAYS_EAGER = False
CELERY_BROKER_URL = 'redis://queue:6379'
CELERY_RESULT_BACKEND = 'redis://queue:6379'
CELERY_TASK_ALWAYS_EAGER = True
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_CACHE_BACKEND = 'memory'
CELERY_BROKER_CONNECTION_MAX_RETRIES = 2

CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://queue:6379',
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient"
},
}
}

NOTIFICATION_BACKENDS = {
'mqtt': {
'BACKEND': 'onadata.apps.messaging.backends.mqtt.MQTTBackend',
'OPTIONS': {
'HOST': 'notifications',
'PORT': 1883,
'QOS': 1,
'RETAIN': False,
'SECURE': False,
'TOPIC_BASE': 'onadata'
}
}
}
FULL_MESSAGE_PAYLOAD = True

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

if TESTING_MODE:
Expand All @@ -76,3 +102,8 @@
ENKETO_API_INSTANCE_IFRAME_URL = ENKETO_URL + "api_v1/instance/iframe"
else:
MEDIA_ROOT = os.path.join(PROJECT_ROOT, 'media/') # noqa

ENKETO_API_ALL_SURVEY_LINKS_PATH = '/api_v2/survey/all'
SUBMISSION_RETRIEVAL_THRESHOLD = 1000
CSV_FILESIZE_IMPORT_ASYNC_THRESHOLD = 100000

5 changes: 5 additions & 0 deletions requirements/base.pip
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ django-ordered-model==3.4.3
# via onadata
django-query-builder==2.0.1
# via onadata
django-redis==5.0.0
# via onadata
django-registration-redux==2.9
# via onadata
django-render-block==0.8.1
Expand All @@ -118,6 +120,7 @@ django==2.2.23
# django-guardian
# django-oauth-toolkit
# django-query-builder
# django-redis
# django-render-block
# django-reversion
# django-taggit
Expand Down Expand Up @@ -305,6 +308,8 @@ raven==6.10.0
# via onadata
recaptcha-client==1.0.6
# via onadata
redis==3.5.3
# via django-redis
requests-mock==1.9.2
# via onadata
requests==2.25.1
Expand Down
5 changes: 5 additions & 0 deletions requirements/dev.pip
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ django-ordered-model==3.4.3
# via onadata
django-query-builder==2.0.1
# via onadata
django-redis==5.0.0
# via onadata
django-registration-redux==2.9
# via onadata
django-render-block==0.8.1
Expand All @@ -129,6 +131,7 @@ django==2.2.23
# django-guardian
# django-oauth-toolkit
# django-query-builder
# django-redis
# django-render-block
# django-reversion
# django-taggit
Expand Down Expand Up @@ -353,6 +356,8 @@ raven==6.10.0
# via onadata
recaptcha-client==1.0.6
# via onadata
redis==3.5.3
# via django-redis
requests-mock==1.9.2
# via onadata
requests==2.25.1
Expand Down
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@
"appoptics-metrics",
# Deprecation tagging
"deprecated",
# Redis cache
"django-redis",
],
dependency_links=[
'https://github.com/onaio/python-digest/tarball/3af1bd0ef6114e24bf23d0e8fd9d7ebf389845d1#egg=python-digest', # noqa pylint: disable=line-too-long
Expand Down

0 comments on commit 4db58e4

Please sign in to comment.