Compare commits
16 commits
Author | SHA1 | Date | |
---|---|---|---|
|
b6fa8e6632 | ||
|
fbcd86a67b | ||
|
a4688c4442 | ||
|
4d74be6b9f | ||
|
1e0099400d | ||
|
d8a82c1334 | ||
|
16030fbe05 | ||
|
e47989d80a | ||
|
076207118b | ||
|
ed5ba680da | ||
|
e0a6c37031 | ||
|
825f55e70a | ||
|
6850133ffe | ||
|
1672331700 | ||
|
6644a0b20e | ||
|
7496d5505e |
2
.github/workflows/python-package.yml
vendored
2
.github/workflows/python-package.yml
vendored
|
@ -15,7 +15,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.5, 3.6, 3.7, 3.8]
|
||||
python-version: [3.6, 3.7, 3.8]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
|||
.pyenv/
|
||||
__pycache__/
|
||||
.pyenv/
|
||||
.pytest_cache/
|
||||
|
|
25
README.md
25
README.md
|
@ -13,18 +13,18 @@ optional arguments:
|
|||
-H, --healthcheck simply exit with 0 for healthy or 1 when unhealthy
|
||||
|
||||
The following environment variables will override any config or default:
|
||||
* DELETE_ONRESOLVE (default: False)
|
||||
* DISABLE_RESOLVED (default: False)
|
||||
* GOTIFY_CLIENT (default: None)
|
||||
* GOTIFY_KEY (default: None)
|
||||
* GOTIFY_PORT (default: 80)
|
||||
* GOTIFY_SERVER (default: localhost)
|
||||
* LISTEN_PORT (default: 8080)
|
||||
* VERBOSE (default: False)
|
||||
* DELETE_ONRESOLVE (default: False)
|
||||
* DISABLE_RESOLVED (default: False)
|
||||
* GOTIFY_KEY_APP (default: None)
|
||||
* GOTIFY_KEY_CLIENT (default: None)
|
||||
* GOTIFY_URL_PREFIX (default: http://localhost)
|
||||
* LISTEN_PORT (default: 8080)
|
||||
* VERBOSE (default: 0)
|
||||
```
|
||||
|
||||
|
||||
# Notes
|
||||
* Requires Python v3.6 or greater (f-strings are used)
|
||||
* Listens on port 8080 by default.
|
||||
* Forwards `resolved` alerts, if not disabled.
|
||||
* Resolved alerts delete the original alert, if enabled.
|
||||
|
@ -47,7 +47,7 @@ docker build . -t 'alertify:latest'
|
|||
|
||||
e.g.
|
||||
```bash
|
||||
docker run --name alertify -p 8080:8080 -e TZ=Europe/London -e GOTIFY_KEY=_APPKEY_ -e GOTIFY_SERVER=gotify -e GOTIFY_PORT=80 alertify:latest
|
||||
docker run --name alertify -p 8080:8080 -e TZ=Europe/London -e GOTIFY_KEY_APP=_APPKEY_ -e GOTIFY_URL_PREFIX=http://gotify alertify:latest
|
||||
```
|
||||
|
||||
## Compose:
|
||||
|
@ -73,9 +73,8 @@ services:
|
|||
environment:
|
||||
- TZ=Europe/London
|
||||
- DELETE_ONRESOLVE=true
|
||||
- GOTIFY_KEY=_APPKEY_
|
||||
- GOTIFY_CLIENT=_CLIENTKEY_
|
||||
- GOTIFY_SERVER=gotify
|
||||
- GOTIFY_PORT=80
|
||||
- GOTIFY_KEY_APP=_APPKEY_
|
||||
- GOTIFY_KEY_CLIENT=_CLIENTKEY_
|
||||
- GOTIFY_URL_PREFIX=http://gotify
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
|
38
alertify.py
38
alertify.py
|
@ -8,18 +8,18 @@ import logging
|
|||
import os
|
||||
import sys
|
||||
|
||||
from src import alertify
|
||||
from src import Alertify
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
def parse_cli():
|
||||
def parse_cli() -> argparse.ArgumentParser:
|
||||
"""
|
||||
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(
|
||||
|
@ -45,9 +45,9 @@ if __name__ == '__main__':
|
|||
|
||||
return parser.parse_args()
|
||||
|
||||
def main():
|
||||
def main() -> int:
|
||||
"""
|
||||
main()
|
||||
Main program logic
|
||||
"""
|
||||
logging.basicConfig(
|
||||
format='%(levelname)s: %(message)s',
|
||||
|
@ -56,25 +56,31 @@ if __name__ == '__main__':
|
|||
|
||||
args = parse_cli()
|
||||
|
||||
# forwarder = alertify.Alertify(args.config)
|
||||
forwarder = alertify.Alertify()
|
||||
alertify = Alertify.Alertify()
|
||||
alertify.configure(args.config)
|
||||
|
||||
if forwarder.config.verbose:
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.DEBUG)
|
||||
# -----------------------------
|
||||
# Calculate logging level
|
||||
# -----------------------------
|
||||
# Config :: Verbose: 0 = WARNING, 1 = INFO, 2 = DEBUG
|
||||
# Logging :: Loglevel: 30 = WARNING, 20 = INFO, 10 = DEBUG
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(max(logging.WARNING - (alertify.config.verbose * 10), 10))
|
||||
# -----------------------------
|
||||
|
||||
if args.healthcheck:
|
||||
_, status = alertify.healthcheck()
|
||||
# Invert the sense of 'healthy' for Unix CLI usage
|
||||
return not forwarder.healthcheck.report()
|
||||
return not 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,7 +1,6 @@
|
|||
---
|
||||
disable_resolved: false
|
||||
gotify_key: sOmEsEcReTkEy1
|
||||
gotify_port: "80"
|
||||
gotify_server: gotifyserver.example.net
|
||||
gotify_url_prefix: http://gotifyserver.example.net
|
||||
listen_port: "8080"
|
||||
verbose: true
|
||||
|
|
|
@ -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
|
||||
requests
|
||||
|
|
100
src/Alertify/__init__.py
Normal file
100
src/Alertify/__init__.py
Normal file
|
@ -0,0 +1,100 @@
|
|||
"""
|
||||
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
|
||||
from typing import Optional, Tuple
|
||||
|
||||
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__)
|
||||
|
||||
# FIXME: * Find a better way to deny FlaskView methods without using a `_`
|
||||
# prefix or raising a NotFound exception
|
||||
|
||||
|
||||
class Alertify(FlaskView):
|
||||
"""
|
||||
Main Alertify class
|
||||
"""
|
||||
|
||||
route_base = '/'
|
||||
trailing_slash = False
|
||||
|
||||
def __init__(self):
|
||||
# Instantiate with defaults
|
||||
self.configure()
|
||||
|
||||
def configure(self, configfile: Optional[str] = None):
|
||||
"""
|
||||
Configure from a configfile
|
||||
"""
|
||||
# Deny via HTTP
|
||||
if request:
|
||||
raise werkzeug.exceptions.NotFound
|
||||
|
||||
self.config = Config(configfile)
|
||||
self.gotify = Gotify(
|
||||
self.config.gotify_url_prefix,
|
||||
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
|
||||
"""
|
||||
# Deny via HTTP
|
||||
if request:
|
||||
raise werkzeug.exceptions.NotFound
|
||||
|
||||
webapp.run(host='0.0.0.0', port=self.config.listen_port)
|
||||
|
||||
@route('/alert', methods=['POST'])
|
||||
def alert(self) -> Tuple[str, int]:
|
||||
"""
|
||||
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) -> Tuple[str, int]:
|
||||
"""
|
||||
Perform a healthcheck and return the results
|
||||
"""
|
||||
response = Healthcheck(self.gotify).gotify_alive()
|
||||
return response['reason'], response['status']
|
||||
|
||||
|
||||
Alertify.register(webapp)
|
|
@ -5,6 +5,7 @@ import inspect
|
|||
import logging
|
||||
import os
|
||||
from distutils.util import strtobool
|
||||
from typing import Optional
|
||||
|
||||
import yaml
|
||||
|
||||
|
@ -16,14 +17,13 @@ class Config:
|
|||
|
||||
delete_onresolve = bool(False)
|
||||
disable_resolved = bool(False)
|
||||
gotify_client = str()
|
||||
gotify_key = str()
|
||||
gotify_port = int(80)
|
||||
gotify_server = str('localhost')
|
||||
gotify_key_app = str()
|
||||
gotify_key_client = str()
|
||||
gotify_url_prefix = str('http://localhost')
|
||||
listen_port = int(8080)
|
||||
verbose = bool(False)
|
||||
verbose = int(0)
|
||||
|
||||
def __init__(self, configfile=None):
|
||||
def __init__(self, configfile: Optional[str] = None):
|
||||
"""
|
||||
Method to parse a configuration file
|
||||
"""
|
||||
|
@ -50,16 +50,16 @@ class Config:
|
|||
else:
|
||||
setattr(self, key, type(default_val)(userval))
|
||||
|
||||
def items(self):
|
||||
def items(self) -> list:
|
||||
"""
|
||||
Method to return an iterator for the configured items
|
||||
"""
|
||||
return {key: getattr(self, key) for key in self.__dict__}.items()
|
||||
|
||||
@classmethod
|
||||
def keys(cls):
|
||||
def keys(cls) -> list:
|
||||
"""
|
||||
Method to return the defaults as a list of keys
|
||||
Method to return the defaults as a list of dict_keys
|
||||
"""
|
||||
return [
|
||||
attr[0]
|
||||
|
@ -74,7 +74,7 @@ class Config:
|
|||
]
|
||||
|
||||
@classmethod
|
||||
def defaults(cls):
|
||||
def defaults(cls) -> dict:
|
||||
"""
|
||||
Classmethod to return the defaults as a dictionary
|
||||
"""
|
|
@ -1,10 +1,12 @@
|
|||
"""
|
||||
Module to handle communication with the Gotify server
|
||||
"""
|
||||
import http.client
|
||||
import json
|
||||
import logging
|
||||
import socket
|
||||
from typing import Optional
|
||||
|
||||
from requests import request
|
||||
|
||||
|
||||
class Gotify:
|
||||
|
@ -12,8 +14,8 @@ class Gotify:
|
|||
Class to handle Gotify communications
|
||||
"""
|
||||
|
||||
def __init__(self, server, port, app_key, client_key=None):
|
||||
self.api = http.client.HTTPConnection(server, port)
|
||||
def __init__(self, url_prefix: str, app_key: str, client_key: Optional[str] = None):
|
||||
self.url_prefix = url_prefix
|
||||
self.app_key = app_key
|
||||
self.client_key = client_key
|
||||
self.base_headers = {
|
||||
|
@ -21,7 +23,7 @@ class Gotify:
|
|||
'Accept': 'application/json',
|
||||
}
|
||||
|
||||
def _call(self, method, url, body=None):
|
||||
def _call(self, method: str, url: str, data: Optional[object] = None) -> dict:
|
||||
"""
|
||||
Method to call Gotify with an app or client key as appropriate
|
||||
"""
|
||||
|
@ -31,11 +33,15 @@ class Gotify:
|
|||
else:
|
||||
headers['X-Gotify-Key'] = self.app_key
|
||||
|
||||
logging.debug('Sending to Gotify:\n%s', body)
|
||||
logging.debug('Sending to Gotify:\n%s', data)
|
||||
|
||||
try:
|
||||
self.api.request(method, url, body=body, headers=headers)
|
||||
response = self.api.getresponse()
|
||||
response = request(
|
||||
method,
|
||||
f'{self.url_prefix}{url}',
|
||||
json=data,
|
||||
headers=headers,
|
||||
)
|
||||
except (ConnectionRefusedError, socket.gaierror) as error:
|
||||
logging.error('Connection error: %s', error)
|
||||
return {
|
||||
|
@ -44,14 +50,13 @@ class Gotify:
|
|||
}
|
||||
|
||||
resp_obj = {
|
||||
'status': response.status,
|
||||
'status': response.status_code,
|
||||
'reason': response.reason,
|
||||
'json': None,
|
||||
}
|
||||
rawbody = response.read()
|
||||
if len(rawbody) > 0:
|
||||
if len(response.content) > 0:
|
||||
try:
|
||||
resp_obj['json'] = json.loads(rawbody.decode())
|
||||
resp_obj['json'] = json.loads(response.content.decode())
|
||||
except json.decoder.JSONDecodeError as error:
|
||||
logging.error('Could not parse JSON: %s', error)
|
||||
|
||||
|
@ -60,14 +65,14 @@ class Gotify:
|
|||
|
||||
return resp_obj
|
||||
|
||||
def delete(self, msg_id):
|
||||
def delete(self, msg_id: str) -> dict:
|
||||
"""
|
||||
Method to delete a message from the Gotify server
|
||||
"""
|
||||
logging.debug('Deleting message ID: %s', msg_id)
|
||||
return self._call('DELETE', f'/message/{msg_id}')
|
||||
|
||||
def find_byfingerprint(self, message):
|
||||
def find_byfingerprint(self, message: str) -> list:
|
||||
"""
|
||||
Method to return the ID of a matching message
|
||||
"""
|
||||
|
@ -75,7 +80,7 @@ class Gotify:
|
|||
new_fingerprint = message['fingerprint']
|
||||
except KeyError:
|
||||
logging.debug('No fingerprint found in new message')
|
||||
return None
|
||||
return list()
|
||||
|
||||
msg_list = []
|
||||
for old_message in self.messages():
|
||||
|
@ -91,7 +96,7 @@ class Gotify:
|
|||
|
||||
return msg_list
|
||||
|
||||
def messages(self):
|
||||
def messages(self) -> dict:
|
||||
"""
|
||||
Method to return a list of messages from the Gotify server
|
||||
"""
|
||||
|
@ -99,18 +104,18 @@ class Gotify:
|
|||
logging.warning(
|
||||
'No client key is configured. No messages could be retrieved.'
|
||||
)
|
||||
return []
|
||||
return dict()
|
||||
logging.debug('Fetching existing messages from Gotify')
|
||||
return self._call('GET', '/message')['json'].get('messages', [])
|
||||
|
||||
def send_alert(self, payload):
|
||||
def send_alert(self, payload: dict) -> dict:
|
||||
"""
|
||||
Method to send a message payload to a Gotify server
|
||||
"""
|
||||
logging.debug('Sending message to Gotify')
|
||||
return self._call('POST', '/message', body=json.dumps(payload, indent=2))
|
||||
return self._call('POST', '/message', data=payload)
|
||||
|
||||
def healthcheck(self):
|
||||
def healthcheck(self) -> dict:
|
||||
"""
|
||||
Method to perform a healthcheck against Gotify
|
||||
"""
|
|
@ -1,6 +1,9 @@
|
|||
"""
|
||||
Module for handling any healthcheck related activity
|
||||
"""
|
||||
from typing import Tuple
|
||||
|
||||
from .gotify import Gotify
|
||||
|
||||
|
||||
class Healthcheck:
|
||||
|
@ -8,21 +11,10 @@ class Healthcheck:
|
|||
Class to handle the healthchecks
|
||||
"""
|
||||
|
||||
def __init__(self, gotify_client):
|
||||
def __init__(self, gotify_client: Gotify):
|
||||
self.gotify = gotify_client
|
||||
|
||||
def report(self):
|
||||
"""
|
||||
Simple method to return a boolean state of the general health
|
||||
"""
|
||||
|
||||
return all(
|
||||
[
|
||||
self.gotify.healthcheck(),
|
||||
]
|
||||
)
|
||||
|
||||
def gotify_alive(self):
|
||||
def gotify_alive(self) -> Tuple[str, int]:
|
||||
"""
|
||||
Simple method to return the Gotify healthcheck response
|
||||
"""
|
|
@ -2,6 +2,9 @@
|
|||
Module for handling the messaging
|
||||
"""
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from .gotify import Gotify
|
||||
|
||||
|
||||
class MessageHandler:
|
||||
|
@ -9,12 +12,17 @@ class MessageHandler:
|
|||
Class to handle alert messaging
|
||||
"""
|
||||
|
||||
def __init__(self, gotify_client, disable_resolved=False, delete_onresolve=False):
|
||||
def __init__(
|
||||
self,
|
||||
gotify_client: Gotify,
|
||||
disable_resolved: Optional[bool] = False,
|
||||
delete_onresolve: Optional[bool] = False,
|
||||
):
|
||||
self.gotify = gotify_client
|
||||
self.disable_resolved = disable_resolved
|
||||
self.delete_onresolve = delete_onresolve
|
||||
|
||||
def process(self, alert):
|
||||
def process(self, alert: dict) -> dict:
|
||||
"""
|
||||
Method to process the alert message
|
||||
"""
|
||||
|
@ -30,11 +38,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,
|
||||
self.config.gotify_client,
|
||||
)
|
||||
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,80 +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()
|
||||
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', '')
|
0
src/conftest.py
Normal file
0
src/conftest.py
Normal file
82
src/tests/Alertify/test___init__.py
Normal file
82
src/tests/Alertify/test___init__.py
Normal file
|
@ -0,0 +1,82 @@
|
|||
"""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.assertEqual(
|
||||
self.alertify.healthcheck(),
|
||||
('OK', 200),
|
||||
)
|
||||
|
||||
@patch('Alertify.health.Healthcheck.gotify_alive')
|
||||
def test_bad_healthcheck(self, mock_gotify_alive):
|
||||
"""Test"""
|
||||
mock_gotify_alive.return_value = {
|
||||
'status': 408,
|
||||
'reason': 'Request Timeout',
|
||||
'json': None,
|
||||
}
|
||||
|
||||
self.assertNotEqual(
|
||||
self.alertify.healthcheck(),
|
||||
('OK', 200),
|
||||
)
|
53
src/tests/Alertify/test_config.py
Normal file
53
src/tests/Alertify/test_config.py
Normal file
|
@ -0,0 +1,53 @@
|
|||
"""Test"""
|
||||
import unittest
|
||||
|
||||
from Alertify import config # pylint: disable=import-error
|
||||
|
||||
|
||||
class ConfigTest(unittest.TestCase):
|
||||
"""
|
||||
Tests for methods in the Config class.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.defaults = {
|
||||
'delete_onresolve': bool(False),
|
||||
'disable_resolved': bool(False),
|
||||
'gotify_key_app': str(),
|
||||
'gotify_key_client': str(),
|
||||
'gotify_url_prefix': str('http://localhost'),
|
||||
'listen_port': int(8080),
|
||||
'verbose': int(0),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
pass
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def test_items(self):
|
||||
"""Test"""
|
||||
self.assertEqual(
|
||||
config.Config().items(),
|
||||
self.defaults.items(),
|
||||
)
|
||||
|
||||
def test_keys(self):
|
||||
"""Test"""
|
||||
self.assertListEqual(
|
||||
config.Config.keys(),
|
||||
list(self.defaults.keys()),
|
||||
)
|
||||
|
||||
def test_defaults(self):
|
||||
"""Test"""
|
||||
self.assertDictEqual(
|
||||
config.Config.defaults(),
|
||||
self.defaults,
|
||||
)
|
120
src/tests/Alertify/test_gotify.py
Normal file
120
src/tests/Alertify/test_gotify.py
Normal file
|
@ -0,0 +1,120 @@
|
|||
"""
|
||||
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
|
||||
|
||||
|
||||
class GotifyTest(unittest.TestCase):
|
||||
"""
|
||||
Tests for methods in the Gotify class.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.gotify_client = gotify.Gotify('http://localhost', '')
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
pass
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
@patch('requests.Session.request')
|
||||
def test_delete(self, mock_request):
|
||||
"""Test"""
|
||||
mock_request.return_value.status_code = 200
|
||||
mock_request.return_value.reason = 'OK'
|
||||
mock_request.return_value.content = ''
|
||||
|
||||
self.assertDictEqual(
|
||||
self.gotify_client.delete('123'),
|
||||
{
|
||||
'status': 200,
|
||||
'reason': 'OK',
|
||||
'json': None,
|
||||
},
|
||||
)
|
||||
|
||||
@patch('Alertify.gotify.Gotify.messages')
|
||||
def test_find_byfingerprint(self, mock_messages):
|
||||
"""Test"""
|
||||
mock_messages.return_value = [
|
||||
{
|
||||
'id': 42,
|
||||
'extras': {'alertify': {'fingerprint': 'deadbeefcafebabe'}},
|
||||
}
|
||||
]
|
||||
|
||||
self.assertListEqual(
|
||||
self.gotify_client.find_byfingerprint({'fingerprint': 'deadbeefcafebabe'}),
|
||||
[42],
|
||||
)
|
||||
|
||||
def test_messages(self):
|
||||
"""Test"""
|
||||
self.assertDictEqual(
|
||||
self.gotify_client.messages(),
|
||||
dict(),
|
||||
)
|
||||
|
||||
@patch('requests.Session.request')
|
||||
def test_send_alert_empty(self, mock_request):
|
||||
"""Test"""
|
||||
mock_request.return_value.status_code = 200
|
||||
mock_request.return_value.reason = 'OK'
|
||||
mock_request.return_value.content = ''
|
||||
|
||||
self.assertDictEqual(
|
||||
self.gotify_client.send_alert(dict()),
|
||||
{
|
||||
'status': 200,
|
||||
'reason': 'OK',
|
||||
'json': None,
|
||||
},
|
||||
)
|
||||
|
||||
@patch('requests.Session.request')
|
||||
def test_send_alert_dummy(self, mock_request):
|
||||
"""Test"""
|
||||
mock_request.return_value.status_code = 200
|
||||
mock_request.return_value.reason = 'OK'
|
||||
mock_request.return_value.content = ''
|
||||
|
||||
self.assertDictEqual(
|
||||
self.gotify_client.send_alert(
|
||||
{
|
||||
'title': 'TITLE',
|
||||
'message': 'MESSAGE',
|
||||
'priority': 0,
|
||||
'extras': dict(),
|
||||
}
|
||||
),
|
||||
{
|
||||
'status': 200,
|
||||
'reason': 'OK',
|
||||
'json': None,
|
||||
},
|
||||
)
|
||||
|
||||
@patch('requests.Session.request')
|
||||
def test_healthcheck(self, mock_request):
|
||||
"""Test"""
|
||||
mock_request.return_value.status_code = 200
|
||||
mock_request.return_value.reason = 'OK'
|
||||
mock_request.return_value.content = ''
|
||||
|
||||
self.assertDictEqual(
|
||||
self.gotify_client.healthcheck(),
|
||||
{
|
||||
'status': 200,
|
||||
'reason': 'OK',
|
||||
'json': None,
|
||||
},
|
||||
)
|
43
src/tests/Alertify/test_health.py
Normal file
43
src/tests/Alertify/test_health.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
"""Test"""
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from Alertify import gotify, health # pylint: disable=import-error
|
||||
|
||||
|
||||
class HealthcheckTest(unittest.TestCase):
|
||||
"""
|
||||
Tests for methods in the Healthcheck class.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.healthcheck = health.Healthcheck(gotify.Gotify('http://localhost', '', ''))
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
pass
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
@patch('Alertify.health.Healthcheck.gotify_alive')
|
||||
def test_gotify_alive(self, mock_gotify_alive):
|
||||
"""Test"""
|
||||
mock_gotify_alive.return_value = {
|
||||
'status': 200,
|
||||
'reason': 'OK',
|
||||
'json': None,
|
||||
}
|
||||
|
||||
self.assertDictEqual(
|
||||
self.healthcheck.gotify_alive(),
|
||||
{
|
||||
'status': 200,
|
||||
'reason': 'OK',
|
||||
'json': None,
|
||||
},
|
||||
)
|
51
src/tests/Alertify/test_messaging.py
Normal file
51
src/tests/Alertify/test_messaging.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
"""Test"""
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from Alertify import gotify, messaging # pylint: disable=import-error
|
||||
|
||||
|
||||
class MessageHandlerTest(unittest.TestCase):
|
||||
"""
|
||||
Tests for methods in the MessageHandler class.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.messaging = messaging.MessageHandler(
|
||||
gotify.Gotify('http://localhost', '', '')
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
pass
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
@patch('Alertify.gotify.Gotify.send_alert')
|
||||
def test_process(self, mock_send_alert):
|
||||
"""Test"""
|
||||
mock_send_alert.return_value = {
|
||||
'status': 200,
|
||||
'reason': 'OK',
|
||||
'json': None,
|
||||
}
|
||||
|
||||
self.assertDictEqual(
|
||||
self.messaging.process(
|
||||
{
|
||||
'status': 'firing',
|
||||
'labels': {},
|
||||
'annotations': {},
|
||||
}
|
||||
),
|
||||
{
|
||||
'status': 200,
|
||||
'reason': 'OK',
|
||||
'json': None,
|
||||
},
|
||||
)
|
Loading…
Reference in a new issue