├── 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 |
10 | - Spam the /ping endpoint a bit,
11 | - and then take a look at /metrics.
12 |
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 |
--------------------------------------------------------------------------------