Merge pull request #8 from mtakaki/mtakaki_1_allow_overriding_through_env_var

Adding support to overriding some of the configuration through environment variables.
This commit is contained in:
mtakaki
2016-05-22 11:55:51 -07:00
2 changed files with 97 additions and 85 deletions
+56 -56
View File
@@ -1,10 +1,12 @@
#!/usr/bin/env python #!/usr/bin/env python
import abc import abc
import cachet_url_monitor.status
import logging import logging
import time
import cachet_url_monitor.status
import os
import re import re
import requests import requests
import time
from yaml import load from yaml import load
# This is the mandatory fields that must be in the configuration file in this # This is the mandatory fields that must be in the configuration file in this
@@ -35,41 +37,52 @@ 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.
""" """
def __init__(self, config_file): def __init__(self, config_file):
# TODO(mtakaki#1|2016-04-28): Accept overriding settings using environment
# variables so we have a more docker-friendly approach.
self.logger = logging.getLogger('cachet_url_monitor.configuration.Configuration') self.logger = logging.getLogger('cachet_url_monitor.configuration.Configuration')
self.config_file = config_file self.config_file = config_file
self.data = load(file(self.config_file, 'r')) self.data = load(file(self.config_file, 'r'))
# 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()
self.headers = {'X-Cachet-Token': self.data['cachet']['token']}
self.status = self.get_current_status(self.data['cachet']['component_id'])
self.logger.info('Monitoring URL: %s %s' % # We store the main information from the configuration file, so we don't keep reading from the data dictionary.
(self.data['endpoint']['method'], self.data['endpoint']['url'])) self.headers = {'X-Cachet-Token': os.environ.get('CACHET_TOKEN') or self.data['cachet']['token']}
self.expectations = [Expectaction.create(expectation) for expectation
in self.data['endpoint']['expectation']] 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_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.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')
# 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.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, component_id):
get_status_request = requests.get(
'%s/components/%d' % (self.data['cachet']['api_url'], self.data['cachet']['component_id']),
headers=self.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.
@@ -107,18 +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.data['endpoint']['method'], self.request = requests.request(self.endpoint_method, self.endpoint_url, timeout=self.endpoint_timeout)
self.data['endpoint']['url'],
timeout=self.data['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
@@ -148,12 +154,9 @@ class Configuration(object):
"""Pushes the status of the component to the cachet server. It will update the component """Pushes the status of the component to the cachet server. It will update the component
status based on the previous call to evaluate(). status based on the previous call to evaluate().
""" """
params = {'id': self.data['cachet']['component_id'], 'status': params = {'id': self.component_id, 'status': self.status}
self.status} component_request = requests.put('%s/components/%d' % (self.api_url, self.component_id), params=params,
component_request = requests.put('%s/components/%d' % headers=self.headers)
(self.data['cachet']['api_url'],
self.data['cachet']['component_id']),
params=params, headers=self.headers)
if component_request.ok: if component_request.ok:
# Successful update # Successful update
self.logger.info('Component update: status [%d]' % (self.status,)) self.logger.info('Component update: status [%d]' % (self.status,))
@@ -167,12 +170,9 @@ class Configuration(object):
It only will send a request if the metric id was set in the configuration. It only will send a request if the metric id was set in the configuration.
""" """
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.data['cachet']['metric_id'], 'value': params = {'id': self.metric_id, 'value': self.request.elapsed.total_seconds(),
self.request.elapsed.total_seconds(), 'timestamp': 'timestamp': self.current_timestamp}
self.current_timestamp} metrics_request = requests.post('%s/metrics/%d/points' % (self.api_url, self.metric_id), params=params,
metrics_request = requests.post('%s/metrics/%d/points' %
(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']
+41 -29
View File
@@ -9,6 +9,7 @@ from requests import ConnectionError, HTTPError, Timeout
sys.modules['requests'] = mock.Mock() sys.modules['requests'] = mock.Mock()
sys.modules['logging'] = mock.Mock() sys.modules['logging'] = mock.Mock()
from cachet_url_monitor.configuration import Configuration from cachet_url_monitor.configuration import Configuration
from test.test_support import EnvironmentVarGuard
class ConfigurationTest(unittest.TestCase): class ConfigurationTest(unittest.TestCase):
@@ -28,14 +29,20 @@ class ConfigurationTest(unittest.TestCase):
sys.modules['requests'].get = get sys.modules['requests'].get = get
self.env = EnvironmentVarGuard()
self.env.set('CACHET_TOKEN', 'token2')
self.configuration = Configuration('config.yml') self.configuration = Configuration('config.yml')
sys.modules['requests'].Timeout = Timeout sys.modules['requests'].Timeout = Timeout
sys.modules['requests'].ConnectionError = ConnectionError sys.modules['requests'].ConnectionError = ConnectionError
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')
self.assertDictEqual(self.configuration.headers, {'X-Cachet-Token': 'token2'}, 'Header was not set correctly')
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():
@@ -52,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():
@@ -70,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': 'my_token'} 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': 'my_token'} 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()