-
Notifications
You must be signed in to change notification settings - Fork 13
Assignment 6
- Student has Github account
- Student has Docker installed.
- Adequate bandwidth. This is not a cafe exercise.
- Student has completed Assignment 5
- Unit Testing and TDD
- Docker Containers
- Implement Django Rest Framework API
- Application Monitoring and Instrumentation
In the previous assignment we set up some basic tests and explored TDD. In this assignment We'll add an endpoint that can monitor the health of our API and underlying services.
Go to the assignment directory
cd hacku-devops-2017/assign6
docker-compose run web sniffer
APIs should have a health endpoint. This is an endpoint that monitoring software can use to validate the service is still available. The health endpoint should test the availability of any services on which our API relies. For example: SQL databases, services like Redis, ElasticSearch, or downstream APIs.
Set up test for /health
returning 200 OK
:
In user/tests.py
make sure we are importing all the classes we will need:
from django.test import TestCase, Client
from django.contrib.auth.models import User, AnonymousUser
from django.core.urlresolvers import reverse
from django.db import DatabaseError
from mock import patch
Add the following health test case to user/tests.py
:
class HealthTestCase(TestCase):
def setUp(self):
self.c = Client()
def test_health_endpoint(self):
response = self.c.get('/health')
assert response.status_code == 200, \
"Expect 200 OK. got: {}" . format (response.status_code)
In user/views.py
add our endpoints and register with the router:
class HealthViewSet(viewsets.ViewSet):
permission_classes = (AllowAny, )
def list(self, request, format=None):
data = { "status": "up" }
return response.Response(data)
and add this line to the end of the list of the routes:
router.register(r'health', HealthViewSet, base_name='health')
Notes:
- We're using a simple
ViewSet
here. This is the simplest of the available ViewSet classes. We'll simply override thelist()
method (GET /<base-name>/
). - We're going to use the
AllowAny
permission so this endpoint is open to the web - Don't forget to register it! (note: we need to specify a
base_name
because there is noqueryset
from which DRF can infer a base-name. We usehealth
. This means thatGET /health/
(reverse('health-list')
will route to ourlist()
method.
Check your terminal to ensure that we are passing.
In user/views.py
update our HealthViewSet class to do something more interesting:
class HealthViewSet(viewsets.ViewSet):
permission_classes = (AllowAny, )
def list(self, request, format=None):
## make sure we can connect to the database
all_statuses = []
status = "up"
db_status = self.__can_connect_to_db()
all_statuses.append(db_status)
if "down" in all_statuses:
status = "down"
data = {
"data": {
"explorer" : "/api-explorer",
},
"status": {
"db": db_status,
"status": status
}
}
return response.Response(data)
def __can_connect_to_db(self):
try:
user = User.objects.first()
return "up"
except Exception:
return "down"
We query the database. If anything goes wrong, we assume the database is down.
In user/tests.py
change your HealthTestCase class to:
class HealthTestCase(TestCase):
def setUp(self):
self.c = Client()
self.status_fields = ['db', 'status']
def test_health_endpoint_ok(self):
url = reverse('health-list')
response = self.c.get(url)
assert response.status_code == 200, \
"Expect 200 OK. got: {}" . format(response.status_code)
expected_fields = ["db", "status"]
for field in self.status_fields:
assert response.json().get("status", {}).get(field, None) == "up", \
"Expected field {} to exist" . format(field)
@patch.object(User.objects, 'first')
def test_determine_db_status(self, mock_query):
"""Health should not be ok if it cannot connect to the db"""
mock_query.side_effect = DatabaseError()
url = reverse('health-list')
response = self.c.get(url)
status = response.json().get("status", {})
db_status = status.get('db')
assert db_status == 'down', \
'Expect DB to be down. Got: {}' . format(db_status)
status = status.get('status')
assert status == 'down', \
'Expect status to be down. Got: {}' . format(status)
Notes:
We use patching here: @patch.object(User.objects, 'first')
mocks this method. We then specify:
mock_query.side_effect = DatabaseError()
So that when we run the code: User.objects.first()
we trigger a DatabaseError()
. That's pretty neat, cause now we can make sure that our code does what we expect in the case that there is an issue connecting to the database.