Skip to content

Commit

Permalink
Add Redis Health Check (#225)
Browse files Browse the repository at this point in the history
  • Loading branch information
ingvaldlorentzen authored and codingjoe committed Sep 6, 2019
1 parent c389ea2 commit 91dd5f0
Show file tree
Hide file tree
Showing 9 changed files with 149 additions and 2 deletions.
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ env:
- DJANGO=111
- DJANGO=22
- DJANGO=master
services:
- redis-server
matrix:
fast_finish: true
allow_failures:
Expand Down
7 changes: 7 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ Add the ``health_check`` applications to your ``INSTALLED_APPS``:
'health_check.contrib.psutil', # disk and memory utilization; requires psutil
'health_check.contrib.s3boto_storage', # requires boto and S3BotoStorage backend
'health_check.contrib.rabbitmq', # requires RabbitMQ broker
'health_check.contrib.redis', # required Redis broker
]
(Optional) If using the ``psutil`` app, you can configure disk and memory
Expand All @@ -99,6 +100,12 @@ on django.conf.settings with the required format to connect to your rabbit serve
BROKER_URL = amqp://myuser:mypassword@localhost:5672/myvhost
To use the Redis healthcheck, please make sure that there is a variable named ``REDIS_URL``
on django.conf.settings with the required format to connect to your redis server. For example:

.. code::
REDIS_URL = redis://localhost:6370
Setting up monitoring
---------------------
Expand Down
2 changes: 1 addition & 1 deletion health_check/contrib/rabbitmq/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ def check_status(self):
except BaseException as e:
self.add_error(ServiceUnavailable("Unknown error"), e)
else:
logger.debug("Connection estabilished. RabbitMQ is healthy.")
logger.debug("Connection established. RabbitMQ is healthy.")
1 change: 1 addition & 0 deletions health_check/contrib/redis/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
default_app_config = 'health_check.contrib.redis.apps.HealthCheckConfig'
12 changes: 12 additions & 0 deletions health_check/contrib/redis/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from django.apps import AppConfig

from health_check.plugins import plugin_dir


class HealthCheckConfig(AppConfig):
name = "health_check.contrib.redis"

def ready(self):
from .backends import RedisHealthCheck

plugin_dir.register(RedisHealthCheck)
35 changes: 35 additions & 0 deletions health_check/contrib/redis/backends.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import logging

from django.conf import settings
from redis import exceptions, from_url

from health_check.backends import BaseHealthCheckBackend
from health_check.exceptions import ServiceUnavailable

logger = logging.getLogger(__name__)


class RedisHealthCheck(BaseHealthCheckBackend):
"""Health check for Redis."""

redis_url = getattr(settings, "REDIS_URL", 'redis://localhost/1')

def check_status(self):
"""Check Redis service by pinging the redis instance with a redis connection."""
logger.debug("Got %s as the redis_url. Connecting to redis...", self.redis_url)

logger.debug("Attempting to connect to redis...")
try:
# conn is used as a context to release opened resources later
with from_url(self.redis_url) as conn:
conn.ping() # exceptions may be raised upon ping
except ConnectionRefusedError as e:
self.add_error(ServiceUnavailable("Unable to connect to Redis: Connection was refused."), e)
except exceptions.TimeoutError as e:
self.add_error(ServiceUnavailable("Unable to connect to Redis: Timeout."), e)
except exceptions.ConnectionError as e:
self.add_error(ServiceUnavailable("Unable to connect to Redis: Connection Error"), e)
except BaseException as e:
self.add_error(ServiceUnavailable("Unknown error"), e)
else:
logger.debug("Connection established. Redis is healthy.")
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ pydocstyle
pep8-naming
pytest<4
pytest-django
redis==3.3.8
3 changes: 2 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,12 @@ setup_requires =
sphinx
pytest-runner
tests_require =
mock
pytest
pytest-cov
pytest-django
celery
mock
redis

[bdist_wheel]
universal = 1
Expand Down
88 changes: 88 additions & 0 deletions tests/test_redis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import mock
from redis.exceptions import ConnectionError, TimeoutError

from health_check.contrib.redis.backends import RedisHealthCheck


class TestRedisHealthCheck:
"""Test Redis health check."""

@mock.patch("health_check.contrib.redis.backends.getattr")
@mock.patch("health_check.contrib.redis.backends.from_url", autospec=True)
def test_redis_refused_connection(self, mocked_connection, mocked_getattr):
"""Test when the connection to Redis is refused."""
mocked_getattr.return_value = "redis_url"

# mock returns
mocked_connection.return_value = mock.MagicMock()
mocked_connection.return_value.__enter__.side_effect = ConnectionRefusedError("Refused connection")

# instantiates the class
redis_healthchecker = RedisHealthCheck()

# invokes the method check_status()
redis_healthchecker.check_status()
assert len(redis_healthchecker.errors), 1

# mock assertions
mocked_connection.assert_called_once_with('redis://localhost/1')

@mock.patch("health_check.contrib.redis.backends.getattr")
@mock.patch("health_check.contrib.redis.backends.from_url")
def test_redis_timeout_error(self, mocked_connection, mocked_getattr):
"""Test Redis TimeoutError."""
mocked_getattr.return_value = "redis_url"

# mock returns
mocked_connection.return_value = mock.MagicMock()
mocked_connection.return_value.__enter__.side_effect = TimeoutError("Timeout Error")

# instantiates the class
redis_healthchecker = RedisHealthCheck()

# invokes the method check_status()
redis_healthchecker.check_status()
assert len(redis_healthchecker.errors), 1

# mock assertions
mocked_connection.assert_called_once_with('redis://localhost/1')

@mock.patch("health_check.contrib.redis.backends.getattr")
@mock.patch("health_check.contrib.redis.backends.from_url")
def test_redis_con_limit_exceeded(self, mocked_connection, mocked_getattr):
"""Test Connection Limit Exceeded error."""
mocked_getattr.return_value = "redis_url"

# mock returns
mocked_connection.return_value = mock.MagicMock()
mocked_connection.return_value.__enter__.side_effect = ConnectionError("Connection Error")

# instantiates the class
redis_healthchecker = RedisHealthCheck()

# invokes the method check_status()
redis_healthchecker.check_status()
assert len(redis_healthchecker.errors), 1

# mock assertions
mocked_connection.assert_called_once_with('redis://localhost/1')

@mock.patch("health_check.contrib.redis.backends.getattr")
@mock.patch("health_check.contrib.redis.backends.from_url")
def test_redis_conn_ok(self, mocked_connection, mocked_getattr):
"""Test everything is OK."""
mocked_getattr.return_value = "redis_url"

# mock returns
mocked_connection.return_value = mock.MagicMock()
mocked_connection.return_value.__enter__.side_effect = True

# instantiates the class
redis_healthchecker = RedisHealthCheck()

# invokes the method check_status()
redis_healthchecker.check_status()
assert len(redis_healthchecker.errors), 0

# mock assertions
mocked_connection.assert_called_once_with('redis://localhost/1')

0 comments on commit 91dd5f0

Please sign in to comment.