Skip to content

Commit

Permalink
#22: First version allowing multiple connectors
Browse files Browse the repository at this point in the history
  • Loading branch information
giancarlo committed Feb 15, 2018
1 parent e5d1d22 commit c5f8faa
Show file tree
Hide file tree
Showing 15 changed files with 845 additions and 99 deletions.
4 changes: 4 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions .idea/prom2teams.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

733 changes: 733 additions & 0 deletions .idea/workspace.xml

Large diffs are not rendered by default.

5 changes: 1 addition & 4 deletions prom2teams/message/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,12 @@ def check_fields(json_alerts_attr, json_alerts_labels_attr, json_alerts_annotati
alert_fields[alert_field_key] = 'unknown'
return alert_fields

def parse(json_str):
json_values = json.loads(json_str)

def parse(json_values):
parsed_alarms = {}

for i, alert in enumerate(json_values['alerts']):
json_alerts_attr = alert
json_alerts_labels_attr = json_alerts_attr['labels']
json_alerts_annotations_attr = json_alerts_attr['annotations']
parsed_alarms['alarm_' + str(i)]=check_fields(json_alerts_attr, json_alerts_labels_attr, json_alerts_annotations_attr)

return parsed_alarms
110 changes: 38 additions & 72 deletions prom2teams/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,111 +2,77 @@
import configparser
import os

from http.server import BaseHTTPRequestHandler, HTTPServer
from logging.config import fileConfig
from flask import Flask, request
from flask_restplus import Api, Resource

from prom2teams.teams.client import post
from prom2teams.teams.json_composer import compose
from prom2teams.message.parser import parse

from prom2teams.exceptions import MissingConnectorConfigKeyException


logger = logging.getLogger()
dir = os.path.dirname(__file__)


def generate_request_handler(teams_webhook_url, template_path):
class PrometheusRequestHandler(BaseHTTPRequestHandler):
def __init__(self, *args, **kwargs):
self.teams_webhook_url = teams_webhook_url
self.template_path = template_path
super(PrometheusRequestHandler, self).__init__(*args, **kwargs)

def _set_headers(self, status_code):
self.send_response(status_code)
self.send_header('Content-type', 'text/html')
self.end_headers()

def do_POST(self):
try:
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length).decode('utf-8')
logger.debug('Data received: %s', post_data)

alarms = parse(post_data)

for key, alarm in alarms.items():
sending_alarm = compose(self.template_path, alarm)
logger.debug('The message that will be sent is: %s',
str(sending_alarm))
post(self.teams_webhook_url, sending_alarm)
self._set_headers(200)

except Exception as e:
logger.exception('Error processing request: %s', str(e))
self.send_error(500, 'Error processing request')

def log_message(self, format, *args):
logger.info("%s - - [%s] %s" % (self.address_string(),
self.log_date_time_string(),
format % args))

return PrometheusRequestHandler


def run(provided_config_file, template_path, log_file_path, log_level):
config = get_config(os.path.join(dir, 'config.ini'),
provided_config_file)

config = get_config(provided_config_file)
load_logging_config(log_file_path, log_level)

host = config['HTTP Server']['Host']
port = int(config['HTTP Server']['Port'])

server_address = (host, port)
request_handler = generate_request_handler(
config['Microsoft Teams']['Connector'],
template_path)
httpd = HTTPServer(server_address, request_handler)
app = Flask(__name__)
api = Api(app)
@api.route('/v2/<string:connector>')
class AlarmSender(Resource):
def post(self, connector):
json_str = request.get_json()
webhook_url = config['Microsoft Teams'][connector]
send_alarms_to_teams(json_str, webhook_url, template_path)
return 'OK', 201

try:
httpd.serve_forever()
except KeyboardInterrupt:
logger.info('server stopped')
@api.route('/')
class AlarmSenderDeprecated(Resource):
def post(self):
json_str = request.get_json()
webhook_url = config['Microsoft Teams']['Connector']
send_alarms_to_teams(json_str, webhook_url, template_path)
return 'OK', 201

