Compare commits

...

20 Commits

Author SHA1 Message Date
6408ca1e42 Remove script
Some checks reported errors
continuous-integration/drone/push Build is passing
continuous-integration/drone Build was killed
2022-04-07 14:09:16 -07:00
a6b05582fd Update curl version
Some checks failed
continuous-integration/drone/push Build is failing
2022-04-07 13:33:04 -07:00
265f9f8372 Remove pypy tests due to missing rust in Docker
Some checks failed
continuous-integration/drone/push Build is failing
2022-04-07 10:14:02 -07:00
d27bcf85dd run tests on pypy3.9
Some checks failed
continuous-integration/drone/push Build is failing
2022-04-05 21:49:25 -07:00
e41d82f9d2 Blacken
Some checks failed
continuous-integration/drone/push Build is failing
2022-04-04 20:23:15 -07:00
094c910cd4 Update supported tested python versions
Some checks failed
continuous-integration/drone/push Build is failing
2022-04-04 20:17:06 -07:00
d5d2be870a Bump bash and curl versions
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-02 09:02:45 -06:00
701ad0be1b Mention minitor-go
Some checks failed
continuous-integration/drone/push Build is failing
2021-06-01 18:38:55 -06:00
8e252f3bcb Slightly reduce image size by removing pip cache
All checks were successful
continuous-integration/drone/push Build is passing
2020-01-30 10:28:30 -08:00
1852e8c439 Revert both commits that remove py-tests
All checks were successful
continuous-integration/drone/push Build is passing
Revert "Remove test dependency too"

This reverts commit db12bb5db1.

Revert "Remove py-tests and pypi to speed up docker validation"

This reverts commit 9aa77b3739.
2020-01-10 16:08:49 -08:00
db12bb5db1 Remove test dependency too
Some checks reported errors
continuous-integration/drone/push Build was killed
2020-01-10 16:07:19 -08:00
8992ac1d33 Add qemu binary download 2020-01-10 16:06:35 -08:00
9aa77b3739 Remove py-tests and pypi to speed up docker validation 2020-01-10 16:06:13 -08:00
67c02a3e6f Reduce piplines in build to reduce parallelism and notifications 2020-01-10 16:01:48 -08:00
c9eaaaa45c Split tox step out of test pipelines 2020-01-10 15:57:08 -08:00
1251532ca6 Fix duplicate step names
Some checks failed
continuous-integration/drone/push Build is failing
2020-01-10 15:37:02 -08:00
874d4ab0aa Again move building docker latest on push to master
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2020-01-10 14:55:13 -08:00
ad4a3770e7 Update build pipeline to notify on docker fail/success
All checks were successful
continuous-integration/drone/push Build is passing
2020-01-10 14:43:05 -08:00
64e6542a93 Bump version of curl
All checks were successful
continuous-integration/drone/push Build is passing
2020-01-10 14:00:18 -08:00
0208858e5e Get back to building latest docker image on pushes to master
Some checks failed
continuous-integration/drone/push Build is failing
2020-01-10 21:35:54 +00:00
9 changed files with 289 additions and 326 deletions

View File

