From 33037c6d9e61930ae785b9027aad9f749e8d5e92 Mon Sep 17 00:00:00 2001 From: andersh Date: Mon, 2 Sep 2019 11:35:24 +0200 Subject: [PATCH 1/3] Fix log name --- icinga2_exporter/log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/icinga2_exporter/log.py b/icinga2_exporter/log.py index b4b7679..4da25b7 100644 --- a/icinga2_exporter/log.py +++ b/icinga2_exporter/log.py @@ -23,7 +23,7 @@ import datetime from pythonjsonlogger import jsonlogger -logger = logging.getLogger('monitor-exporter') +logger = logging.getLogger('icinga2-exporter') # def configure_logger(log_level="INFO", log_filename=None, format=None): From cd5bfea07ccb086e2b405bb47bb5fa2e7f1de7c2 Mon Sep 17 00:00:00 2001 From: andersh Date: Mon, 2 Sep 2019 12:02:35 +0200 Subject: [PATCH 2/3] Replace Flask with Quart --- icinga2_exporter/main.py | 11 ++-- icinga2_exporter/monitorconnection.py | 32 +++++++++ icinga2_exporter/perfdata.py | 45 +++++++++++++ icinga2_exporter/proxy.py | 95 +++++++-------------------- 4 files changed, 106 insertions(+), 77 deletions(-) diff --git a/icinga2_exporter/main.py b/icinga2_exporter/main.py index 4b5526d..a87acf4 100644 --- a/icinga2_exporter/main.py +++ b/icinga2_exporter/main.py @@ -24,7 +24,7 @@ import icinga2_exporter.fileconfiguration as config import icinga2_exporter.proxy as proxy -from flask import Flask +from quart import Quart import icinga2_exporter.monitorconnection as monitorconnection @@ -62,9 +62,9 @@ def start(): monitorconnection.MonitorConfig(configuration) log.info('Starting web app on port: ' + str(port)) - app = Flask(__name__) + app = proxy.app #Quart(__name__) - app.register_blueprint(proxy.app, url_prefix='/') + #app.register_blueprint(proxy.app, url_prefix='/') app.run(host='0.0.0.0', port=port) @@ -86,8 +86,9 @@ def create_app(config_path=None): monitorconnection.MonitorConfig(configuration) log.info('Starting web app') - app = Flask(__name__) + app = proxy.app # Quart(__name__) - app.register_blueprint(proxy.app, url_prefix='/') + #app.register_blueprint(proxy.app, url_prefix='/') + #app.register_blueprint(proxy.app) return app diff --git a/icinga2_exporter/monitorconnection.py b/icinga2_exporter/monitorconnection.py index fe5ddb1..2b6ff9b 100644 --- a/icinga2_exporter/monitorconnection.py +++ b/icinga2_exporter/monitorconnection.py @@ -21,6 +21,7 @@ import requests import json +import aiohttp from requests.auth import HTTPBasicAuth import icinga2_exporter.log as log @@ -142,3 +143,34 @@ def post(self, url, body): log.error("{}".format(str(err))) return data_json + + + async def async_get_perfdata(self, hostname): + # Get performance data from Monitor and return in json format + body = {"joins": ["host.vars"], + "attrs": ["__name", "display_name", "check_command", "last_check_result", "vars", "host_name"], + "filter": 'service.host_name==\"{}\"'.format(hostname)} + + data_json = await self.async_post(self.url_query_service_perfdata, body) + + if not data_json: + log.warn('Received no perfdata from Icinga2') + + return data_json + + + async def async_post(self, url, body): + data_json = {} + try: + async with aiohttp.ClientSession() as session: + async with session.post(url, auth=aiohttp.BasicAuth(self.user, self.passwd), + verify_ssl=False, + headers={'Content-Type': 'application/json', + 'X-HTTP-Method-Override': 'GET'}, + data=json.dumps(body)) as response: + re = await response.text() + print(re) + print(response.status) + return json.loads(re) + finally: + pass diff --git a/icinga2_exporter/perfdata.py b/icinga2_exporter/perfdata.py index 38c01de..7a606ad 100644 --- a/icinga2_exporter/perfdata.py +++ b/icinga2_exporter/perfdata.py @@ -47,6 +47,7 @@ def get_perfdata(self) -> dict: Collect icinga2 data and parse it into prometheus metrics :return: """ + data_json = self.monitor.get_perfdata(self.query_hostname) if 'results' in data_json: for serivce_attrs in data_json['results']: @@ -60,6 +61,50 @@ def get_perfdata(self) -> dict: # For all host custom vars add as label labels.update(Perfdata.get_host_custom_vars(serivce_attrs)) + for perf_string in serivce_attrs['attrs']['last_check_result']['performance_data']: + perf = Perfdata.parse_perfdata(perf_string) + + # For each perfdata metrics + for perf_data_key, perf_data_value in perf.items(): + + if 'value' in perf_data_value: + prometheus_key = self.format_promethues_metrics_name(check_command, perf_data_key, + perf_data_value) + + # Add more labels based on perfname + if check_command in self.perfname_to_label: + labels.update( + Perfdata.add_labels_by_items( + self.perfname_to_label[check_command]['label_name'], + perf_data_key)) + + prometheus_key_with_labels = Perfdata.concat_metrics_name_and_labels(labels, + prometheus_key) + + self.perfdatadict.update({prometheus_key_with_labels: str(perf_data_value['value'])}) + + return self.perfdatadict + + async def async_get_perfdata(self) -> dict: + """ + Collect icinga2 data and parse it into prometheus metrics + :return: + """ + + data_json = await self.monitor.async_get_perfdata(self.query_hostname) + if 'results' in data_json: + for serivce_attrs in data_json['results']: + if 'attrs' in serivce_attrs and 'last_check_result' in serivce_attrs['attrs'] and 'performance_data' in \ + serivce_attrs['attrs']['last_check_result']: + check_command = serivce_attrs['attrs']['check_command'] + # Get default labels + labels = {'hostname': serivce_attrs['attrs']['host_name'], + 'service': serivce_attrs['attrs']['display_name']} + + # For all host custom vars add as label + labels.update(Perfdata.get_host_custom_vars(serivce_attrs)) + + for perf_string in serivce_attrs['attrs']['last_check_result']['performance_data']: perf = Perfdata.parse_perfdata(perf_string) diff --git a/icinga2_exporter/proxy.py b/icinga2_exporter/proxy.py index b1c8a46..e68fd8b 100644 --- a/icinga2_exporter/proxy.py +++ b/icinga2_exporter/proxy.py @@ -18,17 +18,19 @@ along with icinga2-exporter-exporter. If not, see . """ +import asyncio +from quart import request, Response, jsonify, Quart -from flask import request, Response, jsonify, Blueprint from prometheus_client import (CONTENT_TYPE_LATEST, Counter) from icinga2_exporter.perfdata import Perfdata import icinga2_exporter.monitorconnection as monitorconnection import icinga2_exporter.log as log -app = Blueprint("prom", __name__) +app = Quart( __name__) total_requests = Counter('requests', 'Total requests to monitor-exporter endpoint') +loop = asyncio.new_event_loop() @app.route('/', methods=['GET']) def hello_world(): @@ -54,13 +56,31 @@ def get_metrics(): return resp +@app.route("/ametrics", methods=['GET']) +async def get_ametrics(): + log.info(request.url) + target = request.args.get('target') + + log.info('Collect metrics', {'target': target}) + + monitor_data = Perfdata(monitorconnection.MonitorConfig(), target) + + # Fetch performance data from Monitor + await asyncio.get_event_loop().create_task(monitor_data.async_get_perfdata()) + + target_metrics = monitor_data.prometheus_format() + + resp = Response(target_metrics) + resp.headers['Content-Type'] = CONTENT_TYPE_LATEST + #after_request_func(resp) + return resp @app.route("/health", methods=['GET']) def get_health(): return chech_healthy() -@app.after_request +#@app.after_request def after_request_func(response): total_requests.inc() @@ -75,72 +95,3 @@ def chech_healthy() -> Response: resp = jsonify({'status': 'ok'}) resp.status_code = 200 return resp - -# def read_config(config_file: str) -> dict: -# """ -# Read configuration file -# :param config_file: -# :return: -# """ -# config = {} -# try: -# ymlfile = open(config_file, 'r') -# config = yaml.load(ymlfile, Loader=yaml.SafeLoader) -# except (FileNotFoundError, IOError): -# print("Config file {} not found".format(config_file)) -# exit(1) -# except (yaml.YAMLError, yaml.MarkedYAMLError) as err: -# print("Error will reading config file - {}".format(err)) -# exit(1) -# -# return config - - -# def start(): -# parser = argparse.ArgumentParser(description='monitor_exporter') -# -# parser.add_argument('-f', '--configfile', -# dest="configfile", help="configuration file") -# -# parser.add_argument('-p', '--port', -# dest="port", help="Server port") -# -# args = parser.parse_args() -# -# port = 5000 -# if args.port: -# port = args.port -# -# config_file = 'config.yml' -# if args.configfile: -# config_file = args.configfile -# -# configuration = config.read_config(config_file) -# -# formatter = log.configure_logger(configuration) -# ## -# -# monitorconnection.MonitorConfig(configuration) -# log.info('Starting web app on port: ' + str(port)) -# -# -# app.run(host='0.0.0.0', port=port) -# app.logger.handlers[0].setFormatter(formatter) - - -# def create_app(config_path = None): -# -# config_file = 'config.yml' -# if config_path: -# config_file = config_path -# -# config = read_config(config_file) -# -# formatter = log.configure_logger(config) -# -# monitorconnection.MonitorConfig(config) -# log.info('Starting web app') -# -# app.logger.handlers[0].setFormatter(formatter) -# -# return app From 1e5a3200eb3f8df396b6688fef8548fea047cdac Mon Sep 17 00:00:00 2001 From: andersh Date: Wed, 4 Sep 2019 16:22:29 +0200 Subject: [PATCH 3/3] Clean up none used endpoint --- README.md | 13 +++++--- icinga2_exporter/main.py | 25 ++++++++------ icinga2_exporter/monitorconnection.py | 2 -- icinga2_exporter/perfdata.py | 48 ++------------------------- icinga2_exporter/proxy.py | 28 +++------------- requirements.txt | 27 +++++++++++++-- 6 files changed, 53 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index 6682f6c..ce3d56f 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,9 @@ icinga2-exporter The icinga2-exporter utilizes the [Icinga 2](https://icinga.com/) REST API to fetch service based performance data and publish it in a way that lets [Prometheus](https://prometheus.io/) scrape the performance data as metrics. +The service is based on [Quart](https://pgjones.gitlab.io/quart/). Quart's is compatible with Flask but based +on Asyncio. + Benefits: - Enable advanced queries and aggregation on timeseries @@ -81,7 +84,7 @@ All configuration is made in the config.yml file. Example: ```yaml -# Port can be overridden by using -p if running development flask +# Port can be overridden by using -p if running development quart #port: 9638 icinga2: @@ -189,21 +192,21 @@ scrape_configs: # Running -## Development with flask built in webserver +## Development with quart built in webserver python -m icinga2_exporter -f config.yml The switch -p enable setting of the port. -## Production with gunicorn +## Production with gunicorn as ASGI continer Running with default config.yml. The default location is current directory - gunicorn --access-logfile /dev/null -w 4 "wsgi:create_app()" + gunicorn --access-logfile /dev/null -w 4 -k uvicorn.workers.UvicornWorker "wsgi:create_app()" Set the path to the configuration file. - gunicorn --access-logfile /dev/null -w 4 "wsgi:create_app('/etc/icinga2-exporter/config.yml')" + gunicorn --access-logfile /dev/null -w 4 -k uvicorn.workers.UvicornWorker "wsgi:create_app('/etc/icinga2-exporter/config.yml')" > Port for gunicorn is default 8000, but can be set with -b, e.g. `-b localhost:9638` diff --git a/icinga2_exporter/main.py b/icinga2_exporter/main.py index a87acf4..b7cf54b 100644 --- a/icinga2_exporter/main.py +++ b/icinga2_exporter/main.py @@ -20,11 +20,10 @@ """ import argparse +from quart import Quart import icinga2_exporter.log as log import icinga2_exporter.fileconfiguration as config -import icinga2_exporter.proxy as proxy - -from quart import Quart +from icinga2_exporter.proxy import app as icinga2 import icinga2_exporter.monitorconnection as monitorconnection @@ -57,14 +56,13 @@ def start(): port = args.port log.configure_logger(configuration) - ## monitorconnection.MonitorConfig(configuration) log.info('Starting web app on port: ' + str(port)) - app = proxy.app #Quart(__name__) - - #app.register_blueprint(proxy.app, url_prefix='/') + app = Quart(__name__) + app.register_blueprint(icinga2, url_prefix='') + list_routes(app) app.run(host='0.0.0.0', port=port) @@ -86,9 +84,14 @@ def create_app(config_path=None): monitorconnection.MonitorConfig(configuration) log.info('Starting web app') - app = proxy.app # Quart(__name__) - - #app.register_blueprint(proxy.app, url_prefix='/') - #app.register_blueprint(proxy.app) + app = Quart(__name__) + app.register_blueprint(icinga2, url_prefix='') return app + +def list_routes(app): + import urllib + output = [] + for rule in app.url_map.iter_rules(): + print("route") + print (rule) \ No newline at end of file diff --git a/icinga2_exporter/monitorconnection.py b/icinga2_exporter/monitorconnection.py index 2b6ff9b..6de9562 100644 --- a/icinga2_exporter/monitorconnection.py +++ b/icinga2_exporter/monitorconnection.py @@ -169,8 +169,6 @@ async def async_post(self, url, body): 'X-HTTP-Method-Override': 'GET'}, data=json.dumps(body)) as response: re = await response.text() - print(re) - print(response.status) return json.loads(re) finally: pass diff --git a/icinga2_exporter/perfdata.py b/icinga2_exporter/perfdata.py index 7a606ad..f6bfa1d 100644 --- a/icinga2_exporter/perfdata.py +++ b/icinga2_exporter/perfdata.py @@ -42,50 +42,7 @@ def __init__(self, monitor: Monitor, query_hostname: str): self.perfname_to_label = monitor.get_perfname_to_label() self.perfdatadict = {} - def get_perfdata(self) -> dict: - """ - Collect icinga2 data and parse it into prometheus metrics - :return: - """ - - data_json = self.monitor.get_perfdata(self.query_hostname) - if 'results' in data_json: - for serivce_attrs in data_json['results']: - if 'attrs' in serivce_attrs and 'last_check_result' in serivce_attrs['attrs'] and 'performance_data' in \ - serivce_attrs['attrs']['last_check_result']: - check_command = serivce_attrs['attrs']['check_command'] - # Get default labels - labels = {'hostname': serivce_attrs['attrs']['host_name'], - 'service': serivce_attrs['attrs']['display_name']} - - # For all host custom vars add as label - labels.update(Perfdata.get_host_custom_vars(serivce_attrs)) - - for perf_string in serivce_attrs['attrs']['last_check_result']['performance_data']: - perf = Perfdata.parse_perfdata(perf_string) - - # For each perfdata metrics - for perf_data_key, perf_data_value in perf.items(): - - if 'value' in perf_data_value: - prometheus_key = self.format_promethues_metrics_name(check_command, perf_data_key, - perf_data_value) - - # Add more labels based on perfname - if check_command in self.perfname_to_label: - labels.update( - Perfdata.add_labels_by_items( - self.perfname_to_label[check_command]['label_name'], - perf_data_key)) - - prometheus_key_with_labels = Perfdata.concat_metrics_name_and_labels(labels, - prometheus_key) - - self.perfdatadict.update({prometheus_key_with_labels: str(perf_data_value['value'])}) - - return self.perfdatadict - - async def async_get_perfdata(self) -> dict: + async def get_perfdata(self) -> dict: """ Collect icinga2 data and parse it into prometheus metrics :return: @@ -95,7 +52,8 @@ async def async_get_perfdata(self) -> dict: if 'results' in data_json: for serivce_attrs in data_json['results']: if 'attrs' in serivce_attrs and 'last_check_result' in serivce_attrs['attrs'] and 'performance_data' in \ - serivce_attrs['attrs']['last_check_result']: + serivce_attrs['attrs']['last_check_result'] and \ + serivce_attrs['attrs']['last_check_result']['performance_data'] is not None: check_command = serivce_attrs['attrs']['check_command'] # Get default labels labels = {'hostname': serivce_attrs['attrs']['host_name'], diff --git a/icinga2_exporter/proxy.py b/icinga2_exporter/proxy.py index e68fd8b..bbdbc3f 100644 --- a/icinga2_exporter/proxy.py +++ b/icinga2_exporter/proxy.py @@ -19,7 +19,7 @@ """ import asyncio -from quart import request, Response, jsonify, Quart +from quart import request, Response, jsonify, Quart, Blueprint from prometheus_client import (CONTENT_TYPE_LATEST, Counter) @@ -27,36 +27,16 @@ import icinga2_exporter.monitorconnection as monitorconnection import icinga2_exporter.log as log -app = Quart( __name__) +#app = Quart( __name__) +app = Blueprint( 'icinga2',__name__) total_requests = Counter('requests', 'Total requests to monitor-exporter endpoint') -loop = asyncio.new_event_loop() @app.route('/', methods=['GET']) def hello_world(): return 'monitor-exporter alive' - @app.route("/metrics", methods=['GET']) -def get_metrics(): - log.info(request.url) - target = request.args.get('target') - - log.info('Collect metrics', {'target': target}) - - monitor_data = Perfdata(monitorconnection.MonitorConfig(), target) - - # Fetch performance data from Monitor - monitor_data.get_perfdata() - - target_metrics = monitor_data.prometheus_format() - - resp = Response(target_metrics) - resp.headers['Content-Type'] = CONTENT_TYPE_LATEST - - return resp - -@app.route("/ametrics", methods=['GET']) async def get_ametrics(): log.info(request.url) target = request.args.get('target') @@ -66,7 +46,7 @@ async def get_ametrics(): monitor_data = Perfdata(monitorconnection.MonitorConfig(), target) # Fetch performance data from Monitor - await asyncio.get_event_loop().create_task(monitor_data.async_get_perfdata()) + await asyncio.get_event_loop().create_task(monitor_data.get_perfdata()) target_metrics = monitor_data.prometheus_format() diff --git a/requirements.txt b/requirements.txt index 31a4ff2..d1c6523 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,17 +1,38 @@ +aiofiles==0.4.0 +aiohttp==3.5.4 +async-timeout==3.0.1 +asyncio==3.4.3 +attrs==19.1.0 +blinker==1.4 certifi==2019.6.16 chardet==3.0.4 Click==7.0 coverage==4.5.4 -Flask==1.1.1 -Flask-Ext==0.1 +dataclasses==0.6 gunicorn==19.9.0 +h11==0.8.1 +h2==3.1.1 +hpack==3.0.0 +httptools==0.0.13 +Hypercorn==0.5.4 +hyperframe==5.2.0 idna==2.8 +idna-ssl==1.1.0 itsdangerous==1.1.0 Jinja2==2.10.1 MarkupSafe==1.1.1 +multidict==4.5.2 prometheus-client==0.7.1 python-json-logger==0.1.11 +pytoml==0.1.21 PyYAML==5.1.2 +Quart==0.6.14 requests==2.22.0 +sortedcontainers==2.1.0 +typing-extensions==3.7.4 urllib3==1.25.3 -Werkzeug==0.15.5 +uvicorn==0.8.6 +uvloop==0.12.2 +websockets==7.0 +wsproto==0.15.0 +yarl==1.3.0