Improve logging such that a test can be written for an alert

This commit is contained in:
IamTheFij 2018-04-10 11:06:42 -07:00
parent 6597ef50d8
commit fdc13d437d
5 changed files with 92 additions and 9 deletions

View File

@ -1,15 +1,19 @@
import logging import logging
import subprocess
import sys import sys
from argparse import ArgumentParser from argparse import ArgumentParser
from datetime import datetime from datetime import datetime
from subprocess import call from subprocess import CalledProcessError
from subprocess import check_call from subprocess import check_output
from time import sleep from time import sleep
import yamlenv import yamlenv
logging.basicConfig(level=logging.INFO) logging.basicConfig(
level=logging.INFO,
format='%(asctime)s %(levelname)s %(name)s %(message)s'
)
logging.getLogger(__name__).addHandler(logging.NullHandler()) logging.getLogger(__name__).addHandler(logging.NullHandler())
@ -64,6 +68,27 @@ def validate_monitor_settings(settings):
) )
def maybe_decode(bstr, encoding='utf-8'):
try:
return bstr.decode(encoding)
except TypeError:
return bstr
def call_output(*popenargs, **kwargs):
"""Similar to check_output, but instead returns output and exception"""
# So we can capture complete output, redirect sderr to stdout
kwargs.setdefault('stderr', subprocess.STDOUT)
output, ex = None, None
try:
output = check_output(*popenargs, **kwargs)
except CalledProcessError as e:
output, ex = e.output, e
output = output.rstrip(b'\n')
return output, ex
class InvalidAlertException(Exception): class InvalidAlertException(Exception):
pass pass
@ -101,6 +126,10 @@ class Monitor(object):
self.total_failure_count = 0 self.total_failure_count = 0
self.alert_count = 0 self.alert_count = 0
self.logger = logging.getLogger(
'{}({})'.format(self.__class__.__name__, self.name)
)
def should_check(self): def should_check(self):
"""Determines if this Monitor should run it's check command""" """Determines if this Monitor should run it's check command"""
if not self.last_check: if not self.last_check:
@ -115,9 +144,15 @@ class Monitor(object):
""" """
if not self.should_check(): if not self.should_check():
return None return None
result = call(self.command, shell=isinstance(self.command, str))
output, ex = call_output(
self.command,
shell=isinstance(self.command, str),
)
self.logger.debug(output)
self.last_check = datetime.now() self.last_check = datetime.now()
if result == 0:
if ex is None:
self.success() self.success()
return True return True
else: else:
@ -158,6 +193,10 @@ class Alert(object):
if not self.command: if not self.command:
raise InvalidAlertException('Invalid alert {}'.format(self.name)) raise InvalidAlertException('Invalid alert {}'.format(self.name))
self.logger = logging.getLogger(
'{}({})'.format(self.__class__.__name__, self.name)
)
def _formated_command(self, **kwargs): def _formated_command(self, **kwargs):
"""Formats command array or string with kwargs from Monitor""" """Formats command array or string with kwargs from Monitor"""
if isinstance(self.command, str): if isinstance(self.command, str):
@ -169,10 +208,13 @@ class Alert(object):
def alert(self, monitor): def alert(self, monitor):
"""Calls the alert command for the provided monitor""" """Calls the alert command for the provided monitor"""
check_call( output, ex = call_output(
self._formated_command(monitor_name=monitor.name), self._formated_command(monitor_name=monitor.name),
shell=isinstance(self.command, str), shell=isinstance(self.command, str),
) )
self.logger.error(maybe_decode(output))
if ex is not None:
raise ex
class Minitor(object): class Minitor(object):
@ -182,7 +224,7 @@ class Minitor(object):
check_interval = None check_interval = None
def __init__(self): def __init__(self):
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(self.__class__.__name__)
def setup(self, config_path): def setup(self, config_path):
"""Load all setup from YAML file at provided path""" """Load all setup from YAML file at provided path"""

View File

@ -1,7 +1,6 @@
-r requirements.txt -r requirements.txt
coverage coverage
flake8 flake8
mock
pre-commit pre-commit
pytest pytest
tox tox

28
tests/alert_test.py Normal file
View File

@ -0,0 +1,28 @@
from unittest.mock import patch
import pytest
from minitor.main import Alert
from minitor.main import Monitor
class TestAlert(object):
@pytest.fixture
def monitor(self):
return Monitor({
'name': 'Dummy Monitor',
'command': ['echo', 'foo'],
})
@pytest.fixture
def echo_alert(self):
return Alert(
'log',
{'command': ['echo', '{monitor_name} has failed!']}
)
def test_simple_alert(self, monitor, echo_alert):
with patch.object(echo_alert.logger, 'error') as mock_error:
echo_alert.alert(monitor)
mock_error.assert_called_once_with('Dummy Monitor has failed!')

14
tests/minitor_test.py Normal file
View File

@ -0,0 +1,14 @@
from minitor.main import call_output
class TestMinitor(object):
def test_call_output(self):
# valid command should have result and no exception
output, ex = call_output(['echo', 'test'])
assert output == b'test'
assert ex is None
output, ex = call_output(['ls', '--not-a-real-flag'])
assert output.startswith(b'ls: illegal option')
assert ex is not None

View File

@ -11,7 +11,7 @@ class TestMonitor(object):
@pytest.fixture @pytest.fixture
def monitor(self): def monitor(self):
return Monitor({ return Monitor({
'name': 'SampleMonitor', 'name': 'Sample Monitor',
'command': ['echo', 'foo'], 'command': ['echo', 'foo'],
}) })