@ -3,17 +3,13 @@ def main(ctx):
pipelines = [] pipelines = []
# Run tests # Run tests
test_pipelines = build_test_pipelines() pipelines += run_tests()
pipelines += test_pipelines
# Wait for all tests to complete
pipelines.append(wait_for_all_tests(test_pipelines))
# Add pypi push pipeline # Add pypi push pipeline
pipelines.append(push_to_pypi(ctx)) pipelines += push_to_pypi(ctx)
# Add docker push pipelines # Add docker push pipelines
pipelines += docker_pipelines() pipelines += push_to_docker(ctx)
return pipelines return pipelines
@ -27,53 +23,37 @@ def get_workspace():
# Builds a list of all test pipelines to be executed # Builds a list of all test pipelines to be executed
def build_test_pipelines(): def run_tests():
return [ return [{
test("python:3.5"),
test("python:3.6"),
test("python:3.7"),
test("python:3.8"),
test("python:3"),
test("pypy:3.6", "pypy3", "pypy3"),
test("pypy:3", "pypy3", "pypy3"),
]
# Waits for the completion of all test pipelines
def wait_for_all_tests(test_pipelines):
depends_on = []
for pipeline in test_pipelines:
depends_on.append(pipeline["name"])
return {
"kind": "pipeline", "kind": "pipeline",
"name": "py-tests", "name": "tests",
"steps": [],
"depends_on": depends_on,
}
# Builds a single test pipeline
def test(docker_tag, python_cmd="python", tox_env="py3"):
return {
"kind": "pipeline",
"name": "test-{}".format(docker_tag.replace(":", "")),
"workspace": get_workspace(), "workspace": get_workspace(),
"steps": [ "steps": [
{ tox_step("python:3.7"),
"name": "test", tox_step("python:3.8"),
"image": docker_tag, tox_step("python:3.9"),
"environment": { tox_step("python:3.10"),
"TOXENV": tox_env, tox_step("python:3"),
}, # tox_step("pypy:3.9", "pypy3", "pypy3"),
"commands": [ # tox_step("pypy:3", "pypy3", "pypy3"),
"{} -V".format(python_cmd), notify_step(),
"pip install tox", ],
"tox", }]
],
},
notify_step() # Builds a single python test step
] def tox_step(docker_tag, python_cmd="python", tox_env="py3"):
return {
"name": "test {}".format(docker_tag.replace(":", "")),
"image": docker_tag,
"environment": {
"TOXENV": tox_env,
},
"commands": [
"{} -V".format(python_cmd),
"pip install tox",
"tox",
],
} }
@ -105,9 +85,11 @@ def notify_step():
# Push package to pypi # Push package to pypi
def push_to_pypi(ctx): def push_to_pypi(ctx):
return { return [{
"kind": "pipeline", "kind": "pipeline",
"name": "deploy-pypi", "name": "deploy to pypi",
"depends_on": ["tests"],
"workspace": get_workspace(),
"trigger": { "trigger": {
"event": ["tag"], "event": ["tag"],
"ref": [ "ref": [
@ -115,8 +97,6 @@ def push_to_pypi(ctx):
"refs/tags/v*", "refs/tags/v*",
], ],
}, },
"depends_on": ["py-tests"],
"workspace": get_workspace(),
"steps": [ "steps": [
{ {
"name": "push to test pypi", "name": "push to test pypi",
@ -149,93 +129,73 @@ def push_to_pypi(ctx):
}, },
notify_step(), notify_step(),
] ]
}]
# Build and push docker image
def push_docker_step(tag_suffix, arch, repo):
return {
"name": "push {}".format(tag_suffix),
"image": "plugins/docker",
"settings": {
"repo": "iamthefij/minitor",
"auto_tag": True,
"auto_tag_suffix": tag_suffix,
"username": {
"from_secret": "docker_username",
},
"password": {
"from_secret": "docker_password",
},
"build_args": [
"ARCH={}".format(arch),
"REPO={}".format(repo),
],
},
} }
# Deploys image to docker hub # Builds a pipeline to push to docker
def push_docker(tag_suffix, arch, repo): def push_to_docker(ctx):
return { return [{
"kind": "pipeline", "kind": "pipeline",
"name": "deploy-docker-{}".format(tag_suffix), "name": "push to docker",
"depends_on": ["tests"],
"workspace": get_workspace(),
"trigger": { "trigger": {
"event": ["tag"], "event": ["tag", "push"],
"ref": [ "ref": [
"refs/heads/master", "refs/heads/master",
"refs/tags/v*", "refs/tags/v*",
], ],
}, },
"workspace": get_workspace(),
"steps": [ "steps": [
{ {
"name": "get qemu", "name": "get qemu",
"image": "busybox", "image": "busybox",
"commands": ["sh ./get_qemu.sh {}".format(arch)], "commands": ["sh ./get_qemu.sh x86_64 arm aarch64"],
}, },
push_docker_step("linux-amd64", "x86_64", "library"),
push_docker_step("linux-arm", "arm", "arm32v6"),
push_docker_step("linux-arm64", "aarch64", "arm64v8"),
{ {
"name": "build", "name": "publish manifest",
"image": "plugins/docker", "image": "plugins/manifest",
"settings": { "settings": {
"repo": "iamthefij/minitor", "spec": "manifest.tmpl",
"auto_tag": True, "auto_tag": True,
"auto_tag_suffix": tag_suffix, "ignore_missing": True,
"username": { "username": {
"from_secret": "docker_username", "from_secret": "docker_username",
}, },
"password": { "password": {
"from_secret": "docker_password", "from_secret": "docker_password",
}, },
"build_args": [ }
"ARCH={}".format(arch),
"REPO={}".format(repo),
],
},
}, },
notify_step(),
], ],
} }]
# generate all docker pipelines to push images and manifest # vim: ft=python
def docker_pipelines():
# build list of images to push
docker_pipelines = [
push_docker("linux-amd64", "x86_64", "library"),
push_docker("linux-arm", "arm", "arm32v6"),
push_docker("linux-arm64", "aarch64", "arm64v8"),
]
# build list of dependencies
pipeline_names = []
for pipeline in docker_pipelines:
pipeline_names.append(pipeline["name"])
# append manifest pipeline
docker_pipelines.append({
"kind": "pipeline",
"name": "deploy-docker-manifest",
"trigger": {
"event": ["tag"],
"ref": [
"refs/heads/master",
"refs/tags/v*",
],
},
"workspace": get_workspace(),
"depends_on": pipeline_names,
"steps": [{
"name": "publish manifest",
"image": "plugins/manifest",
"settings": {
"spec": "manifest.tmpl",
"auto_tag": True,
"ignore_missing": True,
"username": {
"from_secret": "docker_username",
},
"password": {
"from_secret": "docker_password",
},
}
}],
})
return docker_pipelines

