Move to Flask to fix #17
This commit is contained in:
parent
e0a6c37031
commit
ed5ba680da
23
alertify.py
23
alertify.py
|
@ -8,7 +8,7 @@ import logging
|
|||
import os
|
||||
import sys
|
||||
|
||||
from src import alertify
|
||||
from src import Alertify
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
|
@ -16,10 +16,10 @@ if __name__ == '__main__':
|
|||
"""
|
||||
Function to parse the CLI
|
||||
"""
|
||||
maxlen = max([len(key) for key in alertify.Config.defaults()])
|
||||
maxlen = max([len(key) for key in Alertify.Config.defaults()])
|
||||
defaults = [
|
||||
f' * {key.upper().ljust(maxlen)} (default: {val if val != "" else "None"})'
|
||||
for key, val in alertify.Config.defaults().items()
|
||||
for key, val in Alertify.Config.defaults().items()
|
||||
]
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
|
@ -56,8 +56,8 @@ if __name__ == '__main__':
|
|||
|
||||
args = parse_cli()
|
||||
|
||||
# forwarder = alertify.Alertify(args.config)
|
||||
forwarder = alertify.Alertify()
|
||||
alertify = Alertify.Alertify()
|
||||
alertify.configure(args.config)
|
||||
|
||||
# -----------------------------
|
||||
# Calculate logging level
|
||||
|
@ -65,21 +65,22 @@ if __name__ == '__main__':
|
|||
# Config :: Verbose: 0 = WARNING, 1 = INFO, 2 = DEBUG
|
||||
# Logging :: Loglevel: 30 = WARNING, 20 = INFO, 10 = DEBUG
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(max(logging.WARNING - (forwarder.config.verbose * 10), 10))
|
||||
logger.setLevel(max(logging.WARNING - (alertify.config.verbose * 10), 10))
|
||||
# -----------------------------
|
||||
|
||||
if args.healthcheck:
|
||||
# Invert the sense of 'healthy' for Unix CLI usage
|
||||
return not forwarder.healthcheck.report()
|
||||
_, status = alertify.healthcheck()
|
||||
return status == 200
|
||||
|
||||
logging.info('Version: %s', alertify.__version__)
|
||||
logging.info('Version: %s', Alertify.__version__)
|
||||
|
||||
if forwarder.config.verbose:
|
||||
if alertify.config.verbose:
|
||||
logging.debug('Parsed config:')
|
||||
for key, val in forwarder.config.items():
|
||||
for key, val in alertify.config.items():
|
||||
logging.debug('%s: %s', key, val)
|
||||
|
||||
forwarder.server.listen_and_run()
|
||||
alertify.run()
|
||||
|
||||
return 0
|
||||
|
||||
|
|
|
@ -1,2 +1,4 @@
|
|||
# To ensure app dependencies are ported from your virtual environment/host machine into your container, run 'pip freeze > requirements.txt' in the terminal to overwrite this file
|
||||
flask
|
||||
flask-classful
|
||||
pyyaml
|
||||
|
|
96
src/Alertify/__init__.py
Normal file
96
src/Alertify/__init__.py
Normal file
|
@ -0,0 +1,96 @@
|
|||
"""
|
||||
Alertify module to act as a bridge between Prometheus Alertmanager and Gotify
|
||||
"""
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
__author__ = 'Scott Wallace'
|
||||
__email__ = 'scott@wallace.sh'
|
||||
__maintainer__ = 'Scott Wallace'
|
||||
|
||||
__version__ = '2.0'
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
import werkzeug.exceptions
|
||||
from flask import Flask, request, request_started
|
||||
from flask_classful import FlaskView, route
|
||||
|
||||
from .config import Config
|
||||
from .gotify import Gotify
|
||||
from .health import Healthcheck
|
||||
from .messaging import MessageHandler
|
||||
|
||||
webapp = Flask(__name__)
|
||||
|
||||
|
||||
class Alertify(FlaskView):
|
||||
"""
|
||||
Main alertify class
|
||||
"""
|
||||
|
||||
route_base = '/'
|
||||
trailing_slash = False
|
||||
|
||||
def __init__(self):
|
||||
self.configure()
|
||||
|
||||
def configure(self, configfile=None):
|
||||
"""
|
||||
Configure the object from a configfile
|
||||
"""
|
||||
try:
|
||||
_ = request.args
|
||||
raise werkzeug.exceptions.NotFound
|
||||
except RuntimeError:
|
||||
self.config = Config(configfile)
|
||||
self.gotify = Gotify(
|
||||
self.config.gotify_server,
|
||||
self.config.gotify_port,
|
||||
self.config.gotify_key_app,
|
||||
self.config.gotify_key_client,
|
||||
)
|
||||
self.msg_hndlr = MessageHandler(
|
||||
self.gotify,
|
||||
self.config.disable_resolved,
|
||||
self.config.delete_onresolve,
|
||||
)
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Listen on port and run webserver
|
||||
"""
|
||||
try:
|
||||
_ = request.args
|
||||
raise werkzeug.exceptions.NotFound
|
||||
except RuntimeError:
|
||||
webapp.run(host='0.0.0.0', port=self.config.listen_port)
|
||||
|
||||
@route('/alert', methods=['POST'])
|
||||
def alert(self):
|
||||
"""
|
||||
Handle the alerts from Alertmanager
|
||||
"""
|
||||
message = request.get_json()
|
||||
|
||||
logging.debug(
|
||||
'Received from Alertmanager:\n%s',
|
||||
json.dumps(message, indent=2),
|
||||
)
|
||||
|
||||
for alertmsg in message['alerts']:
|
||||
response = self.msg_hndlr.process(alertmsg)
|
||||
try:
|
||||
return response['reason'], response['status']
|
||||
except UnboundLocalError:
|
||||
return '', 204
|
||||
|
||||
def healthcheck(self):
|
||||
"""
|
||||
Perform a healthcheck and return the results
|
||||
"""
|
||||
response = Healthcheck(self.gotify).gotify_alive()
|
||||
return response['reason'], response['status']
|
||||
|
||||
|
||||
Alertify.register(webapp)
|
|
@ -64,7 +64,7 @@ class Gotify:
|
|||
"""
|
||||
Method to delete a message from the Gotify server
|
||||
"""
|
||||
logging.info('Deleting message ID: %s', msg_id)
|
||||
logging.debug('Deleting message ID: %s', msg_id)
|
||||
return self._call('DELETE', f'/message/{msg_id}')
|
||||
|
||||
def find_byfingerprint(self, message):
|
|
@ -11,16 +11,6 @@ class Healthcheck:
|
|||
def __init__(self, gotify_client):
|
||||
self.gotify = gotify_client
|
||||
|
||||
def report(self):
|
||||
"""
|
||||
Simple method to return a boolean state of the general health
|
||||
"""
|
||||
return all(
|
||||
[
|
||||
self.gotify_alive(),
|
||||
]
|
||||
)
|
||||
|
||||
def gotify_alive(self):
|
||||
"""
|
||||
Simple method to return the Gotify healthcheck response
|
|
@ -30,11 +30,11 @@ class MessageHandler:
|
|||
if self.delete_onresolve:
|
||||
for alert_id in self.gotify.find_byfingerprint(alert):
|
||||
if not self.gotify.delete(alert_id):
|
||||
logging.error('There was a problem removing message ID %d', alert_id)
|
||||
return {
|
||||
'status': 200,
|
||||
'reason': 'Message deletion complete'
|
||||
}
|
||||
logging.error(
|
||||
'There was a problem removing message ID %d',
|
||||
alert_id,
|
||||
)
|
||||
return {'status': 200, 'reason': 'Message deletion complete'}
|
||||
|
||||
prefix = 'resolved'
|
||||
else:
|
|
@ -1,41 +0,0 @@
|
|||
"""
|
||||
Alertify module to act as a bridge between Prometheus Alertmanager and Gotify
|
||||
"""
|
||||
|
||||
__author__ = 'Scott Wallace'
|
||||
__email__ = 'scott@wallace.sh'
|
||||
__maintainer__ = 'Scott Wallace'
|
||||
|
||||
__version__ = '1.6'
|
||||
|
||||
from .config import Config
|
||||
from .gotify import Gotify
|
||||
from .server import Server
|
||||
from .healthcheck import Healthcheck
|
||||
from .messaging import MessageHandler
|
||||
|
||||
|
||||
class Alertify:
|
||||
"""
|
||||
Class for Alertify
|
||||
"""
|
||||
|
||||
def __init__(self, configfile=None):
|
||||
self.config = Config(configfile)
|
||||
self.gotify = Gotify(
|
||||
self.config.gotify_server,
|
||||
self.config.gotify_port,
|
||||
self.config.gotify_key_app,
|
||||
self.config.gotify_key_app,
|
||||
)
|
||||
self.message_handler = MessageHandler(
|
||||
self.gotify,
|
||||
self.config.disable_resolved,
|
||||
self.config.delete_onresolve,
|
||||
)
|
||||
self.healthcheck = Healthcheck(self.gotify)
|
||||
self.server = Server(
|
||||
self.config.listen_port,
|
||||
self.message_handler,
|
||||
self.healthcheck,
|
||||
)
|
|
@ -1,81 +0,0 @@
|
|||
"""
|
||||
Module to act as a bridge between Prometheus Alertmanager and Gotify
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
||||
|
||||
|
||||
class Server:
|
||||
"""
|
||||
Class to handle the webserver for Alertify
|
||||
"""
|
||||
|
||||
def __init__(self, port, message_handler, healthcheck):
|
||||
self.port = port
|
||||
self.message_handler = message_handler
|
||||
self.healthcheck = healthcheck
|
||||
|
||||
def listen_and_run(self):
|
||||
"""
|
||||
Method to bind to the port and run indefinitely
|
||||
"""
|
||||
logging.info('Starting web server on port %d', self.port)
|
||||
|
||||
# FIXME: Find a better way to handle the injection of these values
|
||||
http_handler = self.HTTPHandler
|
||||
http_handler.message_handler = self.message_handler
|
||||
http_handler.healthcheck = self.healthcheck
|
||||
|
||||
try:
|
||||
with HTTPServer(('', self.port), http_handler) as webserver:
|
||||
webserver.serve_forever()
|
||||
return True
|
||||
except KeyboardInterrupt:
|
||||
logging.info('Exiting')
|
||||
|
||||
class HTTPHandler(SimpleHTTPRequestHandler):
|
||||
"""
|
||||
Class to handle the HTTP requests from a client
|
||||
"""
|
||||
|
||||
def _respond(self, status, message):
|
||||
"""
|
||||
Method to output a simple HTTP status and string to the client
|
||||
"""
|
||||
self.send_response(int(status) or 500)
|
||||
self.end_headers()
|
||||
self.wfile.write(bytes(str(message).encode()))
|
||||
|
||||
def do_GET(self): # pylint: disable=invalid-name
|
||||
"""
|
||||
Method to handle GET requests
|
||||
"""
|
||||
if self.path == '/healthcheck':
|
||||
response = self.healthcheck.gotify_alive()
|
||||
self._respond(response['status'], response['reason'])
|
||||
|
||||
def do_POST(self): # pylint: disable=invalid-name
|
||||
"""
|
||||
Method to handle POST requests from AlertManager
|
||||
"""
|
||||
if self.path == '/alert':
|
||||
try:
|
||||
content_length = int(self.headers['Content-Length'])
|
||||
message = json.loads(self.rfile.read(content_length).decode())
|
||||
except json.decoder.JSONDecodeError as error:
|
||||
logging.error('Bad JSON: %s', error)
|
||||
self._respond(400, f'Bad JSON: {error}')
|
||||
|
||||
logging.debug(
|
||||
'Received from Alertmanager:\n%s',
|
||||
json.dumps(message, indent=2),
|
||||
)
|
||||
|
||||
for alert in message['alerts']:
|
||||
response = self.message_handler.process(alert)
|
||||
try:
|
||||
self._respond(response['status'], response['reason'])
|
||||
except UnboundLocalError:
|
||||
self._respond('204', '')
|
68
src/tests/Alertify/test___init__.py
Normal file
68
src/tests/Alertify/test___init__.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
"""Test"""
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import flask
|
||||
|
||||
import Alertify # pylint: disable=import-error
|
||||
|
||||
|
||||
class AlertifyTest(unittest.TestCase):
|
||||
"""
|
||||
Tests for methods in the Alertify class.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.alertify = Alertify.Alertify()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
pass
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def test_configure(self):
|
||||
"""Test"""
|
||||
self.alertify.configure(None)
|
||||
self.assertDictEqual(
|
||||
self.alertify.config.defaults(),
|
||||
Alertify.Config.defaults(),
|
||||
)
|
||||
|
||||
@patch('Alertify.messaging.MessageHandler.process')
|
||||
def test_alert(self, mock_process):
|
||||
"""Test"""
|
||||
mock_process.return_value = {
|
||||
'status': 200,
|
||||
'reason': 'OK',
|
||||
'json': None,
|
||||
}
|
||||
|
||||
with flask.Flask(__name__).test_request_context(
|
||||
'/alert',
|
||||
data='{"alerts": []}',
|
||||
headers={'Content-type': 'application/json'},
|
||||
):
|
||||
self.assertTupleEqual(
|
||||
self.alertify.alert(),
|
||||
('', 204),
|
||||
)
|
||||
|
||||
@patch('Alertify.health.Healthcheck.gotify_alive')
|
||||
def test_healthcheck(self, mock_gotify_alive):
|
||||
"""Test"""
|
||||
mock_gotify_alive.return_value = {
|
||||
'status': 200,
|
||||
'reason': 'OK',
|
||||
'json': None,
|
||||
}
|
||||
|
||||
self.assertTupleEqual(
|
||||
self.alertify.healthcheck(),
|
||||
('OK', 200),
|
||||
)
|
|
@ -1,7 +1,7 @@
|
|||
"""Test"""
|
||||
import unittest
|
||||
|
||||
from alertify import config # pylint: disable=import-error
|
||||
from Alertify import config # pylint: disable=import-error
|
||||
|
||||
|
||||
class ConfigTest(unittest.TestCase):
|
|
@ -1,10 +1,10 @@
|
|||
"""
|
||||
Module to handle unit tests for the alertify.gotify module
|
||||
Module to handle unit tests for the Alertify.gotify module
|
||||
"""
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from alertify import gotify # pylint: disable=import-error
|
||||
from Alertify import gotify # pylint: disable=import-error
|
||||
|
||||
|
||||
class GotifyTest(unittest.TestCase):
|
||||
|
@ -45,7 +45,7 @@ class GotifyTest(unittest.TestCase):
|
|||
},
|
||||
)
|
||||
|
||||
@patch('alertify.gotify.Gotify.messages')
|
||||
@patch('Alertify.gotify.Gotify.messages')
|
||||
def test_find_byfingerprint(self, mock_messages):
|
||||
"""Test"""
|
||||
mock_messages.return_value = [
|
|
@ -2,7 +2,7 @@
|
|||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from alertify import healthcheck, gotify # pylint: disable=import-error
|
||||
from Alertify import gotify, health # pylint: disable=import-error
|
||||
|
||||
|
||||
class HealthcheckTest(unittest.TestCase):
|
||||
|
@ -12,7 +12,7 @@ class HealthcheckTest(unittest.TestCase):
|
|||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.healthcheck = healthcheck.Healthcheck(gotify.Gotify('', 0, '', ''))
|
||||
cls.healthcheck = health.Healthcheck(gotify.Gotify('', 0, '', ''))
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
|
@ -24,21 +24,10 @@ class HealthcheckTest(unittest.TestCase):
|
|||
def tearDown(self):
|
||||
pass
|
||||
|
||||
@patch('alertify.healthcheck.Healthcheck.gotify_alive')
|
||||
def test_report(self, mock_healthcheck):
|
||||
@patch('Alertify.health.Healthcheck.gotify_alive')
|
||||
def test_gotify_alive(self, mock_gotify_alive):
|
||||
"""Test"""
|
||||
mock_healthcheck.return_value = {
|
||||
'status': 200,
|
||||
'reason': 'OK',
|
||||
'json': None,
|
||||
}
|
||||
|
||||
self.assertTrue(self.healthcheck.report())
|
||||
|
||||
@patch('alertify.healthcheck.Healthcheck.gotify_alive')
|
||||
def test_gotify_alive(self, mock_healthcheck):
|
||||
"""Test"""
|
||||
mock_healthcheck.return_value = {
|
||||
mock_gotify_alive.return_value = {
|
||||
'status': 200,
|
||||
'reason': 'OK',
|
||||
'json': None,
|
|
@ -2,7 +2,7 @@
|
|||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from alertify import messaging, gotify # pylint: disable=import-error
|
||||
from Alertify import gotify, messaging # pylint: disable=import-error
|
||||
|
||||
|
||||
class MessageHandlerTest(unittest.TestCase):
|
||||
|
@ -24,7 +24,7 @@ class MessageHandlerTest(unittest.TestCase):
|
|||
def tearDown(self):
|
||||
pass
|
||||
|
||||
@patch('alertify.gotify.Gotify.send_alert')
|
||||
@patch('Alertify.gotify.Gotify.send_alert')
|
||||
def test_process(self, mock_send_alert):
|
||||
"""Test"""
|
||||
mock_send_alert.return_value = {
|
|
@ -1,22 +0,0 @@
|
|||
"""Test"""
|
||||
import unittest
|
||||
|
||||
|
||||
class AlertifyTest(unittest.TestCase):
|
||||
"""
|
||||
Tests for methods in the Alertify class.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
pass
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
|
@ -1,32 +0,0 @@
|
|||
"""Test"""
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from alertify import server # pylint: disable=import-error
|
||||
|
||||
|
||||
class ServerTest(unittest.TestCase):
|
||||
"""
|
||||
Tests for methods in the Server class.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.server = server.Server(0, None, None)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
pass
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
@patch('http.server.HTTPServer.serve_forever')
|
||||
def test_listen_and_run(self, mock_serve_forever):
|
||||
"""Test"""
|
||||
mock_serve_forever.return_value = True
|
||||
|
||||
self.assertTrue(self.server.listen_and_run())
|
Loading…
Reference in a new issue