httpd.server_close()
app.run(host=host, port=port, debug=False)


def send_alarms_to_teams(json, teams_webhook_url, template_path):
alarms = parse(json)
for key, alarm in alarms.items():
sending_alarm = compose(template_path, alarm)
logger.debug('The message that will be sent is: %s',
str(sending_alarm))
post(teams_webhook_url, sending_alarm)


def load_logging_config(log_file_path, log_level):
config_file = os.path.join(dir, 'logging_console_config.ini')
defaults = {'log_level': log_level}

if(log_file_path):
config_file = os.path.join(dir, 'logging_file_config.ini')
defaults = {
'log_level': log_level,
'log_file_path': log_file_path
}

fileConfig(config_file, defaults=defaults)


def get_config(default_config_file, provided_config_file):
def get_config(provided_config_file):
provided_config = configparser.ConfigParser()

with open(default_config_file) as f_def:
provided_config.read_file(f_def)

with open(provided_config_file) as f_prov:
provided_config.read_file(f_prov)

try:
provided_config['Microsoft Teams']['Connector']
except KeyError:
exception_msg = 'missing required Microsoft Teams' \
' Connector key in provided config'

raise MissingConnectorConfigKeyException(exception_msg)
with open(provided_config_file) as f_prov:
provided_config.read_file(f_prov)
if not provided_config.options('Microsoft Teams'):
raise MissingConnectorConfigKeyException('missing connector key in provided config')

except configparser.NoSectionError:
raise MissingConnectorConfigKeyException('missing required Microsoft Teams / '
'connector key in provided config')

return provided_config
6 changes: 3 additions & 3 deletions prom2teams/teams/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@


def post(teams_webhook_url, message):
response = session.post(
teams_webhook_url,
data=message)

response = session.post(teams_webhook_url, data=message)

if not response.ok:
exception_msg = 'Error performing request to: {}.' \
Expand All @@ -20,3 +19,4 @@ def post(teams_webhook_url, message):
raise MicrosoftTeamsRequestException(exception_msg.format(teams_webhook_url,
str(response.status_code),
str(response.text)))

4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
## Requirements
requests
jinja2
jinja2
flask
flask-restplus
4 changes: 2 additions & 2 deletions tests/data/jsons/all_ok.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
"alertname": "DiskSpace",
"device": "rootfs",
"fstype": "rootfs",
"instance": "cs30.evilcorp",
"instance": "idealista.Test",
"job": "fsociety",
"mountpoint": "/",
"severity": "severe"
},
"annotations": {
"description": "disk usage 73% on rootfs device",
"description": "disk usage 93% on rootfs device",
"summary": "Disk usage alert on CS30.evilcorp"
},
"startsAt": "2017-05-09T07:01:37.803Z",
Expand Down
8 changes: 8 additions & 0 deletions tests/data/multiple_connectors_config.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[HTTP Server]
Host: 1.1.1.1
Port: 9089

[Microsoft Teams]
connector1 = teams_webhook_url
connector2 = another_teams_webhook_url
connector3 = definitely_another_teams_webhook_url
2 changes: 1 addition & 1 deletion tests/data/overriding_defaults.ini
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ Host: 1.1.1.1
Port: 9089

[Microsoft Teams]
Connector=some_url
Connector = some_url
6 changes: 5 additions & 1 deletion tests/data/without_overriding_defaults.ini
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
[HTTP Server]
Host: 1.1.1.1
Port: 8089

[Microsoft Teams]
Connector=some_url
Connector = some_url
16 changes: 8 additions & 8 deletions tests/test_json_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,26 @@ class TestJSONFields(unittest.TestCase):
def test_json_with_all_fields(self):
with open(self.TEST_CONFIG_FILES_PATH + 'all_ok.json') as json_data:
json_received = json.load(json_data)
alert_fields = parse(json.dumps(json_received))
self.assertNotIn('unknown',str(alert_fields))
alert_fields = parse(json_received)
self.assertNotIn('unknown', str(alert_fields))