View File

@ -1,17 +1,15 @@
repos: repos:
- repo: https://github.com/psf/black
rev: 22.3.0
hooks:
- id: black
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v1.2.3 rev: v1.2.3
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
- id: end-of-file-fixer - id: end-of-file-fixer
- id: autopep8-wrapper
args:
- -i
- --ignore=E265,E309,E501
- id: debug-statements - id: debug-statements
language_version: python3 language_version: python3
- id: flake8
language_version: python3
- id: check-yaml - id: check-yaml
args: args:
- --allow-multiple-documents - --allow-multiple-documents

View File

@ -8,7 +8,7 @@ ARG ARCH=x86_64
COPY ./build/qemu-${ARCH}-static /usr/bin/ COPY ./build/qemu-${ARCH}-static /usr/bin/
# Add common checking tools # Add common checking tools
RUN apk --no-cache add bash=~5.0 curl=~7.66 jq=~1.6 RUN apk --no-cache add bash=~5.1 curl=~7.80 jq=~1.6
WORKDIR /app WORKDIR /app
# Add minitor user for running as non-root # Add minitor user for running as non-root
@ -24,7 +24,7 @@ COPY ./sample-config.yml /app/config.yml
COPY ./README.md /app/ COPY ./README.md /app/
COPY ./setup.py /app/ COPY ./setup.py /app/
COPY ./minitor /app/minitor COPY ./minitor /app/minitor
RUN pip install -e . RUN pip install --no-cache-dir -e .
# Copy scripts # Copy scripts
COPY ./scripts /app/scripts COPY ./scripts /app/scripts

View File

@ -2,6 +2,10 @@
A minimal monitoring system A minimal monitoring system
## Important
*This has been more or less replaced by a version written in Go. Check out [minitor-go](/iamthefij/minitor-go)*. There are no known issues with this version, but it is not really maintained anymore as I've migrated to the Go version since it uses fewer system resources.
## What does it do? ## What does it do?
Minitor accepts a YAML configuration file with a set of commands to run and a set of alerts to execute when those commands fail. It is designed to be as simple as possible and relies on other command line tools to do checks and issue alerts. Minitor accepts a YAML configuration file with a set of commands to run and a set of alerts to execute when those commands fail. It is designed to be as simple as possible and relies on other command line tools to do checks and issue alerts.

View File

