diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..bf9db52 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,5 @@ +language: python +python: + - "3.6" +install: "pip install -r requirements.txt" +script: python -m unittest discover tests diff --git a/CHANGELOG.md b/CHANGELOG.md index c368cc3..02c0775 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ This project adheres to [Semantic Versioning](http://semver.org/) and [Keep a ch ## [Unreleased](https://github.com/idealista/prom2teams/tree/develop) +## [1.1.0](https://github.com/idealista/prom2teams/tree/1.1.0) +[Full Changelog](https://github.com/idealista/prom2teams/compare/1.0.0...1.1.0) +### Added +- *[#5](https://github.com/idealista/prom2teams/issues/5) Allow to provide log file path and log level as arguments* @dortegau + +### Fixed +- *[#6](https://github.com/idealista/prom2teams/issues/6) Allow to define previously declared default values as blank values in provided config* @dortegau +- *[#8](https://github.com/idealista/prom2teams/issues/8) Closing all file descriptors and adding some unit tests* @dortegau +- *[#10](https://github.com/idealista/prom2teams/issues/10) Capturing Keyboard Interrupt and logging server stop event* @dortegau ## [1.0.0](https://github.com/idealista/prom2teams/tree/1.0.0) ### Added diff --git a/README.md b/README.md index bc4d438..97f6d25 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ ![Logo](https://raw.githubusercontent.com/idealista/prom2teams/master/logo.gif) +[![Build Status](https://travis-ci.org/idealista/prom2teams.png)](https://travis-ci.org/idealista/prom2teams) + # prom2teams Alert example -**prom2teams** is a HTTP server built with Python that receives alert notifications from a previously configured [Prometheus Alertmanager](https://github.com/prometheus/alertmanager) instance and forwards it to [Microsoft Teams](https://teams.microsoft.com/) using defined connectors. +**prom2teams** is an HTTP server built with Python that receives alert notifications from a previously configured [Prometheus Alertmanager](https://github.com/prometheus/alertmanager) instance and forwards it to [Microsoft Teams](https://teams.microsoft.com/) using defined connectors. - [Getting Started](#getting-started) - [Prerequisities](#prerequisities) @@ -13,6 +15,7 @@ - [Config file](#config-file) - [Configuring Prometheus](#configuring-prometheus) - [Templating](#templating) +- [Testing](#testing) - [Built With](#built-with) - [Versioning](#versioning) - [Authors](#authors) @@ -38,13 +41,15 @@ $ pip3 install prom2teams ## Usage ```bash -# To start the server (a config file path must be provided, Jinja2 template is optional): -$ prom2teams start --configpath [--templatepath ] +# To start the server (a config file path must be provided, log file path, log level and Jinja2 template path are optional arguments): +$ prom2teams start --configpath [--logfilepath ] [--loglevel (DEBUG|INFO|WARNING|ERROR|CRITICAL)] [--templatepath ] # To show the help message: $ prom2teams --help ``` +**Note:** default log level is INFO. Messages are redirected to stdout if no log file path is provided. + ### Config file The config file is an [INI file](https://docs.python.org/3/library/configparser.html#supported-ini-file-structure) and should have the structure described below: @@ -73,6 +78,15 @@ url: 0.0.0.0:8089 prom2teams provides a [default template](app/teams/template.j2) built with [Jinja2](http://jinja.pocoo.org/docs/2.9/) to render messages in Microsoft Teams. This template could be overrided using the 'templatepath' argument ('--templatepath ') during the application start. +## Testing + +To run the test suite you should type the following: + +```bash +# After cloning prom2 teams :) +$ python3 -m unittest discover tests +``` + ## Built With ![Python 3.6.2](https://img.shields.io/badge/Python-3.6.2-green.svg) ![pip 9.0.1](https://img.shields.io/badge/pip-9.0.1-green.svg) diff --git a/app/server.py b/app/server.py index 018d79a..6196a7f 100644 --- a/app/server.py +++ b/app/server.py @@ -40,13 +40,19 @@ def do_POST(self): except Exception as e: logger.error('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(config_file, template_path): - config = get_config(config_file) +def run(provided_config_file, template_path, log_file_path, log_level): + config = get_config('config.ini', provided_config_file) - fileConfig('logging_config.ini') + load_logging_config(log_file_path, log_level) host = config['HTTP Server']['Host'] port = int(config['HTTP Server']['Port']) @@ -56,16 +62,37 @@ def run(config_file, template_path): config['Microsoft Teams']['Connector'], template_path) httpd = HTTPServer(server_address, request_handler) - httpd.serve_forever() + try: + httpd.serve_forever() + except KeyboardInterrupt: + logger.info('server stopped') + + httpd.server_close() + + +def load_logging_config(log_file_path, log_level): + config_file = 'logging_console_config.ini' + defaults = {'log_level': log_level} + + if(log_file_path): + config_file = '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): + provided_config = configparser.ConfigParser() -def get_config(provided_config_file): - default_config = configparser.ConfigParser() - default_config.read_file(open('config.ini')) - default_sections = default_config._sections + with open(default_config_file) as f_def: + provided_config.read_file(f_def) - provided_config = configparser.ConfigParser(defaults=default_sections) - provided_config.read_file(open(provided_config_file)) + with open(provided_config_file) as f_prov: + provided_config.read_file(f_prov) try: provided_config['Microsoft Teams']['Connector'] diff --git a/bin/prom2teams b/bin/prom2teams index d0378f3..8d86284 100755 --- a/bin/prom2teams +++ b/bin/prom2teams @@ -14,8 +14,10 @@ if __name__ == "__main__": 'and sends it to Microsoft Teams using configured connectors ') parser.add_argument('-c', '--configpath', help='config INI file path', required=True) + parser.add_argument('-l', '--logfilepath', help='log file path', required=False) + parser.add_argument('-v', '--loglevel', help='log level', required=False, default='INFO') parser.add_argument('-t', '--templatepath', help='Jinja2 template file path', required=False) args = parser.parse_args() - run(args.configpath, args.templatepath) + run(args.configpath, args.templatepath, args.logfilepath, args.loglevel) diff --git a/logging_console_config.ini b/logging_console_config.ini new file mode 100644 index 0000000..1f3f403 --- /dev/null +++ b/logging_console_config.ini @@ -0,0 +1,21 @@ +[loggers] +keys=root + +[handlers] +keys=stream_handler + +[formatters] +keys=formatter + +[logger_root] +level=%(log_level)s +handlers=stream_handler + +[handler_stream_handler] +class=StreamHandler +level=%(log_level)s +formatter=formatter +args=(sys.stdout,) + +[formatter_formatter] +format=%(asctime)s %(name)-4s %(levelname)-4s %(message)s diff --git a/logging_config.ini b/logging_file_config.ini similarity index 79% rename from logging_config.ini rename to logging_file_config.ini index b36b31e..d994937 100644 --- a/logging_config.ini +++ b/logging_file_config.ini @@ -8,13 +8,13 @@ keys=file_handler keys=formatter [logger_root] -level=INFO +level=%(log_level)s handlers=file_handler [handler_file_handler] class=FileHandler -args=('debug.log',) -level=INFO +args=('%(log_file_path)s',) +level=%(log_level)s formatter=formatter [formatter_formatter] diff --git a/setup.py b/setup.py index 8d902f5..d6563a2 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ def read_requirements_file(): setup(name='prom2teams', - version='1.0.0', + version='1.1.0', description='Project that redirects Prometheus Alert Manager ' 'notifications to Microsoft Teams', long_description=readme, diff --git a/tests/context.py b/tests/context.py new file mode 100644 index 0000000..2a7a196 --- /dev/null +++ b/tests/context.py @@ -0,0 +1,7 @@ +import os +import sys + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../app'))) + +import server +import exceptions diff --git a/tests/data/empty_config.ini b/tests/data/empty_config.ini new file mode 100644 index 0000000..e69de29 diff --git a/tests/data/overriding_defaults.ini b/tests/data/overriding_defaults.ini new file mode 100644 index 0000000..6d666c9 --- /dev/null +++ b/tests/data/overriding_defaults.ini @@ -0,0 +1,6 @@ +[HTTP Server] +Host: 1.1.1.1 +Port: 9089 + +[Microsoft Teams] +Connector=some_url diff --git a/tests/data/without_overriding_defaults.ini b/tests/data/without_overriding_defaults.ini new file mode 100644 index 0000000..a0a37e1 --- /dev/null +++ b/tests/data/without_overriding_defaults.ini @@ -0,0 +1,2 @@ +[Microsoft Teams] +Connector=some_url diff --git a/tests/test_server.py b/tests/test_server.py new file mode 100644 index 0000000..d39828b --- /dev/null +++ b/tests/test_server.py @@ -0,0 +1,51 @@ +import unittest + +from context import server +from context import exceptions + + +class TestServer(unittest.TestCase): + + TEST_CONFIG_FILES_PATH = 'tests/data/' + DEFAULT_CONFIG_RELATIVE_PATH = './config.ini' + + def test_get_config_with_invalid_path(self): + invalid_relative_path = self.TEST_CONFIG_FILES_PATH + 'invalid_path' + + 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): + empty_config_relative_path = self.TEST_CONFIG_FILES_PATH + \ + 'empty_config.ini' + + 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) + + self.assertEqual(config.get('HTTP Server', 'Host'), '0.0.0.0') + 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) + + 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')) + + +if __name__ == '__main__': + unittest.main()