Documenting better the code and small tweaks to the unit tests.

This commit is contained in:
Mitsuo Takaki
2016-05-20 02:23:39 -07:00
parent a3a91edadc
commit d63420ac01
2 changed files with 72 additions and 66 deletions

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
import abc
import logging import logging
import time import time
import abc
import cachet_url_monitor.status import cachet_url_monitor.status
import os import os
import re import re
@@ -37,6 +37,20 @@ class ComponentNonexistentError(Exception):
return repr('Component with id [%d] does not exist.' % (self.component_id,)) return repr('Component with id [%d] does not exist.' % (self.component_id,))
def get_current_status(endpoint_url, component_id, headers):
"""Retrieves the current status of the component that is being monitored. It will fail if the component does
not exist or doesn't respond with the expected data.
:return component status.
"""
get_status_request = requests.get('%s/components/%s' % (endpoint_url, component_id), headers=headers)
if get_status_request.ok:
# The component exists.
return get_status_request.json()['data']['status']
else:
raise ComponentNonexistentError(component_id)
class Configuration(object): class Configuration(object):
"""Represents a configuration file, but it also includes the functionality """Represents a configuration file, but it also includes the functionality
of assessing the API and pushing the results to cachet. of assessing the API and pushing the results to cachet.
@@ -50,32 +64,25 @@ class Configuration(object):
# We need to validate the configuration is correct and then validate the component actually exists. # We need to validate the configuration is correct and then validate the component actually exists.
self.validate() self.validate()
# We store the main information from the configuration file, so we don't keep reading from the data dictionary.
self.headers = {'X-Cachet-Token': os.environ.get('CACHET_TOKEN') or self.data['cachet']['token']} self.headers = {'X-Cachet-Token': os.environ.get('CACHET_TOKEN') or self.data['cachet']['token']}
self.endpoint_method = os.environ.get('ENDPOINT_METHOD') or self.data['endpoint']['method'] self.endpoint_method = os.environ.get('ENDPOINT_METHOD') or self.data['endpoint']['method']
self.endpoint_url = os.environ.get('ENDPOINT_URL') or self.data['endpoint']['url'] self.endpoint_url = os.environ.get('ENDPOINT_URL') or self.data['endpoint']['url']
self.endpoint_timeout = os.environ.get('ENDPOINT_TIMEOUT') or self.data['endpoint'].get('timeout') self.endpoint_timeout = os.environ.get('ENDPOINT_TIMEOUT') or self.data['endpoint'].get('timeout') or 1
self.api_url = os.environ.get('CACHET_API_URL') or self.data['cachet']['api_url'] self.api_url = os.environ.get('CACHET_API_URL') or self.data['cachet']['api_url']
self.component_id = os.environ.get('CACHET_COMPONENT_ID') or self.data['cachet']['component_id'] self.component_id = os.environ.get('CACHET_COMPONENT_ID') or self.data['cachet']['component_id']
self.metric_id = os.environ.get('CACHET_METRIC_ID') or self.data['cachet'].get('metric_id') self.metric_id = os.environ.get('CACHET_METRIC_ID') or self.data['cachet'].get('metric_id')
self.status = self.get_current_status(self.api_url, self.component_id, self.headers) # We need the current status so we monitor the status changes. This is necessary for creating incidents.
self.status = get_current_status(self.api_url, self.component_id, self.headers)
self.logger.info('Monitoring URL: %s %s' % (self.endpoint_method, self.endpoint_url)) self.logger.info('Monitoring URL: %s %s' % (self.endpoint_method, self.endpoint_url))
self.expectations = [Expectaction.create(expectation) for expectation in self.data['endpoint']['expectation']] self.expectations = [Expectaction.create(expectation) for expectation in self.data['endpoint']['expectation']]
for expectation in self.expectations: for expectation in self.expectations:
self.logger.info('Registered expectation: %s' % (expectation,)) self.logger.info('Registered expectation: %s' % (expectation,))
def get_current_status(self, endpoint_url, component_id, headers):
get_status_request = requests.get('%s/components/%s' % (endpoint_url, component_id), headers=headers)
if get_status_request.ok:
# The component exists.
return get_status_request.json()['data']['status']
else:
raise ComponentNonexistentError(component_id)
def is_create_incident(self): def is_create_incident(self):
"""Will verify if the configuration is set to create incidents or not. """Will verify if the configuration is set to create incidents or not.
:return True if the configuration is set to create incidents or False it otherwise. :return True if the configuration is set to create incidents or False it otherwise.
@@ -113,16 +120,11 @@ class Configuration(object):
each one of the expectations, one by one. The status will be updated each one of the expectations, one by one. The status will be updated
according to the expectation results. according to the expectation results.
""" """
if hasattr(self, 'status'):
# Keeping track of the previous status.
self.previous_status = self.status
try: try:
self.request = requests.request(self.endpoint_method, self.endpoint_url, timeout=self.endpoint_timeout) self.request = requests.request(self.endpoint_method, self.endpoint_url, timeout=self.endpoint_timeout)
self.current_timestamp = int(time.time()) self.current_timestamp = int(time.time())
except requests.ConnectionError: except requests.ConnectionError:
self.message = 'The URL is unreachable: %s %s' % ( self.message = 'The URL is unreachable: %s %s' % (self.endpoint_method, self.endpoint_url)
self.data['endpoint']['method'], self.data['endpoint']['url'])
self.logger.warning(self.message) self.logger.warning(self.message)
self.status = cachet_url_monitor.status.COMPONENT_STATUS_PARTIAL_OUTAGE self.status = cachet_url_monitor.status.COMPONENT_STATUS_PARTIAL_OUTAGE
return return
@@ -170,9 +172,7 @@ class Configuration(object):
if 'metric_id' in self.data['cachet'] and hasattr(self, 'request'): if 'metric_id' in self.data['cachet'] and hasattr(self, 'request'):
params = {'id': self.metric_id, 'value': self.request.elapsed.total_seconds(), params = {'id': self.metric_id, 'value': self.request.elapsed.total_seconds(),
'timestamp': self.current_timestamp} 'timestamp': self.current_timestamp}
metrics_request = requests.post('%s/metrics/%d/points' % metrics_request = requests.post('%s/metrics/%d/points' % (self.api_url, self.metric_id), params=params,
(self.data['cachet']['api_url'],
self.data['cachet']['metric_id']), params=params,
headers=self.headers) headers=self.headers)
if metrics_request.ok: if metrics_request.ok:
@@ -184,13 +184,16 @@ class Configuration(object):
(metrics_request.status_code,)) (metrics_request.status_code,))
def push_incident(self): def push_incident(self):
if hasattr(self, 'incident_id') and self.status == 1: """If the component status has changed, we create a new incident (if this is the first time it becomes unstable)
# If the incident already exists, it means it's unhealthy. We only update it when it becomes healthy again. or updates the existing incident once it becomes healthy again.
params = {'status': 4, 'visible': 1, 'component_id': self.data['cachet']['component_id'], """
'component_status': self.status, 'notify': True} if hasattr(self, 'incident_id') and self.status == cachet_url_monitor.status.COMPONENT_STATUS_OPERATIONAL:
# If the incident already exists, it means it was unhealthy but now it's healthy again.
params = {'status': 4, 'visible': 1, 'component_id': self.component_id, 'component_status': self.status,
'notify': True}
incident_request = requests.put('%s/incidents/%d' % (self.data['cachet']['api_url'], self.incident_id), incident_request = requests.put('%s/incidents/%d' % (self.api_url, self.incident_id), params=params,
params=params, headers=self.headers) headers=self.headers)
if incident_request.ok: if incident_request.ok:
# Successful metrics upload # Successful metrics upload
self.logger.info( self.logger.info(
@@ -198,16 +201,13 @@ class Configuration(object):
self.status, self.message)) self.status, self.message))
del self.incident_id del self.incident_id
else: else:
self.logger.warning( self.logger.warning('Incident update failed with status [%d], message: "%s"' % (
'Incident update failed with status [%d], message: "%s"' % ( incident_request.status_code, self.message))
incident_request.status_code, self.message)) elif not hasattr(self, 'incident_id') and self.status != cachet_url_monitor.status.COMPONENT_STATUS_OPERATIONAL:
elif not hasattr(self, 'incident_id') and self.status != 1:
# This is the first time the incident is being created. # This is the first time the incident is being created.
params = {'name': 'URL unavailable', 'message': self.message, 'status': 1, 'visible': 1, params = {'name': 'URL unavailable', 'message': self.message, 'status': 1, 'visible': 1,
'component_id': self.data['cachet']['component_id'], 'component_status': self.status, 'component_id': self.component_id, 'component_status': self.status, 'notify': True}
'notify': True} incident_request = requests.post('%s/incidents' % (self.api_url,), params=params, headers=self.headers)
incident_request = requests.post('%s/incidents' % (self.data['cachet']['api_url'],), params=params,
headers=self.headers)
if incident_request.ok: if incident_request.ok:
# Successful incident upload. # Successful incident upload.
self.incident_id = incident_request.json()['data']['id'] self.incident_id = incident_request.json()['data']['id']

