Skip to content

Commit

Permalink
Add support to generate metrics as text
Browse files Browse the repository at this point in the history
  • Loading branch information
rycus86 committed Jun 4, 2022
1 parent b5fc7da commit 3d29e04
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 25 deletions.
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
```
Expand Down Expand Up @@ -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
Expand Down
65 changes: 44 additions & 21 deletions prometheus_flask_exporter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand All @@ -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.
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -965,4 +988,4 @@ def _make_response(response):
return _make_response


__version__ = '0.20.1'
__version__ = '0.20.2'
3 changes: 2 additions & 1 deletion prometheus_flask_exporter/multiprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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():
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
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',
license='MIT',
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',
Expand Down
36 changes: 36 additions & 0 deletions tests/test_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down

0 comments on commit 3d29e04

Please sign in to comment.