├── pandoras_flask ├── __init__.py ├── app.py └── metrics.py ├── tests ├── __init__.py └── test_pandoras_flask.py ├── .gitignore ├── .circleci └── config.yml ├── bin ├── run_demo └── clear_prometheus_multiproc ├── tox.ini ├── html └── index.html ├── conf ├── uwsgi.ini └── nginx.conf ├── setup.py ├── Makefile ├── README.md └── LICENSE /pandoras_flask/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | .tox/ 3 | cover/ 4 | venv/ 5 | access.log 6 | *.egg-info/ 7 | *.pyc 8 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: hostedgraphite/pythonbuild:2venv 6 | steps: 7 | - checkout 8 | - run: make test 9 | -------------------------------------------------------------------------------- /bin/run_demo: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Start the "stack" - nginx + uwsgi running our Flask app. 3 | # We run `nginx` in the foreground and pull everything down after CTRL-C 4 | 5 | . venv/bin/activate 6 | 7 | uwsgi conf/uwsgi.ini & 8 | uwsgi_pid=$! 9 | nginx -c conf/nginx.conf -p ${PWD} 10 | kill ${uwsgi_pid} 11 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27 3 | 4 | [testenv] 5 | setenv = prometheus_multiproc_dir=/tmp 6 | commands = 7 | nosetests -s --with-coverage --cover-html --cover-branches --cover-package=pandoras_flask {posargs} 8 | flake8 pandoras_flask tests 9 | deps = 10 | coverage 11 | flake8 12 | nose 13 | -------------------------------------------------------------------------------- /pandoras_flask/app.py: -------------------------------------------------------------------------------- 1 | import flask 2 | import metrics 3 | 4 | ping_app = flask.Flask(__name__) 5 | metrics.setup(ping_app) 6 | 7 | 8 | @ping_app.route('/ping', methods=['GET']) 9 | def ping(): 10 | return 'Pong', 200 11 | 12 | 13 | # We expect to run under `uwsgi` etc. but just in case someone wants to run 14 | # this script directly, we provide a default 15 | if __name__ == '__main__': # pragma: no cover 16 | ping_app.run(debug=True, host='0.0.0.0', port=8040) 17 | -------------------------------------------------------------------------------- /html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Pandora's Flask 4 | 5 | 6 |

Pandora's Flask

7 |

A worked example of integrating Prometheus monitoring with a Flask app.

8 |

9 |

13 |

