6 Commits

Author SHA1 Message Date
Mitsuo Takaki
1c7fff8d92 Adding twine to dev_requirements 2020-02-11 00:14:40 -08:00
mtakaki
5679bdaa52 83 (#84)
* #83 - Fixing the bug that was preventing the status update

* #83 - Refactoring unit tests for configuration to ensure we catch more cases
2020-02-10 23:51:02 -08:00
Mitsuo Takaki
0a55b1f513 Bumping the version to reduce the risk of overwriting an existing release 2020-01-29 02:05:32 -08:00
Christian Strobel
bd610671fa Added os import (#82) 2020-01-29 02:02:06 -08:00
Mitsuo Takaki
46955addf1 Bumping the version to reduce the risk of overwriting an existing release 2020-01-28 01:43:54 -08:00
mtakaki
df2d094dc6 Fixing push status that has been broken since moving to a client (#80)
* Fixing push status that has been broken since moving to a client

* Adding unit test to cover the bug
2020-01-28 01:42:49 -08:00
12 changed files with 356 additions and 276 deletions

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
from typing import Dict from typing import Dict
from typing import Optional
import click import click
import requests import requests
@@ -81,7 +82,7 @@ class CachetClient(object):
else: else:
raise exceptions.MetricNonexistentError(metric_id) raise exceptions.MetricNonexistentError(metric_id)
def get_component_status(self, component_id): def get_component_status(self, component_id: int) -> Optional[status.ComponentStatus]:
"""Retrieves the current status of the given component. It will fail if the component does """Retrieves the current status of the given component. It will fail if the component does
not exist or doesn't respond with the expected data. not exist or doesn't respond with the expected data.
:return component status. :return component status.
@@ -94,13 +95,13 @@ class CachetClient(object):
else: else:
raise exceptions.ComponentNonexistentError(component_id) raise exceptions.ComponentNonexistentError(component_id)
def push_status(self, component_id, component_status): def push_status(self, component_id: int, component_status: status.ComponentStatus):
"""Pushes the status of the component to the cachet server. """Pushes the status of the component to the cachet server.
""" """
params = {'id': component_id, 'status': component_status} params = {'id': component_id, 'status': component_status.value}
return requests.put(f"{self.url}/components/{component_id}", params=params, headers=self.headers) return requests.put(f"{self.url}/components/{component_id}", params=params, headers=self.headers)
def push_metrics(self, metric_id, latency_time_unit, elapsed_time_in_seconds, timestamp): def push_metrics(self, metric_id: int, latency_time_unit: str, elapsed_time_in_seconds: int, timestamp: int):
"""Pushes the total amount of seconds the request took to get a response from the URL. """Pushes the total amount of seconds the request took to get a response from the URL.
""" """
value = latency_unit.convert_to_unit(latency_time_unit, elapsed_time_in_seconds) value = latency_unit.convert_to_unit(latency_time_unit, elapsed_time_in_seconds)
@@ -122,7 +123,7 @@ class CachetClient(object):
# 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': message, params = {'name': 'URL unavailable', 'message': message,
'status': status.IncidentStatus.INVESTIGATING.value, 'status': status.IncidentStatus.INVESTIGATING.value,
'visible': is_public_incident, 'component_id': component_id, 'component_status': status_value, 'visible': is_public_incident, 'component_id': component_id, 'component_status': status_value.value,
'notify': True} 'notify': True}
return requests.post(f'{self.url}/incidents', params=params, headers=self.headers) return requests.post(f'{self.url}/incidents', params=params, headers=self.headers)

View File

@@ -1,17 +1,16 @@
#!/usr/bin/env python #!/usr/bin/env python
import abc
import copy import copy
import logging import logging
import os
import re
import time import time
from typing import Dict
import requests import requests
from yaml import dump from yaml import dump
import cachet_url_monitor.status as st import cachet_url_monitor.status as st
from cachet_url_monitor.client import CachetClient, normalize_url from cachet_url_monitor.client import CachetClient, normalize_url
from cachet_url_monitor.exceptions import MetricNonexistentError from cachet_url_monitor.exceptions import ConfigurationValidationError
from cachet_url_monitor.expectation import Expectation
from cachet_url_monitor.status import ComponentStatus from cachet_url_monitor.status import ComponentStatus
# 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
@@ -19,27 +18,42 @@ from cachet_url_monitor.status import ComponentStatus
configuration_mandatory_fields = ['url', 'method', 'timeout', 'expectation', 'component_id', 'frequency'] configuration_mandatory_fields = ['url', 'method', 'timeout', 'expectation', 'component_id', 'frequency']
class ConfigurationValidationError(Exception):
"""Exception raised when there's a validation error."""
def __init__(self, value):
self.value = value
def __str__(self):
return repr(self.value)
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.
""" """
endpoint_index: int
endpoint: str
client: CachetClient
token: str
current_fails: int
trigger_update: bool
headers: Dict[str, str]
def __init__(self, config, endpoint_index: int): endpoint_method: str
self.endpoint_index: int = endpoint_index endpoint_url: str
endpoint_timeout: int
endpoint_header: Dict[str, str]
allowed_fails: int
component_id: int
metric_id: int
default_metric_value: int
latency_unit: str
status: ComponentStatus
previous_status: ComponentStatus
message: str
def __init__(self, config, endpoint_index: int, client: CachetClient, token: str):
self.endpoint_index = endpoint_index
self.data = config self.data = config
self.endpoint = self.data['endpoints'][endpoint_index] self.endpoint = self.data['endpoints'][endpoint_index]
self.current_fails: int = 0 self.client = client
self.trigger_update: bool = True self.token = token
self.current_fails = 0
self.trigger_update = True
if 'name' not in self.endpoint: if 'name' not in self.endpoint:
# We have to make this mandatory, otherwise the logs are confusing when there are multiple URLs. # We have to make this mandatory, otherwise the logs are confusing when there are multiple URLs.
@@ -54,7 +68,7 @@ class Configuration(object):
self.validate() self.validate()
# We store the main information from the configuration file, so we don't keep reading from the data dictionary. # We store the main information from the configuration file, so we don't keep reading from the data dictionary.
self.token = os.environ.get('CACHET_TOKEN') or self.data['cachet']['token']
self.headers = {'X-Cachet-Token': self.token} self.headers = {'X-Cachet-Token': self.token}
self.endpoint_method = self.endpoint['method'] self.endpoint_method = self.endpoint['method']
@@ -63,14 +77,11 @@ class Configuration(object):
self.endpoint_header = self.endpoint.get('header') or None self.endpoint_header = self.endpoint.get('header') or None
self.allowed_fails = self.endpoint.get('allowed_fails') or 0 self.allowed_fails = self.endpoint.get('allowed_fails') or 0
self.api_url = os.environ.get('CACHET_API_URL') or self.data['cachet']['api_url']
self.component_id = self.endpoint['component_id'] self.component_id = self.endpoint['component_id']
self.metric_id = self.endpoint.get('metric_id') self.metric_id = self.endpoint.get('metric_id')
self.client = CachetClient(self.api_url, self.token)
if self.metric_id is not None: if self.metric_id is not None:
self.default_metric_value = self.get_default_metric_value(self.metric_id) self.default_metric_value = self.client.get_default_metric_value(self.metric_id)
# The latency_unit configuration is not mandatory and we fallback to seconds, by default. # The latency_unit configuration is not mandatory and we fallback to seconds, by default.
self.latency_unit = self.data['cachet'].get('latency_unit') or 's' self.latency_unit = self.data['cachet'].get('latency_unit') or 's'
@@ -88,15 +99,6 @@ class Configuration(object):
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_default_metric_value(self, metric_id):
"""Returns default value for configured metric."""
get_metric_request = requests.get('%s/metrics/%s' % (self.api_url, metric_id), headers=self.headers)
if get_metric_request.ok:
return get_metric_request.json()['data']['default_value']
else:
raise MetricNonexistentError(metric_id)
def get_action(self): def get_action(self):
"""Retrieves the action list from the configuration. If it's empty, returns an empty list. """Retrieves the action list from the configuration. If it's empty, returns an empty list.
:return: The list of actions, which can be an empty list. :return: The list of actions, which can be an empty list.
@@ -156,7 +158,7 @@ class Configuration(object):
return return
# We initially assume the API is healthy. # We initially assume the API is healthy.
self.status: ComponentStatus = st.ComponentStatus.OPERATIONAL self.status = st.ComponentStatus.OPERATIONAL
self.message = '' self.message = ''
for expectation in self.expectations: for expectation in self.expectations:
status: ComponentStatus = expectation.get_status(self.request) status: ComponentStatus = expectation.get_status(self.request)
@@ -212,7 +214,6 @@ class Configuration(object):
if self.status == api_component_status: if self.status == api_component_status:
return return
self.status = api_component_status
component_request = self.client.push_status(self.component_id, self.status) component_request = self.client.push_status(self.component_id, self.status)
if component_request.ok: if component_request.ok:
@@ -267,123 +268,3 @@ class Configuration(object):
else: else:
self.logger.warning( self.logger.warning(
f'Incident upload failed with status [{incident_request.status_code}], message: "{self.message}"') f'Incident upload failed with status [{incident_request.status_code}], message: "{self.message}"')
class Expectation(object):
"""Base class for URL result expectations. Any new expectation 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.
"""
# If a need expectation is created, this is where we need to add it.
expectations = {
'HTTP_STATUS': HttpStatus,
'LATENCY': Latency,
'REGEX': Regex
}
if configuration['type'] not in expectations:
raise ConfigurationValidationError(f"Invalid type: {configuration['type']}")
return expectations.get(configuration['type'])(configuration)
def __init__(self, configuration):
self.incident_status = self.parse_incident_status(configuration)
@abc.abstractmethod
def get_status(self, response) -> ComponentStatus:
"""Returns the status of the API, following cachet's component status
documentation: https://docs.cachethq.io/docs/component-statuses
"""
@abc.abstractmethod
def get_message(self, response) -> str:
"""Gets the error message."""
@abc.abstractmethod
def get_default_incident(self):
"""Returns the default status when this incident happens."""
def parse_incident_status(self, configuration) -> ComponentStatus:
return st.INCIDENT_MAP.get(configuration.get('incident', None), self.get_default_incident())
class HttpStatus(Expectation):
def __init__(self, configuration):
self.status_range = HttpStatus.parse_range(configuration['status_range'])
super(HttpStatus, self).__init__(configuration)
@staticmethod
def parse_range(range_string):
if isinstance(range_string, int):
# This happens when there's no range and no dash character, it will be parsed as int already.
return range_string, range_string + 1
statuses = range_string.split("-")
if len(statuses) == 1:
# When there was no range given, we should treat the first number as a single status check.
return int(statuses[0]), int(statuses[0]) + 1
else:
# We shouldn't look into more than one value, as this is a range value.
return int(statuses[0]), int(statuses[1])
def get_status(self, response) -> ComponentStatus:
if self.status_range[0] <= response.status_code < self.status_range[1]:
return st.ComponentStatus.OPERATIONAL
else:
return self.incident_status
def get_default_incident(self):
return st.ComponentStatus.PARTIAL_OUTAGE
def get_message(self, response):
return f'Unexpected HTTP status ({response.status_code})'
def __str__(self):
return repr(f'HTTP status range: [{self.status_range[0]}, {self.status_range[1]}[')
class Latency(Expectation):
def __init__(self, configuration):
self.threshold = configuration['threshold']
super(Latency, self).__init__(configuration)
def get_status(self, response) -> ComponentStatus:
if response.elapsed.total_seconds() <= self.threshold:
return st.ComponentStatus.OPERATIONAL
else:
return self.incident_status
def get_default_incident(self):
return st.ComponentStatus.PERFORMANCE_ISSUES
def get_message(self, response):
return 'Latency above threshold: %.4f seconds' % (response.elapsed.total_seconds(),)
def __str__(self):
return repr('Latency threshold: %.4f seconds' % (self.threshold,))
class Regex(Expectation):
def __init__(self, configuration):
self.regex_string = configuration['regex']
self.regex = re.compile(configuration['regex'], re.UNICODE + re.DOTALL)
super(Regex, self).__init__(configuration)
def get_status(self, response) -> ComponentStatus:
if self.regex.match(response.text):
return st.ComponentStatus.OPERATIONAL
else:
return self.incident_status
def get_default_incident(self):
return st.ComponentStatus.PARTIAL_OUTAGE
def get_message(self, response):
return 'Regex did not match anything in the body'
def __str__(self):
return repr(f'Regex: {self.regex_string}')

View File

@@ -17,3 +17,13 @@ class MetricNonexistentError(Exception):
def __str__(self): def __str__(self):
return repr(f'Metric with id [{self.metric_id}] does not exist.') return repr(f'Metric with id [{self.metric_id}] does not exist.')
class ConfigurationValidationError(Exception):
"""Exception raised when there's a validation error."""
def __init__(self, value):
self.value = value
def __str__(self):
return repr(self.value)

View File

@@ -0,0 +1,127 @@
#!/usr/bin/env python
import abc
import re
import cachet_url_monitor.status as st
from cachet_url_monitor.exceptions import ConfigurationValidationError
from cachet_url_monitor.status import ComponentStatus
class Expectation(object):
"""Base class for URL result expectations. Any new expectation 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.
"""
# If a need expectation is created, this is where we need to add it.
expectations = {
'HTTP_STATUS': HttpStatus,
'LATENCY': Latency,
'REGEX': Regex
}
if configuration['type'] not in expectations:
raise ConfigurationValidationError(f"Invalid type: {configuration['type']}")
return expectations.get(configuration['type'])(configuration)
def __init__(self, configuration):
self.incident_status = self.parse_incident_status(configuration)
@abc.abstractmethod
def get_status(self, response) -> ComponentStatus:
"""Returns the status of the API, following cachet's component status
documentation: https://docs.cachethq.io/docs/component-statuses
"""
@abc.abstractmethod
def get_message(self, response) -> str:
"""Gets the error message."""
@abc.abstractmethod
def get_default_incident(self):
"""Returns the default status when this incident happens."""
def parse_incident_status(self, configuration) -> ComponentStatus:
return st.INCIDENT_MAP.get(configuration.get('incident', None), self.get_default_incident())
class HttpStatus(Expectation):
def __init__(self, configuration):
self.status_range = HttpStatus.parse_range(configuration['status_range'])
super(HttpStatus, self).__init__(configuration)
@staticmethod
def parse_range(range_string):
if isinstance(range_string, int):
# This happens when there's no range and no dash character, it will be parsed as int already.
return range_string, range_string + 1
statuses = range_string.split("-")
if len(statuses) == 1:
# When there was no range given, we should treat the first number as a single status check.
return int(statuses[0]), int(statuses[0]) + 1
else:
# We shouldn't look into more than one value, as this is a range value.
return int(statuses[0]), int(statuses[1])
def get_status(self, response) -> ComponentStatus:
if self.status_range[0] <= response.status_code < self.status_range[1]:
return st.ComponentStatus.OPERATIONAL
else:
return self.incident_status
def get_default_incident(self):
return st.ComponentStatus.PARTIAL_OUTAGE
def get_message(self, response):
return f'Unexpected HTTP status ({response.status_code})'
def __str__(self):
return repr(f'HTTP status range: [{self.status_range[0]}, {self.status_range[1]}[')
class Latency(Expectation):
def __init__(self, configuration):
self.threshold = configuration['threshold']
super(Latency, self).__init__(configuration)
def get_status(self, response) -> ComponentStatus:
if response.elapsed.total_seconds() <= self.threshold:
return st.ComponentStatus.OPERATIONAL
else:
return self.incident_status
def get_default_incident(self):
return st.ComponentStatus.PERFORMANCE_ISSUES
def get_message(self, response):
return 'Latency above threshold: %.4f seconds' % (response.elapsed.total_seconds(),)
def __str__(self):
return repr('Latency threshold: %.4f seconds' % (self.threshold,))
class Regex(Expectation):
def __init__(self, configuration):
self.regex_string = configuration['regex']
self.regex = re.compile(configuration['regex'], re.UNICODE + re.DOTALL)
super(Regex, self).__init__(configuration)
def get_status(self, response) -> ComponentStatus:
if self.regex.match(response.text):
return st.ComponentStatus.OPERATIONAL
else:
return self.incident_status
def get_default_incident(self):
return st.ComponentStatus.PARTIAL_OUTAGE
def get_message(self, response):
return 'Regex did not match anything in the body'
def __str__(self):
return repr(f'Regex: {self.regex_string}')

View File

@@ -1,10 +1,11 @@
#!/usr/bin/env python #!/usr/bin/env python
from typing import Dict
seconds_per_unit = {"ms": 1000, "milliseconds": 1000, "s": 1, "seconds": 1, "m": float(1) / 60, seconds_per_unit: Dict[str, float] = {"ms": 1000, "milliseconds": 1000, "s": 1, "seconds": 1, "m": float(1) / 60,
"minutes": float(1) / 60, "h": float(1) / 3600, "hours": float(1) / 3600} "minutes": float(1) / 60, "h": float(1) / 3600, "hours": float(1) / 3600}
def convert_to_unit(time_unit, value): def convert_to_unit(time_unit: str, value: float):
""" """
Will convert the given value from seconds to the given time_unit. Will convert the given value from seconds to the given time_unit.

View File

@@ -3,10 +3,12 @@ import logging
import sys import sys
import threading import threading
import time import time
import os
import schedule import schedule
from yaml import load, SafeLoader from yaml import load, SafeLoader
from cachet_url_monitor.client import CachetClient
from cachet_url_monitor.configuration import Configuration from cachet_url_monitor.configuration import Configuration
cachet_mandatory_fields = ['api_url', 'token'] cachet_mandatory_fields = ['api_url', 'token']
@@ -105,14 +107,14 @@ def build_agent(configuration, logger):
def validate_config(): def validate_config():
if 'endpoints' not in config_file.keys(): if 'endpoints' not in config_data.keys():
fatal_error('Endpoints is a mandatory field') fatal_error('Endpoints is a mandatory field')
if config_file['endpoints'] is None: if config_data['endpoints'] is None:
fatal_error('Endpoints array can not be empty') fatal_error('Endpoints array can not be empty')
for key in cachet_mandatory_fields: for key in cachet_mandatory_fields:
if key not in config_file['cachet']: if key not in config_data['cachet']:
fatal_error('Missing cachet mandatory fields') fatal_error('Missing cachet mandatory fields')
@@ -132,14 +134,16 @@ if __name__ == "__main__":
sys.exit(1) sys.exit(1)
try: try:
config_file = load(open(sys.argv[1], 'r'), SafeLoader) config_data = load(open(sys.argv[1], 'r'), SafeLoader)
except FileNotFoundError: except FileNotFoundError:
logging.getLogger('cachet_url_monitor.scheduler').fatal(f'File not found: {sys.argv[1]}') logging.getLogger('cachet_url_monitor.scheduler').fatal(f'File not found: {sys.argv[1]}')
sys.exit(1) sys.exit(1)
validate_config() validate_config()
for endpoint_index in range(len(config_file['endpoints'])): for endpoint_index in range(len(config_data['endpoints'])):
configuration = Configuration(config_file, endpoint_index) token = os.environ.get('CACHET_TOKEN') or config_data['cachet']['token']
api_url = os.environ.get('CACHET_API_URL') or config_data['cachet']['api_url']
configuration = Configuration(config_data, endpoint_index, CachetClient(api_url, token), token)
NewThread(Scheduler(configuration, NewThread(Scheduler(configuration,
build_agent(configuration, logging.getLogger('cachet_url_monitor.scheduler')))).start() build_agent(configuration, logging.getLogger('cachet_url_monitor.scheduler')))).start()

View File

@@ -7,6 +7,7 @@ from enum import Enum
class ComponentStatus(Enum): class ComponentStatus(Enum):
UNKNOWN = 0
OPERATIONAL = 1 OPERATIONAL = 1
PERFORMANCE_ISSUES = 2 PERFORMANCE_ISSUES = 2
PARTIAL_OUTAGE = 3 PARTIAL_OUTAGE = 3

View File

@@ -4,6 +4,7 @@ coveralls==1.10.0
ipython==7.11.1 ipython==7.11.1
mock==3.0.5 mock==3.0.5
pudb==2019.2 pudb==2019.2
pytest==5.3.3 pytest==5.3.5
pytest-cov==2.8.1 pytest-cov==2.8.1
requests-mock==1.7.0 requests-mock==1.7.0
twine==3.1.1

View File

@@ -3,7 +3,7 @@
from setuptools import setup, find_packages from setuptools import setup, find_packages
setup(name='cachet-url-monitor', setup(name='cachet-url-monitor',
version='0.6.4', version='0.6.7',
description='Cachet URL monitor plugin', description='Cachet URL monitor plugin',
author='Mitsuo Takaki', author='Mitsuo Takaki',
author_email='mitsuotakaki@gmail.com', author_email='mitsuotakaki@gmail.com',

View File

@@ -145,3 +145,11 @@ class ClientTest(unittest.TestCase):
self.assertEqual(status, ComponentStatus.OPERATIONAL, self.assertEqual(status, ComponentStatus.OPERATIONAL,
'Getting component status value is incorrect.') 'Getting component status value is incorrect.')
@requests_mock.mock()
def test_push_status(self, m):
m.put(f'{CACHET_URL}/components/123?id=123&status={ComponentStatus.PARTIAL_OUTAGE.value}',
headers={'X-Cachet-Token': TOKEN})
response = self.client.push_status(123, ComponentStatus.PARTIAL_OUTAGE)
self.assertTrue(response.ok, 'Pushing status value is failed.')

View File

@@ -1,6 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python
import sys import sys
import unittest
import mock import mock
import pytest import pytest
@@ -8,6 +7,7 @@ import requests
import requests_mock import requests_mock
from yaml import load, SafeLoader from yaml import load, SafeLoader
import cachet_url_monitor.exceptions
import cachet_url_monitor.status import cachet_url_monitor.status
sys.modules['logging'] = mock.Mock() sys.modules['logging'] = mock.Mock()
@@ -15,135 +15,170 @@ from cachet_url_monitor.configuration import Configuration
import os import os
class ConfigurationTest(unittest.TestCase): @pytest.fixture()
@mock.patch.dict(os.environ, {'CACHET_TOKEN': 'token2'}) def mock_client():
def setUp(self): client = mock.Mock()
def getLogger(name): client.get_component_status.return_value = cachet_url_monitor.status.ComponentStatus.OPERATIONAL
self.mock_logger = mock.Mock() yield client
return self.mock_logger
sys.modules['logging'].getLogger = getLogger
# def get(url, headers): @pytest.fixture()
# get_return = mock.Mock() def config_file():
# get_return.ok = True with open(os.path.join(os.path.dirname(__file__), 'configs/config.yml'), 'rt') as yaml_file:
# get_return.json = mock.Mock() config_file_data = load(yaml_file, SafeLoader)
# get_return.json.return_value = {'data': {'status': 1, 'default_value': 0.5}} yield config_file_data
# return get_return
#
# sys.modules['requests'].get = get
self.configuration = Configuration(
load(open(os.path.join(os.path.dirname(__file__), 'configs/config.yml'), 'rt'), SafeLoader), 0)
# sys.modules['requests'].Timeout = Timeout
# sys.modules['requests'].ConnectionError = ConnectionError
# sys.modules['requests'].HTTPError = HTTPError
def test_init(self): @pytest.fixture()
self.assertEqual(len(self.configuration.data), 2, 'Number of root elements in config.yml is incorrect') def multiple_urls_config_file():
self.assertEqual(len(self.configuration.expectations), 3, 'Number of expectations read from file is incorrect') with open(os.path.join(os.path.dirname(__file__), 'configs/config_multiple_urls.yml'), 'rt') as yaml_file:
self.assertDictEqual(self.configuration.headers, {'X-Cachet-Token': 'token2'}, 'Header was not set correctly') config_file_data = load(yaml_file, SafeLoader)
self.assertEqual(self.configuration.api_url, 'https://demo.cachethq.io/api/v1', yield config_file_data
'Cachet API URL was set incorrectly')
self.assertDictEqual(self.configuration.endpoint_header, {'SOME-HEADER': 'SOME-VALUE'}, 'Header is incorrect')
@requests_mock.mock()
def test_evaluate(self, m): @pytest.fixture()
def invalid_config_file():
with open(os.path.join(os.path.dirname(__file__), 'configs/config_invalid_type.yml'), 'rt') as yaml_file:
config_file_data = load(yaml_file, SafeLoader)
yield config_file_data
@pytest.fixture()
def mock_logger():
mock_logger = mock.Mock()
def getLogger(name):
return mock_logger
sys.modules['logging'].getLogger = getLogger
yield mock_logger
@pytest.fixture()
def configuration(config_file, mock_client, mock_logger):
yield Configuration(config_file, 0, mock_client, 'token2')
@pytest.fixture()
def multiple_urls_configuration(multiple_urls_config_file, mock_client, mock_logger):
yield [Configuration(multiple_urls_config_file, index, mock_client, 'token2') for index in
range(len(multiple_urls_config_file['endpoints']))]
def test_init(configuration):
assert len(configuration.data) == 2, 'Number of root elements in config.yml is incorrect'
assert len(configuration.expectations) == 3, 'Number of expectations read from file is incorrect'
assert configuration.headers == {'X-Cachet-Token': 'token2'}, 'Header was not set correctly'
assert configuration.endpoint_header == {'SOME-HEADER': 'SOME-VALUE'}, 'Header is incorrect'
def test_init_unknown_status(config_file, mock_client):
mock_client.get_component_status.return_value = cachet_url_monitor.status.ComponentStatus.UNKNOWN
configuration = Configuration(config_file, 0, mock_client, 'token2')
assert configuration.previous_status == cachet_url_monitor.status.ComponentStatus.UNKNOWN
def test_evaluate(configuration):
with requests_mock.mock() as m:
m.get('http://localhost:8080/swagger', text='<body>') m.get('http://localhost:8080/swagger', text='<body>')
self.configuration.evaluate() configuration.evaluate()
self.assertEqual(self.configuration.status, cachet_url_monitor.status.ComponentStatus.OPERATIONAL, assert configuration.status == cachet_url_monitor.status.ComponentStatus.OPERATIONAL, 'Component status set incorrectly'
'Component status set incorrectly')
@requests_mock.mock()
def test_evaluate_without_header(self, m): def test_evaluate_without_header(configuration):
with requests_mock.mock() as m:
m.get('http://localhost:8080/swagger', text='<body>') m.get('http://localhost:8080/swagger', text='<body>')
self.configuration.evaluate() configuration.evaluate()
self.assertEqual(self.configuration.status, cachet_url_monitor.status.ComponentStatus.OPERATIONAL, assert configuration.status == cachet_url_monitor.status.ComponentStatus.OPERATIONAL, 'Component status set incorrectly'
'Component status set incorrectly')
@requests_mock.mock()
def test_evaluate_with_failure(self, m): def test_evaluate_with_failure(configuration):
with requests_mock.mock() as m:
m.get('http://localhost:8080/swagger', text='<body>', status_code=400) m.get('http://localhost:8080/swagger', text='<body>', status_code=400)
self.configuration.evaluate() configuration.evaluate()
self.assertEqual(self.configuration.status, cachet_url_monitor.status.ComponentStatus.MAJOR_OUTAGE, assert configuration.status == cachet_url_monitor.status.ComponentStatus.MAJOR_OUTAGE, 'Component status set incorrectly or custom incident status is incorrectly parsed'
'Component status set incorrectly or custom incident status is incorrectly parsed')
@requests_mock.mock()
def test_evaluate_with_timeout(self, m): def test_evaluate_with_timeout(configuration, mock_logger):
with requests_mock.mock() as m:
m.get('http://localhost:8080/swagger', exc=requests.Timeout) m.get('http://localhost:8080/swagger', exc=requests.Timeout)
self.configuration.evaluate() configuration.evaluate()
self.assertEqual(self.configuration.status, cachet_url_monitor.status.ComponentStatus.PERFORMANCE_ISSUES, assert configuration.status == cachet_url_monitor.status.ComponentStatus.PERFORMANCE_ISSUES, 'Component status set incorrectly'
'Component status set incorrectly') mock_logger.warning.assert_called_with('Request timed out')
self.mock_logger.warning.assert_called_with('Request timed out')
@requests_mock.mock()
def test_evaluate_with_connection_error(self, m): def test_evaluate_with_connection_error(configuration, mock_logger):
with requests_mock.mock() as m:
m.get('http://localhost:8080/swagger', exc=requests.ConnectionError) m.get('http://localhost:8080/swagger', exc=requests.ConnectionError)
self.configuration.evaluate() configuration.evaluate()
self.assertEqual(self.configuration.status, cachet_url_monitor.status.ComponentStatus.PARTIAL_OUTAGE, assert configuration.status == cachet_url_monitor.status.ComponentStatus.PARTIAL_OUTAGE, 'Component status set incorrectly'
'Component status set incorrectly') mock_logger.warning.assert_called_with('The URL is unreachable: GET http://localhost:8080/swagger')
self.mock_logger.warning.assert_called_with('The URL is unreachable: GET http://localhost:8080/swagger')
@requests_mock.mock()
def test_evaluate_with_http_error(self, m): def test_evaluate_with_http_error(configuration, mock_logger):
with requests_mock.mock() as m:
m.get('http://localhost:8080/swagger', exc=requests.HTTPError) m.get('http://localhost:8080/swagger', exc=requests.HTTPError)
self.configuration.evaluate() configuration.evaluate()
self.assertEqual(self.configuration.status, cachet_url_monitor.status.ComponentStatus.PARTIAL_OUTAGE, assert configuration.status == cachet_url_monitor.status.ComponentStatus.PARTIAL_OUTAGE, 'Component status set incorrectly'
'Component status set incorrectly') mock_logger.exception.assert_called_with('Unexpected HTTP response')
self.mock_logger.exception.assert_called_with('Unexpected HTTP response')
@requests_mock.mock()
def test_push_status(self, m):
m.put('https://demo.cachethq.io/api/v1/components/1?id=1&status=1', headers={'X-Cachet-Token': 'token2'})
self.assertEqual(self.configuration.status, cachet_url_monitor.status.ComponentStatus.OPERATIONAL,
'Incorrect component update parameters')
self.configuration.push_status()
@requests_mock.mock()
def test_push_status_with_failure(self, m):
m.put('https://demo.cachethq.io/api/v1/components/1?id=1&status=1', headers={'X-Cachet-Token': 'token2'},
status_code=400)
self.assertEqual(self.configuration.status, cachet_url_monitor.status.ComponentStatus.OPERATIONAL,
'Incorrect component update parameters')
self.configuration.push_status()
class ConfigurationMultipleUrlTest(unittest.TestCase): def test_push_status(configuration, mock_client):
@mock.patch.dict(os.environ, {'CACHET_TOKEN': 'token2'}) mock_client.get_component_status.return_value = cachet_url_monitor.status.ComponentStatus.PARTIAL_OUTAGE
def setUp(self): push_status_response = mock.Mock()
config_yaml = load(open(os.path.join(os.path.dirname(__file__), 'configs/config_multiple_urls.yml'), 'rt'), mock_client.push_status.return_value = push_status_response
SafeLoader) push_status_response.ok = True
self.configuration = [] configuration.previous_status = cachet_url_monitor.status.ComponentStatus.PARTIAL_OUTAGE
configuration.status = cachet_url_monitor.status.ComponentStatus.OPERATIONAL
for index in range(len(config_yaml['endpoints'])): configuration.push_status()
self.configuration.append(Configuration(config_yaml, index))
def test_init(self): mock_client.push_status.assert_called_once_with(1, cachet_url_monitor.status.ComponentStatus.OPERATIONAL)
expected_method = ['GET', 'POST']
expected_url = ['http://localhost:8080/swagger', 'http://localhost:8080/bar']
for index in range(len(self.configuration)):
config = self.configuration[index]
self.assertEqual(len(config.data), 2, 'Number of root elements in config.yml is incorrect')
self.assertEqual(len(config.expectations), 1, 'Number of expectations read from file is incorrect')
self.assertDictEqual(config.headers, {'X-Cachet-Token': 'token2'}, 'Header was not set correctly')
self.assertEqual(config.api_url, 'https://demo.cachethq.io/api/v1',
'Cachet API URL was set incorrectly')
self.assertEqual(expected_method[index], config.endpoint_method)
self.assertEqual(expected_url[index], config.endpoint_url)
class ConfigurationNegativeTest(unittest.TestCase): def test_push_status_with_new_failure(configuration, mock_client):
@mock.patch.dict(os.environ, {'CACHET_TOKEN': 'token2'}) mock_client.get_component_status.return_value = cachet_url_monitor.status.ComponentStatus.OPERATIONAL
def test_init(self): push_status_response = mock.Mock()
with pytest.raises(cachet_url_monitor.configuration.ConfigurationValidationError): mock_client.push_status.return_value = push_status_response
self.configuration = Configuration( push_status_response.ok = False
load(open(os.path.join(os.path.dirname(__file__), 'configs/config_invalid_type.yml'), 'rt'), configuration.status = cachet_url_monitor.status.ComponentStatus.PARTIAL_OUTAGE
SafeLoader), 0)
configuration.push_status()
mock_client.push_status.assert_called_once_with(1, cachet_url_monitor.status.ComponentStatus.PARTIAL_OUTAGE)
def test_push_status_same_status(configuration, mock_client):
mock_client.get_component_status.return_value = cachet_url_monitor.status.ComponentStatus.OPERATIONAL
configuration.status = cachet_url_monitor.status.ComponentStatus.OPERATIONAL
configuration.push_status()
mock_client.push_status.assert_not_called()
def test_init_multiple_urls(multiple_urls_configuration):
expected_method = ['GET', 'POST']
expected_url = ['http://localhost:8080/swagger', 'http://localhost:8080/bar']
assert len(multiple_urls_configuration) == 2
for index in range(len(multiple_urls_configuration)):
config = multiple_urls_configuration[index]
assert len(config.data) == 2, 'Number of root elements in config.yml is incorrect'
assert len(config.expectations) == 1, 'Number of expectations read from file is incorrect'
assert config.headers == {'X-Cachet-Token': 'token2'}, 'Header was not set correctly'
assert expected_method[index] == config.endpoint_method
assert expected_url[index] == config.endpoint_url
def test_init_invalid_configuration(invalid_config_file, mock_client):
with pytest.raises(cachet_url_monitor.configuration.ConfigurationValidationError):
Configuration(invalid_config_file, 0, mock_client, 'token2')

View File

@@ -5,8 +5,7 @@ import unittest
import mock import mock
import pytest import pytest
from cachet_url_monitor.configuration import HttpStatus, Regex from cachet_url_monitor.expectation import HttpStatus, Regex, Latency
from cachet_url_monitor.configuration import Latency
from cachet_url_monitor.status import ComponentStatus from cachet_url_monitor.status import ComponentStatus
@@ -76,12 +75,24 @@ class HttpStatusTest(unittest.TestCase):
assert self.expectation.get_status(request) == ComponentStatus.OPERATIONAL assert self.expectation.get_status(request) == ComponentStatus.OPERATIONAL
def test_get_status_healthy_boundary(self):
request = mock.Mock()
request.status_code = 299
assert self.expectation.get_status(request) == ComponentStatus.OPERATIONAL
def test_get_status_unhealthy(self): def test_get_status_unhealthy(self):
request = mock.Mock() request = mock.Mock()
request.status_code = 400 request.status_code = 400
assert self.expectation.get_status(request) == ComponentStatus.PARTIAL_OUTAGE assert self.expectation.get_status(request) == ComponentStatus.PARTIAL_OUTAGE
def test_get_status_unhealthy_boundary(self):
request = mock.Mock()
request.status_code = 300
assert self.expectation.get_status(request) == ComponentStatus.PARTIAL_OUTAGE
def test_get_message(self): def test_get_message(self):
request = mock.Mock() request = mock.Mock()
request.status_code = 400 request.status_code = 400