@ -16,15 +16,14 @@ from prometheus_client import start_http_server
DEFAULT_METRICS_PORT = 8080 DEFAULT_METRICS_PORT = 8080
logging.basicConfig( logging.basicConfig(
level=logging.ERROR, level=logging.ERROR, format="%(asctime)s %(levelname)s %(name)s %(message)s"
format='%(asctime)s %(levelname)s %(name)s %(message)s'
) )
logging.getLogger(__name__).addHandler(logging.NullHandler()) logging.getLogger(__name__).addHandler(logging.NullHandler())
def read_yaml(path): def read_yaml(path):
"""Loads config from a YAML file with env interpolation""" """Loads config from a YAML file with env interpolation"""
with open(path, 'r') as yaml: with open(path, "r") as yaml:
contents = yaml.read() contents = yaml.read()
return yamlenv.load(contents) return yamlenv.load(contents)
@ -35,44 +34,40 @@ def validate_monitor_settings(settings):
Note: Cannot yet validate the Alerts exist from within this class. Note: Cannot yet validate the Alerts exist from within this class.
That will be done by Minitor later That will be done by Minitor later
""" """
name = settings.get('name') name = settings.get("name")
if not name: if not name:
raise InvalidMonitorException('Invalid name for monitor') raise InvalidMonitorException("Invalid name for monitor")
if not settings.get('command'): if not settings.get("command"):
raise InvalidMonitorException( raise InvalidMonitorException("Invalid command for monitor {}".format(name))
'Invalid command for monitor {}'.format(name)
)
type_assertions = ( type_assertions = (
('check_interval', int), ("check_interval", int),
('alert_after', int), ("alert_after", int),
('alert_every', int), ("alert_every", int),
) )
for key, val_type in type_assertions: for key, val_type in type_assertions:
val = settings.get(key) val = settings.get(key)
if not isinstance(val, val_type): if not isinstance(val, val_type):
raise InvalidMonitorException( raise InvalidMonitorException(
'Invalid type on {}: {}. Expected {} and found {}'.format( "Invalid type on {}: {}. Expected {} and found {}".format(
name, key, val_type.__name__, type(val).__name__ name, key, val_type.__name__, type(val).__name__
) )
) )
non_zero = ( non_zero = (
'check_interval', "check_interval",
'alert_after', "alert_after",
) )
for key in non_zero: for key in non_zero:
if settings.get(key) == 0: if settings.get(key) == 0:
raise InvalidMonitorException( raise InvalidMonitorException(
'Invalid value for {}: {}. Value cannot be 0'.format( "Invalid value for {}: {}. Value cannot be 0".format(name, key)
name, key
)
) )
def maybe_decode(bstr, encoding='utf-8'): def maybe_decode(bstr, encoding="utf-8"):
try: try:
return bstr.decode(encoding) return bstr.decode(encoding)
except TypeError: except TypeError:
@ -82,14 +77,14 @@ def maybe_decode(bstr, encoding='utf-8'):
def call_output(*popenargs, **kwargs): def call_output(*popenargs, **kwargs):
"""Similar to check_output, but instead returns output and exception""" """Similar to check_output, but instead returns output and exception"""
# So we can capture complete output, redirect sderr to stdout # So we can capture complete output, redirect sderr to stdout
kwargs.setdefault('stderr', subprocess.STDOUT) kwargs.setdefault("stderr", subprocess.STDOUT)
output, ex = None, None output, ex = None, None
try: try:
output = check_output(*popenargs, **kwargs) output = check_output(*popenargs, **kwargs)
except CalledProcessError as e: except CalledProcessError as e:
output, ex = e.output, e output, ex = e.output, e
output = output.rstrip(b'\n') output = output.rstrip(b"\n")
return output, ex return output, ex
@ -113,23 +108,23 @@ class Monitor(object):
def __init__(self, config, counter=None, logger=None): def __init__(self, config, counter=None, logger=None):
"""Accepts a dictionary of configuration items to override defaults""" """Accepts a dictionary of configuration items to override defaults"""
settings = { settings = {
'alerts': ['log'], "alerts": ["log"],
'check_interval': 30, "check_interval": 30,
'alert_after': 4, "alert_after": 4,
'alert_every': -1, "alert_every": -1,
} }
settings.update(config) settings.update(config)
validate_monitor_settings(settings) validate_monitor_settings(settings)
self.name = settings['name'] self.name = settings["name"]
self.command = settings['command'] self.command = settings["command"]
self.alert_down = settings.get('alert_down', []) self.alert_down = settings.get("alert_down", [])
if not self.alert_down: if not self.alert_down:
self.alert_down = settings.get('alerts', []) self.alert_down = settings.get("alerts", [])
self.alert_up = settings.get('alert_up', []) self.alert_up = settings.get("alert_up", [])
self.check_interval = settings.get('check_interval') self.check_interval = settings.get("check_interval")
self.alert_after = settings.get('alert_after') self.alert_after = settings.get("alert_after")
self.alert_every = settings.get('alert_every') self.alert_every = settings.get("alert_every")
self.alert_count = 0 self.alert_count = 0
self.last_check = None self.last_check = None
@ -140,18 +135,18 @@ class Monitor(object):
self._counter = counter self._counter = counter
if logger is None: if logger is None:
self._logger = logging.getLogger( self._logger = logging.getLogger(
'{}({})'.format(self.__class__.__name__, self.name) "{}({})".format(self.__class__.__name__, self.name)
) )
else: else:
self._logger = logger.getChild( self._logger = logger.getChild(
'{}({})'.format(self.__class__.__name__, self.name) "{}({})".format(self.__class__.__name__, self.name)
) )
def _count_check(self, is_success=True, is_alert=False): def _count_check(self, is_success=True, is_alert=False):
if self._counter is not None: if self._counter is not None:
self._counter.labels( self._counter.labels(
monitor=self.name, monitor=self.name,
status=('success' if is_success else 'failure'), status=("success" if is_success else "failure"),
is_alert=is_alert, is_alert=is_alert,
).inc() ).inc()
@ -199,7 +194,7 @@ class Monitor(object):
back_up = None back_up = None
if not self.is_up(): if not self.is_up():
back_up = MinitorAlert( back_up = MinitorAlert(
'{} check is up again!'.format(self.name), "{} check is up again!".format(self.name),
self, self,
) )
self.total_failure_count = 0 self.total_failure_count = 0
@ -215,7 +210,7 @@ class Monitor(object):
if self.total_failure_count < self.alert_after: if self.total_failure_count < self.alert_after:
return return
failure_count = (self.total_failure_count - self.alert_after) failure_count = self.total_failure_count - self.alert_after
if self.alert_every > 0: if self.alert_every > 0:
# Otherwise, we should check against our alert_every # Otherwise, we should check against our alert_every
should_alert = (failure_count % self.alert_every) == 0 should_alert = (failure_count % self.alert_every) == 0
@ -223,15 +218,15 @@ class Monitor(object):
# Only alert on the first failure # Only alert on the first failure
should_alert = failure_count == 1 should_alert = failure_count == 1
else: else:
should_alert = (failure_count >= (2 ** self.alert_count) - 1) should_alert = failure_count >= (2**self.alert_count) - 1
if should_alert: if should_alert:
self.alert_count += 1 self.alert_count += 1
raise MinitorAlert( raise MinitorAlert(
'{} check has failed {} times'.format( "{} check has failed {} times".format(
self.name, self.total_failure_count self.name, self.total_failure_count
), ),
self self,
) )
def is_up(self): def is_up(self):
@ -243,18 +238,18 @@ class Alert(object):
def __init__(self, name, config, counter=None, logger=None): def __init__(self, name, config, counter=None, logger=None):
"""An alert must be named and have a config dict""" """An alert must be named and have a config dict"""
self.name = name self.name = name
self.command = config.get('command') self.command = config.get("command")
if not self.command: if not self.command:
raise InvalidAlertException('Invalid alert {}'.format(self.name)) raise InvalidAlertException("Invalid alert {}".format(self.name))
self._counter = counter self._counter = counter
if logger is None: if logger is None:
self._logger = logging.getLogger( self._logger = logging.getLogger(
'{}({})'.format(self.__class__.__name__, self.name) "{}({})".format(self.__class__.__name__, self.name)
) )
else: else:
self._logger = logger.getChild( self._logger = logger.getChild(
'{}({})'.format(self.__class__.__name__, self.name) "{}({})".format(self.__class__.__name__, self.name)
) )
def _count_alert(self, monitor): def _count_alert(self, monitor):
@ -277,7 +272,7 @@ class Alert(object):
def _format_datetime(self, dt): def _format_datetime(self, dt):
"""Formats a datetime for an alert""" """Formats a datetime for an alert"""
if dt is None: if dt is None:
return 'Never' return "Never"
return dt.isoformat() return dt.isoformat()
def alert(self, message, monitor): def alert(self, message, monitor):
@ -313,64 +308,72 @@ class Minitor(object):
def _parse_args(self, args=None): def _parse_args(self, args=None):
"""Parses command line arguments and returns them""" """Parses command line arguments and returns them"""
parser = ArgumentParser(description='Minimal monitoring') parser = ArgumentParser(description="Minimal monitoring")
parser.add_argument( parser.add_argument(
'--config', '-c', "--config",
dest='config_path', "-c",
default='config.yml', dest="config_path",
help='Path to the config YAML file to use', default="config.yml",
help="Path to the config YAML file to use",
) )
parser.add_argument( parser.add_argument(
'--metrics', '-m', "--metrics",
dest='metrics', "-m",
action='store_true', dest="metrics",
help='Start webserver with metrics', action="store_true",
help="Start webserver with metrics",
) )
parser.add_argument( parser.add_argument(
'--metrics-port', '-p', "--metrics-port",
dest='metrics_port', "-p",
dest="metrics_port",
type=int, type=int,
default=DEFAULT_METRICS_PORT, default=DEFAULT_METRICS_PORT,
help='Port to use when serving metrics', help="Port to use when serving metrics",
) )
parser.add_argument( parser.add_argument(
'--verbose', '-v', "--verbose",
action='count', "-v",
help=('Adjust log verbosity by increasing arg count. Default log', action="count",
'level is ERROR. Level increases with each `v`'), help=(
"Adjust log verbosity by increasing arg count. Default log",
"level is ERROR. Level increases with each `v`",
),
) )
return parser.parse_args(args) return parser.parse_args(args)
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"""
config = read_yaml(config_path) config = read_yaml(config_path)
self.check_interval = config.get('check_interval', 30) self.check_interval = config.get("check_interval", 30)
self.monitors = [ self.monitors = [
Monitor( Monitor(
mon, mon,
counter=self._monitor_counter, counter=self._monitor_counter,
logger=self._logger, logger=self._logger,
) )
for mon in config.get('monitors', []) for mon in config.get("monitors", [])
] ]
# Add default alert for logging # Add default alert for logging
self.alerts = { self.alerts = {
'log': Alert( "log": Alert(
'log', "log",
{'command': ['echo', '{alert_message}!']}, {"command": ["echo", "{alert_message}!"]},
counter=self._alert_counter, counter=self._alert_counter,
logger=self._logger, logger=self._logger,
) )
} }
self.alerts.update({ self.alerts.update(
alert_name: Alert( {
alert_name, alert_name: Alert(
alert, alert_name,
counter=self._alert_counter, alert,
logger=self._logger, counter=self._alert_counter,
) logger=self._logger,
for alert_name, alert in config.get('alerts', {}).items() )
}) for alert_name, alert in config.get("alerts", {}).items()
}
)
def _validate_monitors(self): def _validate_monitors(self):
"""Validates monitors are valid against other config values""" """Validates monitors are valid against other config values"""
@ -378,7 +381,7 @@ class Minitor(object):
# Validate that the interval is valid # Validate that the interval is valid
if monitor.check_interval < self.check_interval: if monitor.check_interval < self.check_interval:
raise InvalidMonitorException( raise InvalidMonitorException(
'Monitor {} check interval is lower global value {}'.format( "Monitor {} check interval is lower global value {}".format(
monitor.name, self.check_interval monitor.name, self.check_interval
) )
) )
@ -386,26 +389,26 @@ class Minitor(object):
for alert in chain(monitor.alert_down, monitor.alert_up): for alert in chain(monitor.alert_down, monitor.alert_up):
if alert not in self.alerts: if alert not in self.alerts:
raise InvalidMonitorException( raise InvalidMonitorException(
'Monitor {} contains an unknown alert: {}'.format( "Monitor {} contains an unknown alert: {}".format(
monitor.name, alert monitor.name, alert
) )
) )
def _init_metrics(self): def _init_metrics(self):
self._alert_counter = Counter( self._alert_counter = Counter(
'minitor_alert_total', "minitor_alert_total",
'Number of Minitor alerts', "Number of Minitor alerts",
['alert', 'monitor'], ["alert", "monitor"],
) )
self._monitor_counter = Counter( self._monitor_counter = Counter(
'minitor_check_total', "minitor_check_total",
'Number of Minitor checks', "Number of Minitor checks",
['monitor', 'status', 'is_alert'], ["monitor", "status", "is_alert"],
) )
self._monitor_status_gauge = Gauge( self._monitor_status_gauge = Gauge(
'minitor_monitor_up_count', "minitor_monitor_up_count",
'Currently responsive monitors', "Currently responsive monitors",
['monitor'], ["monitor"],
) )
def _loop(self): def _loop(self):
@ -420,9 +423,7 @@ class Minitor(object):
result = monitor.check() result = monitor.check()
if result is not None: if result is not None:
self._logger.info( self._logger.info(
'%s: %s', "%s: %s", monitor.name, "SUCCESS" if result else "FAILURE"
monitor.name,
'SUCCESS' if result else 'FAILURE'
) )
except MinitorAlert as minitor_alert: except MinitorAlert as minitor_alert:
self._logger.warning(minitor_alert) self._logger.warning(minitor_alert)
@ -475,5 +476,5 @@ def main(args=None):
return 0 return 0
if __name__ == '__main__': if __name__ == "__main__":
sys.exit(main()) sys.exit(main())

View File

@ -7,47 +7,49 @@ from setuptools import setup
here = path.abspath(path.dirname(__file__)) here = path.abspath(path.dirname(__file__))
# Get the long description from the README file # Get the long description from the README file
with open(path.join(here, 'README.md'), encoding='utf-8') as f: with open(path.join(here, "README.md"), encoding="utf-8") as f:
long_description = f.read() long_description = f.read()
setup( setup(
name='minitor', name="minitor",
version='1.0.3', version="1.0.3",
description='A minimal monitoring tool', description="A minimal monitoring tool",
long_description=long_description, long_description=long_description,
long_description_content_type='text/markdown', long_description_content_type="text/markdown",
url='https://git.iamthefij.com/iamthefij/minitor', url="https://git.iamthefij.com/iamthefij/minitor",
download_url=( download_url=("https://git.iamthefij.com/iamthefij/minitor/archive/master.tar.gz"),
'https://git.iamthefij.com/iamthefij/minitor/archive/master.tar.gz' author="Ian Fijolek",
), author_email="ian@iamthefij.com",
author='Ian Fijolek',
author_email='ian@iamthefij.com',
classifiers=[ classifiers=[
'Development Status :: 5 - Production/Stable', "Development Status :: 5 - Production/Stable",
'Intended Audience :: Developers', "Intended Audience :: Developers",
'Intended Audience :: System Administrators', "Intended Audience :: System Administrators",
'Topic :: System :: Monitoring', "Topic :: System :: Monitoring",
'License :: OSI Approved :: Apache Software License', "License :: OSI Approved :: Apache Software License",
'Programming Language :: Python :: 3', "Programming Language :: Python :: 3",
'Programming Language :: Python :: 3.5', "Programming Language :: Python :: 3.6",
'Programming Language :: Python :: 3.6', "Programming Language :: Python :: 3.7",
'Programming Language :: Python :: 3.7', "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
], ],
keywords='minitor monitoring alerting', keywords="minitor monitoring alerting",
packages=find_packages(exclude=[ packages=find_packages(
'contrib', exclude=[
'docs', "contrib",
'examples', "docs",
'scripts', "examples",
'tests', "scripts",
]), "tests",
]
),
install_requires=[ install_requires=[
'prometheus_client', "prometheus_client",
'yamlenv', "yamlenv",
], ],
entry_points={ entry_points={
'console_scripts': [ "console_scripts": [
'minitor=minitor.main:main', "minitor=minitor.main:main",
], ],
}, },
) )

View File

@ -9,54 +9,47 @@ from tests.util import assert_called_once_with
class TestAlert(object): class TestAlert(object):
@pytest.fixture @pytest.fixture
def monitor(self): def monitor(self):
return Monitor({ return Monitor(
'name': 'Dummy Monitor', {
'command': ['echo', 'foo'], "name": "Dummy Monitor",
}) "command": ["echo", "foo"],
}
)
@pytest.fixture @pytest.fixture
def echo_alert(self): def echo_alert(self):
return Alert( return Alert(
'log', "log",
{ {
'command': [ "command": [
'echo', ( "echo",
'{monitor_name} has failed {failure_count} time(s)!\n' (
'We have alerted {alert_count} time(s)\n' "{monitor_name} has failed {failure_count} time(s)!\n"
'Last success was {last_success}\n' "We have alerted {alert_count} time(s)\n"
'Last output was: {last_output}' "Last success was {last_success}\n"
) "Last output was: {last_output}"
),
] ]
} },
) )
@pytest.mark.parametrize( @pytest.mark.parametrize(
'last_success,expected_success', "last_success,expected_success",
[ [(None, "Never"), (datetime(2018, 4, 10), "2018-04-10T00:00:00")],
(None, 'Never'),
(datetime(2018, 4, 10), '2018-04-10T00:00:00')
]
) )
def test_simple_alert( def test_simple_alert(self, monitor, echo_alert, last_success, expected_success):
self,
monitor,
echo_alert,
last_success,
expected_success
):
monitor.alert_count = 1 monitor.alert_count = 1
monitor.last_output = 'beep boop' monitor.last_output = "beep boop"
monitor.last_success = last_success monitor.last_success = last_success
monitor.total_failure_count = 1 monitor.total_failure_count = 1
with patch.object(echo_alert._logger, 'error') as mock_error: with patch.object(echo_alert._logger, "error") as mock_error:
echo_alert.alert('Exception message', monitor) echo_alert.alert("Exception message", monitor)
assert_called_once_with( assert_called_once_with(
mock_error, mock_error,
'Dummy Monitor has failed 1 time(s)!\n' "Dummy Monitor has failed 1 time(s)!\n"
'We have alerted 1 time(s)\n' "We have alerted 1 time(s)\n"
'Last success was ' + expected_success + '\n' "Last success was " + expected_success + "\n"
'Last output was: beep boop' "Last output was: beep boop",
) )

View File

@ -6,30 +6,31 @@ from minitor.main import Minitor
class TestMinitor(object): class TestMinitor(object):
def test_call_output(self): def test_call_output(self):
# valid command should have result and no exception # valid command should have result and no exception
output, ex = call_output(['echo', 'test']) output, ex = call_output(["echo", "test"])
assert output == b'test' assert output == b"test"
assert ex is None assert ex is None
output, ex = call_output(['ls', '--not-a-real-flag']) output, ex = call_output(["ls", "--not-a-real-flag"])
assert output.startswith(b'ls: ') assert output.startswith(b"ls: ")
assert ex is not None assert ex is not None
def test_run(self): def test_run(self):
"""Doesn't really check much, but a simple integration sanity test""" """Doesn't really check much, but a simple integration sanity test"""
test_loop_count = 5 test_loop_count = 5
os.environ.update({ os.environ.update(
'MAILGUN_API_KEY': 'test-mg-key', {
'AVAILABLE_NUMBER': '555-555-5050', "MAILGUN_API_KEY": "test-mg-key",
'MY_PHONE': '555-555-0505', "AVAILABLE_NUMBER": "555-555-5050",
'ACCOUNT_SID': 'test-account-id', "MY_PHONE": "555-555-0505",
'AUTH_TOKEN': 'test-account-token', "ACCOUNT_SID": "test-account-id",
}) "AUTH_TOKEN": "test-account-token",
args = '--config ./sample-config.yml'.split(' ') }
)
args = "--config ./sample-config.yml".split(" ")
minitor = Minitor() minitor = Minitor()
with patch.object(minitor, '_loop'): with patch.object(minitor, "_loop"):
minitor.run(args) minitor.run(args)
# Skip the loop, but run a single check # Skip the loop, but run a single check
for _ in range(test_loop_count): for _ in range(test_loop_count):

View File

@ -11,40 +11,44 @@ from tests.util import assert_called_once
class TestMonitor(object): class TestMonitor(object):
@pytest.fixture @pytest.fixture
def monitor(self): def monitor(self):
return Monitor({ return Monitor(
'name': 'Sample Monitor', {
'command': ['echo', 'foo'], "name": "Sample Monitor",
'alert_down': ['log'], "command": ["echo", "foo"],
'alert_up': ['log'], "alert_down": ["log"],
'check_interval': 1, "alert_up": ["log"],
'alert_after': 1, "check_interval": 1,
'alert_every': 1, "alert_after": 1,
}) "alert_every": 1,
}
)
@pytest.mark.parametrize('settings', [ @pytest.mark.parametrize(
{'alert_after': 0}, "settings",
{'alert_every': 0}, [
{'check_interval': 0}, {"alert_after": 0},
{'alert_after': 'invalid'}, {"alert_every": 0},
{'alert_every': 'invalid'}, {"check_interval": 0},
{'check_interval': 'invalid'}, {"alert_after": "invalid"},
]) {"alert_every": "invalid"},
{"check_interval": "invalid"},
],
)
def test_monitor_invalid_configuration(self, settings): def test_monitor_invalid_configuration(self, settings):
with pytest.raises(InvalidMonitorException): with pytest.raises(InvalidMonitorException):
validate_monitor_settings(settings) validate_monitor_settings(settings)
@pytest.mark.parametrize( @pytest.mark.parametrize(
'alert_after', "alert_after",
[1, 20], [1, 20],
ids=lambda arg: 'alert_after({})'.format(arg), ids=lambda arg: "alert_after({})".format(arg),
) )
@pytest.mark.parametrize( @pytest.mark.parametrize(
'alert_every', "alert_every",
[-1, 1, 2, 1440], [-1, 1, 2, 1440],
ids=lambda arg: 'alert_every({})'.format(arg), ids=lambda arg: "alert_every({})".format(arg),
) )
def test_monitor_alert_after(self, monitor, alert_after, alert_every): def test_monitor_alert_after(self, monitor, alert_after, alert_every):
monitor.alert_after = alert_after monitor.alert_after = alert_after
@ -59,14 +63,14 @@ class TestMonitor(object):
monitor.failure() monitor.failure()
@pytest.mark.parametrize( @pytest.mark.parametrize(
'alert_after', "alert_after",
[1, 20], [1, 20],
ids=lambda arg: 'alert_after({})'.format(arg), ids=lambda arg: "alert_after({})".format(arg),
) )
@pytest.mark.parametrize( @pytest.mark.parametrize(
'alert_every', "alert_every",
[1, 2, 1440], [1, 2, 1440],
ids=lambda arg: 'alert_every({})'.format(arg), ids=lambda arg: "alert_every({})".format(arg),
) )
def test_monitor_alert_every(self, monitor, alert_after, alert_every): def test_monitor_alert_every(self, monitor, alert_after, alert_every):
monitor.alert_after = alert_after monitor.alert_after = alert_after
@ -102,27 +106,27 @@ class TestMonitor(object):
else: else:
monitor.failure() monitor.failure()
@pytest.mark.parametrize('last_check', [None, datetime(2018, 4, 10)]) @pytest.mark.parametrize("last_check", [None, datetime(2018, 4, 10)])
def test_monitor_should_check(self, monitor, last_check): def test_monitor_should_check(self, monitor, last_check):
monitor.last_check = last_check monitor.last_check = last_check
assert monitor.should_check() assert monitor.should_check()
def test_monitor_check_fail(self, monitor): def test_monitor_check_fail(self, monitor):
assert monitor.last_output is None assert monitor.last_output is None
with patch.object(monitor, 'failure') as mock_failure: with patch.object(monitor, "failure") as mock_failure:
monitor.command = ['ls', '--not-real'] monitor.command = ["ls", "--not-real"]
assert not monitor.check() assert not monitor.check()
assert_called_once(mock_failure) assert_called_once(mock_failure)
assert monitor.last_output is not None assert monitor.last_output is not None
def test_monitor_check_success(self, monitor): def test_monitor_check_success(self, monitor):
assert monitor.last_output is None assert monitor.last_output is None
with patch.object(monitor, 'success') as mock_success: with patch.object(monitor, "success") as mock_success:
assert monitor.check() assert monitor.check()
assert_called_once(mock_success) assert_called_once(mock_success)
assert monitor.last_output is not None assert monitor.last_output is not None
@pytest.mark.parametrize('failure_count', [0, 1]) @pytest.mark.parametrize("failure_count", [0, 1])
def test_monitor_success(self, monitor, failure_count): def test_monitor_success(self, monitor, failure_count):
monitor.alert_count = 0 monitor.alert_count = 0
monitor.total_failure_count = failure_count monitor.total_failure_count = failure_count