14 | 15 | 16 | -------------------------------------------------------------------------------- /conf/uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | socket = /var/tmp/uwsgi_pandoras_flask.sock 3 | stats = /var/tmp/uwsgi_pandoras_flask_stats.sock 4 | plugin = python 5 | virtualenv = ./venv 6 | module = pandoras_flask.app:ping_app 7 | workers = 4 8 | chmod-socket = 644 9 | master = true 10 | die-on-term = true 11 | enable-threads = true 12 | lazy-apps = true 13 | env = prometheus_multiproc_dir=/var/tmp/uwsgi_pandoras_flask_metrics 14 | exec-asap = bin/clear_prometheus_multiproc /var/tmp/uwsgi_pandoras_flask_metrics 15 | 16 | # In a real deployment, you might like to change the below to a dedicated 17 | # daemon user. 18 | # If you then run uwsgi as root, it will have full permissions for the 19 | # `exec-asap` hook, and then drop privs to the below. 20 | # uid = pandora 21 | # gid = pandora 22 | -------------------------------------------------------------------------------- /bin/clear_prometheus_multiproc: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Clear out a Prometheus Python client multiprocessing directory. 3 | # Intended to be run as a uWSGI hook when starting Flask servers. 4 | # 5 | # usage: clear_prometheus_multiproc [user] [group] [mode] 6 | # directory `path` will be removed and recreated with `mode` and 7 | # ownership `user:group`, by default mode `0755`, `${USER}:${USER}` 8 | # targeting the (fairly?) common Linux layout. 9 | # Parent directory is assumed to exist. 10 | set -ex 11 | 12 | path="${1:?'usage: clear_prometheus_multiproc [user] [group] [mode]'}" 13 | user="${2:-${USER}}" 14 | group="${3:-${USER}}" 15 | mode="${4:-755}" 16 | 17 | rm -Rf ${path} 18 | mkdir --mode ${mode} ${path} 19 | chown ${user}:${group} ${path} 20 | -------------------------------------------------------------------------------- /tests/test_pandoras_flask.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import unittest 5 | 6 | from pandoras_flask import app 7 | 8 | 9 | class TestPandorasFlask(unittest.TestCase): 10 | 11 | def setUp(self): 12 | self.client = app.ping_app.test_client() 13 | 14 | # Ensure metrics are initialized by making a call prior to tests. 15 | self.client.get('/ping') 16 | 17 | def tearDown(self): 18 | pass 19 | 20 | def test_ping(self): 21 | rp = self.client.get('/ping') 22 | self.assertEqual(rp.status_code, 200) 23 | self.assertEqual(rp.get_data(), 'Pong') 24 | 25 | def test_metrics(self): 26 | rp = self.client.get('/metrics') 27 | self.assertEqual(rp.status_code, 200) 28 | self.assertIn('request_count_total', rp.get_data()) 29 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | try: 4 | from setuptools import setup 5 | except ImportError: 6 | from distutils.core import setup 7 | 8 | requirements = [ 9 | 'prometheus_client==0.5.0', 10 | 'uwsgi==2.0.18', 11 | 'Flask==1.1.1', 12 | ] 13 | 14 | setup( 15 | name='pandoras_flask', 16 | version='1.0.1', 17 | description='Flask app with Prometheus monitoring', 18 | long_description='Worked example of integrating Prometheus monitoring with a Flask app', 19 | url='https://github.com/metricfire/pandoras_flask', 20 | author='Metricfire', 21 | author_email='maintainer@metricfire.com', 22 | install_requires=requirements, 23 | packages=['pandoras_flask'], 24 | test_suite='tests', 25 | include_package_data=True, 26 | zip_safe=False, 27 | keywords='flask prometheus example', 28 | ) 29 | -------------------------------------------------------------------------------- /conf/nginx.conf: -------------------------------------------------------------------------------- 1 | daemon off; 2 | worker_processes 1; 3 | error_log stderr; 4 | pid nginx.pid; 5 | 6 | events { 7 | worker_connections 1024; 8 | } 9 | 10 | http { 11 | access_log access.log; 12 | 13 | server { 14 | listen 8040; 15 | 16 | location = / { 17 | root html; 18 | } 19 | 20 | location /ping { try_files $uri @pandoras_flask; } 21 | 22 | location @pandoras_flask { 23 | include /etc/nginx/uwsgi_params; 24 | uwsgi_pass unix:/var/tmp/uwsgi_pandoras_flask.sock; 25 | } 26 | } 27 | 28 | server { 29 | listen 9040; 30 | 31 | location /metrics { try_files $uri @pandoras_flask; } 32 | location @pandoras_flask { 33 | include /etc/nginx/uwsgi_params; 34 | uwsgi_pass unix:/var/tmp/uwsgi_pandoras_flask.sock; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | help: 2 | @echo "run - run the Pandora's Flask example" 3 | @echo "test - run tests under tox" 4 | @echo "clean - remove all build & Python artifacts" 5 | @echo "distclean - clean up all artifacts for distribution" 6 | 7 | run: setup 8 | ( . venv/bin/activate && bin/run_demo ) 9 | 10 | test: 11 | tox 12 | 13 | clean: clean-build clean-pyc 14 | 15 | distclean: clean clean-test clean-run 16 | 17 | venv: 18 | virtualenv ./venv 19 | 20 | setup: venv 21 | ( . venv/bin/activate && pip install . ) 22 | 23 | clean-build: 24 | rm -fr deb_dist 25 | rm -fr build/ 26 | rm -fr dist/ 27 | rm -fr .eggs/ 28 | find . -name '*.egg-info' -exec rm -fr {} + 29 | find . -name '*.egg' -exec rm -f {} + 30 | 31 | clean-pyc: 32 | find . -name '*.pyc' -exec rm -f {} + 33 | find . -name '*.pyo' -exec rm -f {} + 34 | find . -name '*~' -exec rm -f {} + 35 | find . -name '__pycache__' -exec rm -fr {} + 36 | 37 | clean-test: 38 | rm -f .coverage 39 | rm -fr htmlcov/ cover/ 40 | 41 | clean-run: 42 | rm -f *.log 43 | rm -fr venv 44 | rm -fr .tox 45 | 46 | .PHONY: help run test clean distclean setup clean-build clean-pyc clean-test clean-run 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Pandora's Flask 2 | 3 | A worked example of integrating Prometheus monitoring with a Flask app running 4 | in the common `nginx` + `uwsgi` + `Flask` stack. 5 | 6 | See the [related blog post](https://www.hostedgraphite.com/blog). 7 | 8 | You'll need: 9 | * `nginx` for your platform (we used 1.14.0); 10 | * Python development libraries to build `uWSGI`; 11 | * [virtualenv](https://virtualenv.pypa.io/en/latest/) to build in. 12 | 13 | If you want to `make test`, you'll need 14 | [tox](https://tox.readthedocs.io/en/latest/) as well (we used 2.5.0). 15 | 16 | Then you should be able to get going with 17 | 18 | make run 19 | 20 | Which will start the app running under `nginx` + `uwsgi`. 21 | 22 | You can then visit http://localhost:8040/ and look around, or CTRL-C to bring 23 | everything down again. 24 | 25 | Once everything's running, you can point a test Prometheus at 26 | http://localhost:9040/metrics to scrape the monitoring data. 27 | 28 | Directory layout: 29 | * `bin/` - some helper scripts; 30 | * `conf/` - `nginx` and `uwsgi` configuration; 31 | * `html/` - a simple index for the app; 32 | * `pandoras_flask/` - Python app code; 33 | * `tests/` - simple tests. 34 | 35 | [Let us know](mailto:help@hostedgraphite.com) if you run into issues getting 36 | this demo going, or if you find problems in how we've set things up. Thanks! 37 | -------------------------------------------------------------------------------- /pandoras_flask/metrics.py: -------------------------------------------------------------------------------- 1 | import prometheus_client 2 | import time 3 | 4 | from flask import request, Response 5 | from prometheus_client import multiprocess, Counter, Histogram 6 | 7 | # Multiprocessing setup 8 | # Cf. https://github.com/prometheus/client_python#multiprocess-mode-gunicorn 9 | registry = prometheus_client.CollectorRegistry() 10 | multiprocess.MultiProcessCollector(registry) 11 | 12 | # Metrics for the `app` module - reads & writes 13 | 14 | PING_RESPONSE = Counter('ping_response', 15 | '/ping response codes', ['code']) 16 | 17 | # Middleware / setup 18 | # Following the Flask multiprocessing example in 19 | # https://github.com/amitsaha/python-prometheus-demo 20 | 21 | REQUEST_COUNT = Counter('request_count', 'App Request Count', 22 | ['method', 'endpoint', 'http_status']) 23 | 24 | REQUEST_LATENCY = Histogram('request_latency_seconds', 'Request Latency', 25 | ['endpoint']) 26 | 27 | 28 | def start_timer(): 29 | request.start_time = time.time() 30 | 31 | 32 | def stop_timer(response): 33 | resp_time = time.time() - request.start_time 34 | REQUEST_LATENCY.labels(request.path).observe(resp_time) 35 | return response 36 | 37 | 38 | def record_request_data(response): 39 | REQUEST_COUNT.labels(request.method, request.path, 40 | response.status_code).inc() 41 | return response 42 | 43 | 44 | # Cf. Prometheus exposition format in https://git.io/fpSOY 45 | CONTENT_TYPE_LATEST = str('text/plain; version=0.0.4; charset=utf-8') 46 | 47 | 48 | def setup(app): 49 | app.before_request(start_timer) 50 | 51 | # `after_request` functions are executed in the reverse of the order 52 | # they were added. We want `stop_timer` to be executed first. 53 | app.after_request(record_request_data) 54 | app.after_request(stop_timer) 55 | 56 | @app.route('/metrics') 57 | def metrics(): 58 | return Response(prometheus_client.generate_latest(registry), 59 | mimetype=CONTENT_TYPE_LATEST) 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Metricfire Limited. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | pandoras_flask/metrics.py includes portions of code by Amit Saha, licensed as 24 | follows: 25 | 26 | Copyright (c) 2017 Amit Saha 27 | 28 | Permission is hereby granted, free of charge, to any person obtaining a copy 29 | of this software and associated documentation files (the "Software"), to deal 30 | in the Software without restriction, including without limitation the rights 31 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 32 | copies of the Software, and to permit persons to whom the Software is 33 | furnished to do so, subject to the following conditions: 34 | 35 | The above copyright notice and this permission notice shall be included in all 36 | copies or substantial portions of the Software. 37 | 38 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 39 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 40 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 41 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 42 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 43 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 44 | SOFTWARE. 45 | --------------------------------------------------------------------------------