Add defaults and config parser.

Resolves #4
Resolves #6
This commit is contained in:
Scott Wallace 2020-10-19 18:44:02 +01:00
parent 2d5f17bacb
commit c05d02a16f
4 changed files with 121 additions and 40 deletions

View file

@ -2,18 +2,22 @@ This application bridges [Prometheus Alertmanager](https://prometheus.io/docs/al
# Usage # Usage
``` ```
usage: alertify.py [-h] [-H] usage: alertify.py [-h] [-c CONFIG] [-H]
Bridge between Prometheus Alertmanager and Gotify Bridge between Prometheus Alertmanager and Gotify
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
-H, --healthcheck Simply exit with 0 for healthy or 1 when unhealthy -c CONFIG, --config CONFIG
path to config YAML. (default: alertify.yaml)
-H, --healthcheck simply exit with 0 for healthy or 1 when unhealthy
Three environment variables are required to be set: The following environment variables will override any config or default:
* GOTIFY_SERVER: hostname of the Gotify server * LISTEN_PORT (default: 8080)
* GOTIFY_PORT: port of the Gotify server * GOTIFY_SERVER (default: localhost)
* GOTIFY_KEY: app token for alertify * GOTIFY_PORT (default: 80)
* GOTIFY_KEY (default: None)
* VERBOSE (default: False)
``` ```

View file

@ -5,14 +5,23 @@ Module to act as a Prometheus Exporter for Docker containers with a
""" """
import argparse import argparse
import functools
import http.client import http.client
import json import json
import os import os
import sys import sys
from distutils.util import strtobool
from http.server import HTTPServer, SimpleHTTPRequestHandler from http.server import HTTPServer, SimpleHTTPRequestHandler
LISTEN_PORT = 8080 import yaml
VERBOSE = int(os.environ.get('ALERTIFY_VERBOSE', 0))
DEFAULTS = {
'listen_port': int(8080),
'gotify_server': str('localhost'),
'gotify_port': int(80),
'gotify_key': str(),
'verbose': bool(False),
}
class HTTPHandler(SimpleHTTPRequestHandler): class HTTPHandler(SimpleHTTPRequestHandler):
@ -21,6 +30,15 @@ class HTTPHandler(SimpleHTTPRequestHandler):
for Docker containers with a healthcheck configured for Docker containers with a healthcheck configured
""" """
config = None
@staticmethod
def set_config(config):
"""
Method
"""
HTTPHandler.config = config
# Override built-in method # Override built-in method
# pylint: disable=invalid-name # pylint: disable=invalid-name
def do_GET(self): def do_GET(self):
@ -28,7 +46,7 @@ class HTTPHandler(SimpleHTTPRequestHandler):
Method to handle GET requests Method to handle GET requests
""" """
if self.path == '/healthcheck': if self.path == '/healthcheck':
if not healthy(): if not healthy(self.config):
print('ERROR: Check requirements') print('ERROR: Check requirements')
self._respond(500, 'ERR') self._respond(500, 'ERR')
@ -44,6 +62,9 @@ class HTTPHandler(SimpleHTTPRequestHandler):
self._alerts() self._alerts()
def _respond(self, status, message): 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.send_response(int(status) or 500)
self.end_headers() self.end_headers()
self.wfile.write(bytes(str(message).encode())) self.wfile.write(bytes(str(message).encode()))
@ -52,7 +73,7 @@ class HTTPHandler(SimpleHTTPRequestHandler):
""" """
Method to handle the request for alerts Method to handle the request for alerts
""" """
if not healthy(): if not healthy(self.config):
print('ERROR: Check requirements') print('ERROR: Check requirements')
self._respond(500, 'Server not configured correctly') self._respond(500, 'Server not configured correctly')
return return
@ -67,7 +88,7 @@ class HTTPHandler(SimpleHTTPRequestHandler):
self._respond(400, f'Bad JSON: {error}') self._respond(400, f'Bad JSON: {error}')
return return
if VERBOSE: if self.config.get('verbose'):
print('Received from Alertmanager:') print('Received from Alertmanager:')
print(json.dumps(alert, indent=2)) print(json.dumps(alert, indent=2))
@ -94,21 +115,20 @@ class HTTPHandler(SimpleHTTPRequestHandler):
self._respond(400, f'Missing field: {error}') self._respond(400, f'Missing field: {error}')
return return
if VERBOSE: if self.config.get('verbose'):
print('Sending to Gotify:') print('Sending to Gotify:')
print(json.dumps(gotify_msg, indent=2)) print(json.dumps(gotify_msg, indent=2))
response = 'Status: {status}, Reason: {reason}'.format(
**gotify_send( response = gotify_send(
os.environ['GOTIFY_SERVER'], self.config.get('gotify_server'),
os.environ['GOTIFY_PORT'], self.config.get('gotify_port'),
os.environ['GOTIFY_KEY'], self.config.get('gotify_key'),
gotify_msg gotify_msg
) )
)
if VERBOSE: if self.config.get('verbose'):
print(response) print('Status: {status}, Reason: {reason}'.format(**response))
self._respond(200, response) self._respond(response['status'], response['reason'])
def gotify_send(server, port, authkey, payload): def gotify_send(server, port, authkey, payload):
@ -122,8 +142,15 @@ def gotify_send(server, port, authkey, payload):
'Content-type': 'application/json', 'Content-type': 'application/json',
} }
try:
gotify.request('POST', '/message', json.dumps(payload), headers) gotify.request('POST', '/message', json.dumps(payload), headers)
response = gotify.getresponse() response = gotify.getresponse()
except ConnectionRefusedError as error:
print(f'ERROR: {error}')
return {
'status': error.errno,
'reason': error.strerror
}
return { return {
'status': response.status, 'status': response.status,
@ -131,35 +158,73 @@ def gotify_send(server, port, authkey, payload):
} }
def healthy(): def healthy(config):
""" """
Simple funtion to return if all the requirements are met Simple function to return if all the requirements are met
""" """
return all([ return all([
'GOTIFY_SERVER' in os.environ, len(config.get('gotify_key', ''))
'GOTIFY_PORT' in os.environ,
'GOTIFY_KEY' in os.environ,
]) ])
@functools.lru_cache
def parse_config(configfile):
"""
Function to parse a configuration file
"""
config = {}
try:
with open(configfile, 'r') as file:
parsed = yaml.safe_load(file.read())
except FileNotFoundError as error:
print(f'WARN: {error}')
parsed = {}
# Iterate over the DEFAULTS dictionary and check for environment variables
# of the same name, then check for any items in the YAML config, otherwise
# use the default values.
# Ensure the types are adhered to.
for key, val in DEFAULTS.items():
config[key] = os.environ.get(key.upper(), parsed.get(key, val))
if isinstance(val, bool):
config[key] = strtobool(str(config[key]))
else:
config[key] = type(val)(config[key])
if config['verbose']:
print(f'Config:\n{yaml.dump(config, explicit_start=True, default_flow_style=False)}')
return config
if __name__ == '__main__': if __name__ == '__main__':
def cli_parse(): def parse_cli():
""" """
Function to parse the CLI Function to parse the CLI
""" """
maxlen = max([len(key) for key in DEFAULTS])
defaults = [
f' * {key.upper().ljust(maxlen)} (default: {val if val != "" else "None"})'
for key, val in DEFAULTS.items()
]
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter, formatter_class=argparse.RawDescriptionHelpFormatter,
description='Bridge between Prometheus Alertmanager and Gotify\n', description='Bridge between Prometheus Alertmanager and Gotify\n',
epilog='Three environment variables are required to be set:\n' epilog='The following environment variables will override any config or default:\n' +
' * GOTIFY_SERVER: hostname of the Gotify server\n' '\n'.join(defaults)
' * GOTIFY_PORT: port of the Gotify server\n' )
' * GOTIFY_KEY: app token for alertify'
parser.add_argument(
'-c', '--config',
default=f'{os.path.splitext(__file__)[0]}.yaml',
help=f'path to config YAML. (default: {os.path.splitext(__file__)[0]}.yaml)',
) )
parser.add_argument( parser.add_argument(
'-H', '--healthcheck', '-H', '--healthcheck',
action='store_true', action='store_true',
help='Simply exit with 0 for healthy or 1 when unhealthy', help='simply exit with 0 for healthy or 1 when unhealthy',
) )
return parser.parse_args() return parser.parse_args()
@ -168,15 +233,20 @@ if __name__ == '__main__':
""" """
main() main()
""" """
args = cli_parse() args = parse_cli()
config = parse_config(args.config)
if args.healthcheck: if args.healthcheck:
# Invert the sense of 'healthy' for Unix CLI usage # Invert the sense of 'healthy' for Unix CLI usage
return not healthy() return not healthy(config)
print(f'Starting web server on port {LISTEN_PORT}') listen_port = config.get('listen_port')
print(f'Starting web server on port {listen_port}')
try: try:
HTTPServer(('', LISTEN_PORT), HTTPHandler).serve_forever() with HTTPServer(('', listen_port), HTTPHandler) as webserver:
HTTPHandler.set_config(config)
webserver.serve_forever()
except KeyboardInterrupt: except KeyboardInterrupt:
print('Exiting') print('Exiting')

6
example.yaml Normal file
View file

@ -0,0 +1,6 @@
---
# verbose: true
gotify_server: gotifyserver.example.net
gotify_key: sOmEsEcReTkEy1
gotify_port: "80"
listen_port: "8080"

View file

@ -1 +1,2 @@
# 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 # 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
pyyaml