View File

@@ -38,10 +38,11 @@ class ConfigurationTest(unittest.TestCase):
sys.modules['requests'].HTTPError = HTTPError sys.modules['requests'].HTTPError = HTTPError
def test_init(self): def test_init(self):
assert len(self.configuration.data) == 3 self.assertEqual(len(self.configuration.data), 3, 'Configuration data size is incorrect')
assert len(self.configuration.expectations) == 3 self.assertEquals(len(self.configuration.expectations), 3, 'Number of expectations read from file is incorrect')
assert self.configuration.headers == {'X-Cachet-Token': 'token2'} self.assertDictEqual(self.configuration.headers, {'X-Cachet-Token': 'token2'}, 'Header was not set correctly')
assert self.configuration.api_url == 'https://demo.cachethq.io/api/v1' self.assertEquals(self.configuration.api_url, 'https://demo.cachethq.io/api/v1',
'Cachet API URL was set incorrectly')
def test_evaluate(self): def test_evaluate(self):
def total_seconds(): def total_seconds():
@@ -58,7 +59,8 @@ class ConfigurationTest(unittest.TestCase):
sys.modules['requests'].request = request sys.modules['requests'].request = request
self.configuration.evaluate() self.configuration.evaluate()
assert self.configuration.status == cachet_url_monitor.status.COMPONENT_STATUS_OPERATIONAL self.assertEquals(self.configuration.status, cachet_url_monitor.status.COMPONENT_STATUS_OPERATIONAL,
'Component status set incorrectly')
def test_evaluate_with_failure(self): def test_evaluate_with_failure(self):
def total_seconds(): def total_seconds():
@@ -76,76 +78,80 @@ class ConfigurationTest(unittest.TestCase):
sys.modules['requests'].request = request sys.modules['requests'].request = request
self.configuration.evaluate() self.configuration.evaluate()
assert self.configuration.status == cachet_url_monitor.status.COMPONENT_STATUS_PARTIAL_OUTAGE self.assertEquals(self.configuration.status, cachet_url_monitor.status.COMPONENT_STATUS_PARTIAL_OUTAGE,
'Component status set incorrectly')
def test_evaluate_with_timeout(self): def test_evaluate_with_timeout(self):
def request(method, url, timeout=None): def request(method, url, timeout=None):
assert method == 'GET' self.assertEquals(method, 'GET', 'Incorrect HTTP method')
assert url == 'http://localhost:8080/swagger' self.assertEquals(url, 'http://localhost:8080/swagger', 'Monitored URL is incorrect')
assert timeout == 0.010 self.assertEquals(timeout, 0.010)
raise Timeout() raise Timeout()
sys.modules['requests'].request = request sys.modules['requests'].request = request
self.configuration.evaluate() self.configuration.evaluate()
assert self.configuration.status == cachet_url_monitor.status.COMPONENT_STATUS_PERFORMANCE_ISSUES self.assertEquals(self.configuration.status, cachet_url_monitor.status.COMPONENT_STATUS_PERFORMANCE_ISSUES,
'Component status set incorrectly')
self.mock_logger.warning.assert_called_with('Request timed out') self.mock_logger.warning.assert_called_with('Request timed out')
def test_evaluate_with_connection_error(self): def test_evaluate_with_connection_error(self):
def request(method, url, timeout=None): def request(method, url, timeout=None):
assert method == 'GET' self.assertEquals(method, 'GET', 'Incorrect HTTP method')
assert url == 'http://localhost:8080/swagger' self.assertEquals(url, 'http://localhost:8080/swagger', 'Monitored URL is incorrect')
assert timeout == 0.010 self.assertEquals(timeout, 0.010)
raise ConnectionError() raise ConnectionError()
sys.modules['requests'].request = request sys.modules['requests'].request = request
self.configuration.evaluate() self.configuration.evaluate()
assert self.configuration.status == 3 self.assertEquals(self.configuration.status, cachet_url_monitor.status.COMPONENT_STATUS_PARTIAL_OUTAGE,
self.mock_logger.warning.assert_called_with(('The URL is ' 'Component status set incorrectly')
'unreachable: GET http://localhost:8080/swagger')) self.mock_logger.warning.assert_called_with('The URL is unreachable: GET http://localhost:8080/swagger')
def test_evaluate_with_http_error(self): def test_evaluate_with_http_error(self):
def request(method, url, timeout=None): def request(method, url, timeout=None):
assert method == 'GET' self.assertEquals(method, 'GET', 'Incorrect HTTP method')
assert url == 'http://localhost:8080/swagger' self.assertEquals(url, 'http://localhost:8080/swagger', 'Monitored URL is incorrect')
assert timeout == 0.010 self.assertEquals(timeout, 0.010)
raise HTTPError() raise HTTPError()
sys.modules['requests'].request = request sys.modules['requests'].request = request
self.configuration.evaluate() self.configuration.evaluate()
assert self.configuration.status == 3 self.assertEquals(self.configuration.status, cachet_url_monitor.status.COMPONENT_STATUS_PARTIAL_OUTAGE,
self.mock_logger.exception.assert_called_with(('Unexpected HTTP ' 'Component status set incorrectly')
'response')) self.mock_logger.exception.assert_called_with('Unexpected HTTP response')
def test_push_status(self): def test_push_status(self):
def put(url, params=None, headers=None): def put(url, params=None, headers=None):
assert url == 'https://demo.cachethq.io/api/v1/components/1' self.assertEquals(url, 'https://demo.cachethq.io/api/v1/components/1', 'Incorrect cachet API URL')
assert params == {'id': 1, 'status': 1} self.assertDictEqual(params, {'id': 1, 'status': 1}, 'Incorrect component update parameters')
assert headers == {'X-Cachet-Token': 'token2'} self.assertDictEqual(headers, {'X-Cachet-Token': 'token2'}, 'Incorrect component update parameters')
response = mock.Mock() response = mock.Mock()
response.status_code = 200 response.status_code = 200
return response return response
sys.modules['requests'].put = put sys.modules['requests'].put = put
self.configuration.status = 1 self.assertEquals(self.configuration.status, cachet_url_monitor.status.COMPONENT_STATUS_OPERATIONAL,
'Incorrect component update parameters')
self.configuration.push_status() self.configuration.push_status()
def test_push_status_with_failure(self): def test_push_status_with_failure(self):
def put(url, params=None, headers=None): def put(url, params=None, headers=None):
assert url == 'https://demo.cachethq.io/api/v1/components/1' self.assertEquals(url, 'https://demo.cachethq.io/api/v1/components/1', 'Incorrect cachet API URL')
assert params == {'id': 1, 'status': 1} self.assertDictEqual(params, {'id': 1, 'status': 1}, 'Incorrect component update parameters')
assert headers == {'X-Cachet-Token': 'token2'} self.assertDictEqual(headers, {'X-Cachet-Token': 'token2'}, 'Incorrect component update parameters')
response = mock.Mock() response = mock.Mock()
response.status_code = 300 response.status_code = 400
return response return response
sys.modules['requests'].put = put sys.modules['requests'].put = put
self.configuration.status = 1 self.assertEquals(self.configuration.status, cachet_url_monitor.status.COMPONENT_STATUS_OPERATIONAL,
'Incorrect component update parameters')
self.configuration.push_status() self.configuration.push_status()