From c5520450bae1fb12b63bfcae81101867802c7427 Mon Sep 17 00:00:00 2001 From: Mitsuo Takaki Date: Fri, 29 Apr 2016 08:48:53 -0700 Subject: [PATCH] Adding new expectation (Regex) and creating unit tests. --- .gitignore | 3 + cachet_url_monitor/configuration.py | 37 +++++++++-- cachet_url_monitor/scheduler.py | 7 ++ config.yml | 3 +- dev_requirements.txt | 3 + setup.py | 11 ++++ tests/test_configuration.py | 11 ++++ tests/test_expectation.py | 99 +++++++++++++++++++++++++++++ 8 files changed, 167 insertions(+), 7 deletions(-) create mode 100644 setup.py create mode 100644 tests/test_configuration.py create mode 100644 tests/test_expectation.py diff --git a/.gitignore b/.gitignore index 099d114..9b369ae 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ share/ .Python *.swp *.pyc +.cache +.coverage +*.egg-info diff --git a/cachet_url_monitor/configuration.py b/cachet_url_monitor/configuration.py index ab44ecb..c7c7cca 100644 --- a/cachet_url_monitor/configuration.py +++ b/cachet_url_monitor/configuration.py @@ -1,6 +1,7 @@ #!/usr/bin/env python import abc import logging +import re import requests import timeit from yaml import load @@ -20,10 +21,14 @@ class Configuration(object): in self.data['endpoint']['expectation']] def evaluate(self): - #TODO(mtakaki|2016-04-27): Add support to configurable timeout. + """Sends the request to the URL set in the configuration and executes + each one of the expectations, one by one. The status will be updated + according to the expectation results. + """ try: self.request = requests.request(self.data['endpoint']['method'], - self.data['endpoint']['url']) + self.data['endpoint']['url'], + timeout=self.data['endpoint']['timeout']) except requests.ConnectionError: logging.warning('The URL is unreachable: %s %s' % (self.data['endpoint']['method'], @@ -39,9 +44,8 @@ class Configuration(object): self.status = 3 return - # We, by default, assume the API is healthy. + # We initially assume the API is healthy. self.status = 1 - self.message = '' for expectation in self.expectations: status = expectation.get_status(self.request) @@ -66,11 +70,18 @@ class Configuration(object): ' status: [%d]' % (component_request.status_code, self.status)) class Expectaction(object): + """Base class for URL result expectations. Any new excpectation should extend + this class and the name added to create() method. + """ @staticmethod def create(configuration): + """Creates a list of expectations based on the configuration types + list. + """ expectations = { 'HTTP_STATUS': HttpStatus, - 'LATENCY': Latency + 'LATENCY': Latency, + 'REGEX': Regex } return expectations.get(configuration['type'])(configuration) @@ -110,4 +121,18 @@ class Latency(Expectaction): return 2 def get_message(self, response): - return 'Latency above threshold: %d' % (response.elapsed.total_seconds(),) + return 'Latency above threshold: %.4f' % (response.elapsed.total_seconds(),) + + +class Regex(Expectaction): + def __init__(self, configuration): + self.regex = re.compile(configuration['regex']) + + def get_status(self, response): + if self.regex.match(response.text): + return 1 + else: + return 3 + + def get_message(self, response): + return 'Regex did not match anything in the body' diff --git a/cachet_url_monitor/scheduler.py b/cachet_url_monitor/scheduler.py index 1377eb6..c990280 100644 --- a/cachet_url_monitor/scheduler.py +++ b/cachet_url_monitor/scheduler.py @@ -7,14 +7,21 @@ import time class Agent(object): + """Monitor agent that will be constantly verifying if the URL is healthy + and updating the component. + """ def __init__(self, configuration): self.configuration = configuration def execute(self): + """Will verify the API status and push the status and metrics to the + cachet server. + """ self.configuration.evaluate() self.configuration.push_status_and_metrics() def start(self): + """Sets up the schedule based on the configuration file.""" schedule.every(self.configuration.data['frequency']).seconds.do(self.execute) diff --git a/config.yml b/config.yml index edd6ae6..8eee8d3 100644 --- a/config.yml +++ b/config.yml @@ -1,6 +1,7 @@ endpoint: - url: http://www.google.com + url: http://localhost:8080/swagger method: GET + timeout: 0.010 expectation: - type: HTTP_STATUS status: 200 diff --git a/dev_requirements.txt b/dev_requirements.txt index 07e2a84..66f2356 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,5 +1,8 @@ PyYAML==3.11 ipython==4.2.0 +mock==2.0.0 pudb==2016.1 +pytest==2.9.1 +pytest-cov==2.2.1 requests==2.9.1 schedule==0.3.2 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..8b61fc6 --- /dev/null +++ b/setup.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +from distutils.core import setup + +setup(name='cachet-url-monitor', + version='0.1', + description='Cachet URL monitor plugin', + author='Mitsuo Takaki', + author_email='mitsuotakaki@gmail.com', + url='https://github.com/mtakaki/cachet-url-monitor', + packages=['cachet_url_monitor'], + ) diff --git a/tests/test_configuration.py b/tests/test_configuration.py new file mode 100644 index 0000000..28aa9c8 --- /dev/null +++ b/tests/test_configuration.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +import unittest +from cachet_url_monitor.configuration import Configuration + + +class ConfigurationTest(unittest.TestCase): + def test_init(self): + configuration = Configuration('config.yml') + + assert len(configuration.data) == 3 + assert len(configuration.expectations) == 2 diff --git a/tests/test_expectation.py b/tests/test_expectation.py new file mode 100644 index 0000000..e3d6c35 --- /dev/null +++ b/tests/test_expectation.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python +import mock +import re +import unittest +from cachet_url_monitor.configuration import Expectaction,Latency +from cachet_url_monitor.configuration import HttpStatus,Regex + + +class LatencyTest(unittest.TestCase): + def setUp(self): + self.expectation = Latency({'type': 'LATENCY', 'threshold': 1}) + + def test_init(self): + assert self.expectation.threshold == 1 + + def test_get_status_healthy(self): + def total_seconds(): + return 0.1 + request = mock.Mock() + elapsed = mock.Mock() + request.elapsed = elapsed + elapsed.total_seconds = total_seconds + + assert self.expectation.get_status(request) == 1 + + def test_get_status_unhealthy(self): + def total_seconds(): + return 2 + request = mock.Mock() + elapsed = mock.Mock() + request.elapsed = elapsed + elapsed.total_seconds = total_seconds + + assert self.expectation.get_status(request) == 2 + + def test_get_message(self): + def total_seconds(): + return 0.1 + request = mock.Mock() + elapsed = mock.Mock() + request.elapsed = elapsed + elapsed.total_seconds = total_seconds + + assert self.expectation.get_message(request) == ('Latency above ' + 'threshold: 0.1000') + + +class HttpStatusTest(unittest.TestCase): + def setUp(self): + self.expectation = HttpStatus({'type': 'HTTP_STATUS', 'status': 200}) + + def test_init(self): + assert self.expectation.status == 200 + + def test_get_status_healthy(self): + request = mock.Mock() + request.status_code = 200 + + assert self.expectation.get_status(request) == 1 + + def test_get_status_unhealthy(self): + request = mock.Mock() + request.status_code = 400 + + assert self.expectation.get_status(request) == 3 + + def test_get_message(self): + request = mock.Mock() + request.status_code = 400 + + assert self.expectation.get_message(request) == ('Unexpected HTTP ' + 'status (400)') + + +class RegexTest(unittest.TestCase): + def setUp(self): + self.expectation = Regex({'type': 'REGEX', 'regex': '.*(found stuff).*'}) + + def test_init(self): + assert self.expectation.regex == re.compile('.*(found stuff).*') + + def test_get_status_healthy(self): + request = mock.Mock() + request.text = 'We cound found stuff in this body.' + + assert self.expectation.get_status(request) == 1 + + def test_get_status_unhealthy(self): + request = mock.Mock() + request.text = 'We will not find here' + + assert self.expectation.get_status(request) == 3 + + def test_get_message(self): + request = mock.Mock() + request.text = 'We will not find here' + + assert self.expectation.get_message(request) == ('Regex did not match ' + 'anything in the body')