def test_json_without_mandatory_field(self):
with open(self.TEST_CONFIG_FILES_PATH + 'without_mandatory_field.json') as json_data:
json_received = json.load(json_data)
alert_fields = parse(json.dumps(json_received))
self.assertIn('unknown',str(alert_fields))
alert_fields = parse(json_received)
self.assertIn('unknown', str(alert_fields))

def test_json_without_optional_field(self):
with open(self.TEST_CONFIG_FILES_PATH + 'without_optional_field.json') as json_data:
json_received = json.load(json_data)
alert_fields = parse(json.dumps(json_received))
self.assertNotIn('unknown',str(alert_fields))
alert_fields = parse(json_received)
self.assertNotIn('unknown', str(alert_fields))

def test_json_without_instance_field(self):
with open(self.TEST_CONFIG_FILES_PATH + 'without_instance_field.json') as json_data:
json_received = json.load(json_data)
alert_fields = parse(json.dumps(json_received))
self.assertEqual('unknown',str(alert_fields['alarm_0']['alert_instance']))
alert_fields = parse(json_received)
self.assertEqual('unknown', str(alert_fields['alarm_0']['alert_instance']))

if __name__ == '__main__':
unittest.main()
19 changes: 12 additions & 7 deletions tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ def test_get_config_with_invalid_path(self):

self.assertRaises(FileNotFoundError,
server.get_config,
self.DEFAULT_CONFIG_RELATIVE_PATH,
invalid_relative_path)

def test_get_config_without_required_keys_should_raise_exception(self):
Expand All @@ -23,29 +22,35 @@ def test_get_config_without_required_keys_should_raise_exception(self):

self.assertRaises(exceptions.MissingConnectorConfigKeyException,
server.get_config,
self.DEFAULT_CONFIG_RELATIVE_PATH,
empty_config_relative_path)

def test_get_config_without_override(self):
provided_config_relative_path = self.TEST_CONFIG_FILES_PATH + \
'without_overriding_defaults.ini'
config = server.get_config(self.DEFAULT_CONFIG_RELATIVE_PATH,
provided_config_relative_path)
config = server.get_config(provided_config_relative_path)

self.assertEqual(config.get('HTTP Server', 'Host'), '0.0.0.0')
self.assertEqual(config.get('HTTP Server', 'Host'), '1.1.1.1')
self.assertEqual(config.get('HTTP Server', 'Port'), '8089')
self.assertTrue(config.get('Microsoft Teams', 'Connector'))

def test_get_config_overriding_defaults(self):
provided_config_relative_path = self.TEST_CONFIG_FILES_PATH + \
'overriding_defaults.ini'
config = server.get_config(self.DEFAULT_CONFIG_RELATIVE_PATH,
provided_config_relative_path)
config = server.get_config(provided_config_relative_path)

self.assertEqual(config.get('HTTP Server', 'Host'), '1.1.1.1')
self.assertEqual(config.get('HTTP Server', 'Port'), '9089')
self.assertTrue(config.get('Microsoft Teams', 'Connector'))


def test_connectors_configured(self):
provided_config_relative_path = self.TEST_CONFIG_FILES_PATH + \
'multiple_connectors_config.ini'
config = server.get_config(provided_config_relative_path)
self.assertEqual(config['Microsoft Teams']['connector1'], 'teams_webhook_url')
self.assertEqual(config['Microsoft Teams']['connector2'], 'another_teams_webhook_url')
self.assertEqual(config['Microsoft Teams']['connector3'], 'definitely_another_teams_webhook_url')


if __name__ == '__main__':
unittest.main()

0 comments on commit c5f8faa

Please sign in to comment.