mirror of
https://github.com/mtan93/cachet-url-monitor.git
synced 2026-03-20 13:20:57 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d105f73c4b | ||
|
|
e0418e79fc | ||
|
|
6f70206603 | ||
|
|
d7bf54fa0a | ||
|
|
47ff348071 | ||
|
|
f563526a2a | ||
|
|
a91a1ad0ea | ||
|
|
2a385c93b6 | ||
|
|
bcc2e2cf19 |
@@ -1,6 +1,7 @@
|
||||
FROM python:2-onbuild
|
||||
MAINTAINER Mitsuo Takaki <mitsuotakaki@gmail.com>
|
||||
|
||||
VOLUME /usr/src/app/
|
||||
COPY config.yml /usr/src/app/config/
|
||||
VOLUME /usr/src/app/config/
|
||||
|
||||
ENTRYPOINT ["python", "cachet_url_monitor/scheduler.py", "config.yml"]
|
||||
ENTRYPOINT ["python", "cachet_url_monitor/scheduler.py", "config/config.yml"]
|
||||
|
||||
23
README.md
23
README.md
@@ -1,11 +1,14 @@
|
||||
# Status
|
||||

|
||||
[](https://coveralls.io/github/mtakaki/cachet-url-monitor?branch=master)
|
||||
[](https://www.codacy.com/app/mitsuotakaki/cachet-url-monitor?utm_source=github.com&utm_medium=referral&utm_content=mtakaki/cachet-url-monitor&utm_campaign=Badge_Grade)
|
||||
|
||||
cachet-url-monitor
|
||||
========================
|
||||
Python plugin for [cachet](cachethq.io) that monitors an URL, verifying it's response status and latency. The frequency the URL is tested is configurable, along with the assertion applied to the request response.
|
||||
|
||||
This project is available at PyPI: [https://pypi.python.org/pypi/cachet-url-monitor](https://pypi.python.org/pypi/cachet-url-monitor)
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
@@ -59,4 +62,22 @@ To start the agent:
|
||||
|
||||
```
|
||||
$ python cachet_url_monitor/scheduler.py config.yml
|
||||
```
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
You can run the agent in docker, so you won't need to worry about installing python, virtualenv, or any other dependency into your OS. The `Dockerfile` and `docker-compose.yml` files are already checked in and it's ready to be used.
|
||||
|
||||
To start the agent in a container using docker compose:
|
||||
|
||||
```
|
||||
$ docker-compose build
|
||||
$ docker-compose up
|
||||
```
|
||||
|
||||
Or pulling directly from [dockerhub](https://hub.docker.com/r/mtakaki/cachet-url-monitor/). You will need to create your own custom `config.yml` file and put it in a folder (`my_config`):
|
||||
|
||||
```
|
||||
$ docker pull mtakaki/cachet-url-monitor:0.1
|
||||
$ docker run --rm -it -v my_config/:/usr/src/app/config/ mtakaki/cachet-url-monitor:0.1
|
||||
```
|
||||
|
||||
@@ -3,25 +3,69 @@ import abc
|
||||
import logging
|
||||
import re
|
||||
import requests
|
||||
import timeit
|
||||
import time
|
||||
from yaml import load
|
||||
|
||||
|
||||
# This is the mandatory fields that must be in the configuration file in this
|
||||
# same exact structure.
|
||||
configuration_mandatory_fields = {
|
||||
'endpoint': ['url', 'method', 'timeout', 'expectation'],
|
||||
'cachet': ['api_url', 'token', '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):
|
||||
"""Represents a configuration file, but it also includes the functionality
|
||||
of assessing the API and pushing the results to cachet.
|
||||
"""
|
||||
def __init__(self, config_file):
|
||||
#TODO(mtakaki|2016-04-26): Needs validation if the config is correct.
|
||||
#TODO(mtakaki|2016-04-28): Accept overriding settings using environment
|
||||
#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.config_file = config_file
|
||||
self.data = load(file(self.config_file, 'r'))
|
||||
|
||||
self.validate()
|
||||
|
||||
self.logger.info('Monitoring URL: %s %s' %
|
||||
(self.data['endpoint']['method'], self.data['endpoint']['url']))
|
||||
self.expectations = [Expectaction.create(expectation) for expectation
|
||||
in self.data['endpoint']['expectation']]
|
||||
for expectation in self.expectations:
|
||||
self.logger.info('Registered expectation: %s' % (expectation,))
|
||||
self.headers = {'X-Cachet-Token': self.data['cachet']['token']}
|
||||
|
||||
def validate(self):
|
||||
configuration_errors = []
|
||||
for key, sub_entries in configuration_mandatory_fields.iteritems():
|
||||
if key not in self.data:
|
||||
configuration_errors.append(key)
|
||||
|
||||
for sub_key in sub_entries:
|
||||
if sub_key not in self.data[key]:
|
||||
configuration_errors.append('%s.%s' % (key, sub_key))
|
||||
|
||||
if ('endpoint' in self.data and 'expectation' in
|
||||
self.data['endpoint']):
|
||||
if (not isinstance(self.data['endpoint']['expectation'], list) or
|
||||
(isinstance(self.data['endpoint']['expectation'], list) and
|
||||
len(self.data['endpoint']['expectation']) == 0)):
|
||||
configuration_errors.append('endpoint.expectation')
|
||||
|
||||
if len(configuration_errors) > 0:
|
||||
raise ConfigurationValidationError(('Config file [%s] failed '
|
||||
'validation. Missing keys: %s') % (self.config_file,
|
||||
', '.join(configuration_errors)))
|
||||
|
||||
def evaluate(self):
|
||||
"""Sends the request to the URL set in the configuration and executes
|
||||
@@ -32,6 +76,7 @@ class Configuration(object):
|
||||
self.request = requests.request(self.data['endpoint']['method'],
|
||||
self.data['endpoint']['url'],
|
||||
timeout=self.data['endpoint']['timeout'])
|
||||
self.current_timestamp = int(time.time())
|
||||
except requests.ConnectionError:
|
||||
self.logger.warning('The URL is unreachable: %s %s' %
|
||||
(self.data['endpoint']['method'],
|
||||
@@ -56,15 +101,14 @@ class Configuration(object):
|
||||
if status > self.status:
|
||||
self.status = status
|
||||
|
||||
def push_status_and_metrics(self):
|
||||
def push_status(self):
|
||||
params = {'id': self.data['cachet']['component_id'], 'status':
|
||||
self.status}
|
||||
headers = {'X-Cachet-Token': self.data['cachet']['token']}
|
||||
component_request = requests.put('%s/components/%d' %
|
||||
(self.data['cachet']['api_url'],
|
||||
self.data['cachet']['component_id']),
|
||||
params=params, headers=headers)
|
||||
if component_request.status_code == 200:
|
||||
params=params, headers=self.headers)
|
||||
if component_request.ok:
|
||||
# Successful update
|
||||
self.logger.info('Component update: status [%d]' % (self.status,))
|
||||
else:
|
||||
@@ -72,6 +116,25 @@ class Configuration(object):
|
||||
self.logger.warning('Component update failed with status [%d]: API'
|
||||
' status: [%d]' % (component_request.status_code, self.status))
|
||||
|
||||
def push_metrics(self):
|
||||
if 'metric_id' in self.data['cachet'] and hasattr(self, 'request'):
|
||||
params = {'id': self.data['cachet']['metric_id'], 'value':
|
||||
self.request.elapsed.total_seconds(), 'timestamp':
|
||||
self.current_timestamp}
|
||||
metrics_request = requests.post('%s/metrics/%d/points' %
|
||||
(self.data['cachet']['api_url'],
|
||||
self.data['cachet']['metric_id']), params=params,
|
||||
headers=self.headers)
|
||||
|
||||
if metrics_request.ok:
|
||||
# Successful metrics upload
|
||||
self.logger.info('Metric uploaded: %.6f seconds' %
|
||||
(self.request.elapsed.total_seconds(),))
|
||||
else:
|
||||
self.logger.warning('Metric upload failed with status [%d]' %
|
||||
(metrics_request.status_code,))
|
||||
|
||||
|
||||
class Expectaction(object):
|
||||
"""Base class for URL result expectations. Any new excpectation should extend
|
||||
this class and the name added to create() method.
|
||||
@@ -112,6 +175,9 @@ class HttpStatus(Expectaction):
|
||||
def get_message(self, response):
|
||||
return 'Unexpected HTTP status (%s)' % (response.status_code,)
|
||||
|
||||
def __str__(self):
|
||||
return repr('HTTP status: %s' % (self.status,))
|
||||
|
||||
|
||||
class Latency(Expectaction):
|
||||
def __init__(self, configuration):
|
||||
@@ -126,9 +192,13 @@ class Latency(Expectaction):
|
||||
def get_message(self, response):
|
||||
return 'Latency above threshold: %.4f' % (response.elapsed.total_seconds(),)
|
||||
|
||||
def __str__(self):
|
||||
return repr('Latency threshold: %.4f' % (self.threshold,))
|
||||
|
||||
|
||||
class Regex(Expectaction):
|
||||
def __init__(self, configuration):
|
||||
self.regex_string = configuration['regex']
|
||||
self.regex = re.compile(configuration['regex'])
|
||||
|
||||
def get_status(self, response):
|
||||
@@ -139,3 +209,6 @@ class Regex(Expectaction):
|
||||
|
||||
def get_message(self, response):
|
||||
return 'Regex did not match anything in the body'
|
||||
|
||||
def __str__(self):
|
||||
return repr('Regex: %s' % (self.regex_string,))
|
||||
|
||||
@@ -18,7 +18,8 @@ class Agent(object):
|
||||
cachet server.
|
||||
"""
|
||||
self.configuration.evaluate()
|
||||
self.configuration.push_status_and_metrics()
|
||||
self.configuration.push_status()
|
||||
self.configuration.push_metrics()
|
||||
|
||||
def start(self):
|
||||
"""Sets up the schedule based on the configuration file."""
|
||||
|
||||
@@ -8,10 +8,10 @@ endpoint:
|
||||
- type: LATENCY
|
||||
threshold: 1
|
||||
- type: REGEX
|
||||
regex: ".*<body>.*"
|
||||
regex: '.*<body>.*'
|
||||
cachet:
|
||||
api_url: http://status.cachethq.io/api/v1/
|
||||
api_url: https://demo.cachethq.io/api/v1
|
||||
token: my_token
|
||||
component_id: 1
|
||||
metric_id: 1
|
||||
#metric_id: 1
|
||||
frequency: 30
|
||||
|
||||
2
setup.py
2
setup.py
@@ -2,7 +2,7 @@
|
||||
from distutils.core import setup
|
||||
|
||||
setup(name='cachet-url-monitor',
|
||||
version='0.1',
|
||||
version='0.2',
|
||||
description='Cachet URL monitor plugin',
|
||||
author='Mitsuo Takaki',
|
||||
author_email='mitsuotakaki@gmail.com',
|
||||
|
||||
@@ -100,9 +100,9 @@ class ConfigurationTest(unittest.TestCase):
|
||||
self.mock_logger.exception.assert_called_with(('Unexpected HTTP '
|
||||
'response'))
|
||||
|
||||
def test_push_status_and_metrics(self):
|
||||
def test_push_status(self):
|
||||
def put(url, params=None, headers=None):
|
||||
assert url == 'http://status.cachethq.io/api/v1//components/1'
|
||||
assert url == 'https://demo.cachethq.io/api/v1/components/1'
|
||||
assert params == {'id': 1, 'status': 1}
|
||||
assert headers == {'X-Cachet-Token': 'my_token'}
|
||||
|
||||
@@ -112,11 +112,11 @@ class ConfigurationTest(unittest.TestCase):
|
||||
|
||||
sys.modules['requests'].put = put
|
||||
self.configuration.status = 1
|
||||
self.configuration.push_status_and_metrics()
|
||||
self.configuration.push_status()
|
||||
|
||||
def test_push_status_and_metrics_with_failure(self):
|
||||
def test_push_status_with_failure(self):
|
||||
def put(url, params=None, headers=None):
|
||||
assert url == 'http://status.cachethq.io/api/v1//components/1'
|
||||
assert url == 'https://demo.cachethq.io/api/v1/components/1'
|
||||
assert params == {'id': 1, 'status': 1}
|
||||
assert headers == {'X-Cachet-Token': 'my_token'}
|
||||
|
||||
@@ -126,4 +126,4 @@ class ConfigurationTest(unittest.TestCase):
|
||||
|
||||
sys.modules['requests'].put = put
|
||||
self.configuration.status = 1
|
||||
self.configuration.push_status_and_metrics()
|
||||
self.configuration.push_status()
|
||||
|
||||
@@ -17,11 +17,11 @@ class AgentTest(unittest.TestCase):
|
||||
|
||||
def test_execute(self):
|
||||
evaluate = self.configuration.evaluate
|
||||
push_status_and_metrics = self.configuration.push_status_and_metrics
|
||||
push_status = self.configuration.push_status
|
||||
self.agent.execute()
|
||||
|
||||
evaluate.assert_called_once()
|
||||
push_status_and_metrics.assert_called_once()
|
||||
push_status.assert_called_once()
|
||||
|
||||
def test_start(self):
|
||||
every = sys.modules['schedule'].every
|
||||
|
||||
Reference in New Issue
Block a user