diff --git a/README.md b/README.md index 24d1a45..965b3b9 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ or paste it into requirements.txt: prometheus-flask-exporter # or with specific version number -prometheus-flask-exporter==0.20.1 +prometheus-flask-exporter==0.20.2 ``` and then install dependencies from requirements.txt file as usual: ``` @@ -275,6 +275,22 @@ metrics = PrometheusMetrics(app, metrics_decorator=auth.login_required) See a full example in the [examples/flask-httpauth](https://github.com/rycus86/prometheus_flask_exporter/tree/master/examples/flask-httpauth) folder. +## Custom metrics endpoint + +You can also take full control of the metrics endpoint by generating its contents, +and managing how it is exposed by yourself. + +```python +app = Flask(__name__) +# path=None to avoid registering a /metrics endpoint on the same Flask app +metrics = PrometheusMetrics(app, path=None) + +# later ... generate the response (and its content type) to expose to Prometheus +response_data, content_type = metrics.generate_metrics() +``` + +See the related conversation in [issue #135](https://github.com/rycus86/prometheus_flask_exporter/issues/135). + ## Debug mode Please note, that changes being live-reloaded, when running the Flask diff --git a/prometheus_flask_exporter/__init__.py b/prometheus_flask_exporter/__init__.py index ca711c2..d64285e 100644 --- a/prometheus_flask_exporter/__init__.py +++ b/prometheus_flask_exporter/__init__.py @@ -11,12 +11,13 @@ from flask import request, make_response, current_app from flask.views import MethodViewType from prometheus_client import Counter, Histogram, Gauge, Summary +from prometheus_client import multiprocess as pc_multiprocess, CollectorRegistry try: # prometheus-client >= 0.14.0 - from prometheus_client.exposition import choose_formatter + from prometheus_client.exposition import choose_encoder except ImportError: # prometheus-client < 0.14.0 - from prometheus_client.exposition import choose_encoder as choose_formatter + from prometheus_client.exposition import choose_formatter as choose_encoder from werkzeug.serving import is_running_from_reloader @@ -234,8 +235,8 @@ def init_app(self, app): This callback can be used to initialize an application for the use with this prometheus reporter setup. - This is usually used with a Flask "app factory" configuration. Please - see: http://flask.pocoo.org/docs/1.0/patterns/appfactories/ + This is usually used with a Flask "app factory" configuration. + Please see: http://flask.pocoo.org/docs/1.0/patterns/appfactories/ Note, that you need to use `PrometheusMetrics.for_app_factory()` for this mode, otherwise it is called automatically. @@ -270,23 +271,15 @@ def register_endpoint(self, path, app=None): @self.do_not_track() def prometheus_metrics(): - # import these here so they don't clash with our own multiprocess module - from prometheus_client import multiprocess, CollectorRegistry - - if 'PROMETHEUS_MULTIPROC_DIR' in os.environ or 'prometheus_multiproc_dir' in os.environ: - registry = CollectorRegistry() - else: - registry = self.registry - + accept_header = request.headers.get("Accept") if 'name[]' in request.args: - registry = registry.restricted_registry(request.args.getlist('name[]')) - - if 'PROMETHEUS_MULTIPROC_DIR' in os.environ or 'prometheus_multiproc_dir' in os.environ: - multiprocess.MultiProcessCollector(registry) + names = request.args.getlist('name[]') + else: + names = None - generate_latest, content_type = choose_formatter(request.headers.get("Accept")) + generated_data, content_type = self.generate_metrics(accept_header, names) headers = {'Content-Type': content_type} - return generate_latest(registry), 200, headers + return generated_data, 200, headers # apply any user supplied decorators, like authentication if self._metrics_decorator: @@ -295,6 +288,36 @@ def prometheus_metrics(): # apply the Flask route decorator on our metrics endpoint app.route(path)(prometheus_metrics) + def generate_metrics(self, accept_header=None, names=None): + """ + Generate the metrics output for Prometheus to consume. + This can be exposed on a dedicated server, or on the Flask app, or for + local development you can use the shorthand method to expose it on a + new Flask app, see `PrometheusMetrics.start_http_server()`. + + :param accept_header: The value of the HTTP Accept request header + (default `None`) + :param names: Names to only return samples for, must be a list of + strings if not `None` (default `None`) + :return: a tuple of response content and response content type + (both `str` types) + """ + + if 'PROMETHEUS_MULTIPROC_DIR' in os.environ or 'prometheus_multiproc_dir' in os.environ: + registry = CollectorRegistry() + else: + registry = self.registry + + if names: + registry = registry.restricted_registry(names) + + if 'PROMETHEUS_MULTIPROC_DIR' in os.environ or 'prometheus_multiproc_dir' in os.environ: + pc_multiprocess.MultiProcessCollector(registry) + + generate_latest, content_type = choose_encoder(accept_header) + generated_content = generate_latest(registry).decode('utf-8') + return generated_content, content_type + def start_http_server(self, port, host='0.0.0.0', endpoint='/metrics', ssl=None): """ Start an HTTP server for exposing the metrics. @@ -861,9 +884,9 @@ def info(self, name, description, labelnames=None, labelvalues=None, **labels): @staticmethod def _is_string(value): try: - return isinstance(value, basestring) # python2 - except NameError: return isinstance(value, str) # python3 + except NameError: + return isinstance(value, basestring) # python2 @staticmethod def _not_yet_handled(tracking_key): @@ -965,4 +988,4 @@ def _make_response(response): return _make_response -__version__ = '0.20.1' +__version__ = '0.20.2' diff --git a/prometheus_flask_exporter/multiprocess.py b/prometheus_flask_exporter/multiprocess.py index 89ca3c8..4a26708 100644 --- a/prometheus_flask_exporter/multiprocess.py +++ b/prometheus_flask_exporter/multiprocess.py @@ -63,7 +63,7 @@ def __init__(self, app=None, **kwargs): app=app, path=None, registry=registry, **kwargs ) - def start_http_server(self, port, host='0.0.0.0', endpoint=None): + def start_http_server(self, port, host='0.0.0.0', endpoint=None, ssl=None): """ Start an HTTP server for exposing the metrics, if the `should_start_http_server` function says we should, otherwise just return. @@ -72,6 +72,7 @@ def start_http_server(self, port, host='0.0.0.0', endpoint=None): :param port: the HTTP port to expose the metrics endpoint on :param host: the HTTP host to listen on (default: `0.0.0.0`) :param endpoint: **ignored**, the HTTP server will respond on any path + :param ssl: **ignored**, the server will not support SSL/HTTPS """ if self.should_start_http_server(): diff --git a/setup.py b/setup.py index 05795af..66a8285 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name='prometheus_flask_exporter', packages=['prometheus_flask_exporter'], - version='0.20.1', + version='0.20.2', description='Prometheus metrics exporter for Flask', long_description=long_description, long_description_content_type='text/markdown', @@ -14,7 +14,7 @@ author='Viktor Adam', author_email='rycus86@gmail.com', url='https://github.com/rycus86/prometheus_flask_exporter', - download_url='https://github.com/rycus86/prometheus_flask_exporter/archive/0.20.1.tar.gz', + download_url='https://github.com/rycus86/prometheus_flask_exporter/archive/0.20.2.tar.gz', keywords=['prometheus', 'flask', 'monitoring', 'exporter'], classifiers=[ 'Development Status :: 4 - Beta', diff --git a/tests/test_endpoint.py b/tests/test_endpoint.py index 1008427..8b5cff5 100644 --- a/tests/test_endpoint.py +++ b/tests/test_endpoint.py @@ -44,6 +44,42 @@ def test(): self.assertIn('flask_http_request_duration_seconds_count', str(response.data)) self.assertIn('flask_http_request_duration_seconds_sum', str(response.data)) + def test_generate_metrics_content(self): + metrics = self.metrics(path=None) + + @self.app.route('/test') + def test(): + return 'OK' + + self.client.get('/test') + + response = self.client.get('/metrics') + self.assertEqual(404, response.status_code) + + response_data, _ = metrics.generate_metrics() + + self.assertIn('flask_exporter_info', response_data) + self.assertIn('flask_http_request_total', response_data) + self.assertIn('flask_http_request_duration_seconds', response_data) + + response_data, _ = metrics.generate_metrics(names=['flask_exporter_info']) + + self.assertIn('flask_exporter_info', response_data) + self.assertNotIn('flask_http_request_total', response_data) + self.assertNotIn('flask_http_request_duration_seconds', response_data) + + response_data, _ = metrics.generate_metrics(names=[ + 'flask_http_request_duration_seconds_bucket', + 'flask_http_request_duration_seconds_count', + 'flask_http_request_duration_seconds_sum' + ]) + + self.assertNotIn('flask_exporter_info', response_data) + self.assertNotIn('flask_http_request_total', response_data) + self.assertIn('flask_http_request_duration_seconds_bucket', response_data) + self.assertIn('flask_http_request_duration_seconds_count', response_data) + self.assertIn('flask_http_request_duration_seconds_sum', response_data) + def test_http_server(self): metrics = self.metrics()