├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── changelog.txt ├── examples └── app.py ├── flask_profiler ├── __init__.py ├── flask_profiler.py ├── static │ └── dist │ │ ├── css │ │ └── main.css │ │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ └── glyphicons-halflings-regular.woff │ │ ├── images │ │ └── python-flask.png │ │ ├── index.html │ │ └── js │ │ ├── main.js │ │ ├── settings.js │ │ └── vendor.js └── storage │ ├── __init__.py │ ├── base.py │ ├── mongo.py │ ├── sql_alchemy.py │ └── sqlite.py ├── requirements.txt ├── resources ├── dashboard_screen.png ├── filtering_all_screen.png ├── filtering_detail_screen.png └── filtering_method_screen.png ├── setup.py └── tests ├── __init__.py ├── basetest.py ├── module_accounts.py ├── test_endpoint_ignore.py ├── test_measure_endpoint.py └── test_measurement.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | !flask_profiler/static/dist/ 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | 60 | # IDE specific changes 61 | .idea/ 62 | 63 | # Generated files 64 | *.sql 65 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.7" 5 | - "3.4" 6 | 7 | 8 | env: 9 | - FLASK_PROFILER_TEST_CONF=sqlite 10 | - FLASK_PROFILER_TEST_CONF=mongodb 11 | - FLASK_PROFILER_TEST_CONF=sqlalchemy 12 | 13 | install: 14 | - pip install flask 15 | - pip install flask-testing 16 | - pip install pymongo 17 | - pip install coveralls 18 | - pip install sqlalchemy 19 | - pip install simplejson 20 | 21 | services: 22 | - mongodb 23 | 24 | script: python setup.py test -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Making Changes 2 | tl;dr: Contributors should follow the standard team development practices. 3 | 4 | * Fork the repository on GitHub. 5 | * Create a topic branch from where you want to base your work. 6 | * This is usually the master branch. 7 | * Please avoid working directly on master branch. 8 | * Make commits of logical units (if needed rebase your feature branch before submitting it). 9 | * Check for unnecessary whitespace with git diff --check before committing. 10 | * Make sure your commit messages are in the proper format. 11 | * If your commit fixes an open issue, reference it in the commit message (#15). 12 | * Make sure your code comforms to [PEP8](https://www.python.org/dev/peps/pep-0008/). 13 | * Make sure you have added the necessary tests for your changes. 14 | * Run all the tests to assure nothing else was accidentally broken. 15 | 16 | 17 | It is highly encouraged to follow this link and understand git branching model explained in it: http://nvie.com/posts/a-successful-git-branching-model 18 | 19 | ## Submitting Changes 20 | 21 | * Push your changes to a topic branch in your fork of the repository. 22 | * Submit a Pull Request. 23 | * Wait for maintainer feedback. 24 | 25 | 26 | ## Dont' know where to start? 27 | There are usually several TODO comments scattered around the codebase, maybe 28 | check them out and see if you have ideas, or can help with them. Also, check 29 | the [open issues](https://github.com/muatik/flask-profiler/issues) in case there's something that sparks your interest. What 30 | about documentation? I suck at english so if you're fluent with it (or notice 31 | any error), why not help with that? 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Mustafa Atik 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | include *.md 3 | include setup.py 4 | include LICENSE 5 | graft flask_profiler/static/ 6 | recursive-include flask_profiler/static * 7 | global-exclude *.pyc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask-profiler 2 | 3 | 4 | **version: 1.8** [![Build Status](https://travis-ci.org/muatik/flask-profiler.svg?branch=master)](https://travis-ci.org/muatik/flask-profiler) 5 | 6 | ##### Flask-profiler measures endpoints defined in your flask application; and provides you fine-grained report through a web interface. 7 | 8 | It gives answers to these questions: 9 | * Where are the bottlenecks in my application? 10 | * Which endpoints are the slowest in my application? 11 | * Which are the most frequently called endpoints? 12 | * What causes my slow endpoints? In which context, with what args and kwargs are they slow? 13 | * How much time did a specific request take? 14 | 15 | In short, if you are curious about what your endpoints are doing and what requests they are receiving, give a try to flask-profiler. 16 | 17 | With flask-profiler's web interface, you can monitor all your endpoints' performance and investigate endpoints and received requests by drilling down through filters. 18 | 19 | ## Screenshots 20 | 21 | Dashboard view displays a summary. 22 | 23 | ![Alt text](resources/dashboard_screen.png?raw=true "Dashboard view") 24 | 25 | You can create filters to investigate certain type requests. 26 | 27 | ![Alt text](resources/filtering_all_screen.png?raw=true "Filtering by endpoint") 28 | 29 | ![Alt text](resources/filtering_method_screen.png?raw=true "Filtering by method") 30 | 31 | You can see all the details of a request. 32 | ![Alt text](resources/filtering_detail_screen.png?raw=true "Request detail") 33 | 34 | ## Quick Start 35 | It is easy to understand flask-profiler going through an example. Let's dive in. 36 | 37 | Install flask-profiler by pip. 38 | ```sh 39 | pip install flask_profiler 40 | ``` 41 | 42 | 43 | Edit your code where you are creating Flask app. 44 | ```python 45 | # your app.py 46 | from flask import Flask 47 | import flask_profiler 48 | 49 | app = Flask(__name__) 50 | app.config["DEBUG"] = True 51 | 52 | # You need to declare necessary configuration to initialize 53 | # flask-profiler as follows: 54 | app.config["flask_profiler"] = { 55 | "enabled": app.config["DEBUG"], 56 | "storage": { 57 | "engine": "sqlite" 58 | }, 59 | "basicAuth":{ 60 | "enabled": True, 61 | "username": "admin", 62 | "password": "admin" 63 | }, 64 | "ignore": [ 65 | "^/static/.*" 66 | ] 67 | } 68 | 69 | 70 | @app.route('/product/', methods=['GET']) 71 | def getProduct(id): 72 | return "product id is " + str(id) 73 | 74 | 75 | @app.route('/product/', methods=['PUT']) 76 | def updateProduct(id): 77 | return "product {} is being updated".format(id) 78 | 79 | 80 | @app.route('/products', methods=['GET']) 81 | def listProducts(): 82 | return "suppose I send you product list..." 83 | 84 | @app.route('/static/something/', methods=['GET']) 85 | def staticSomething(): 86 | return "this should not be tracked..." 87 | 88 | # In order to active flask-profiler, you have to pass flask 89 | # app as an argument to flask-profiler. 90 | # All the endpoints declared so far will be tracked by flask-profiler. 91 | flask_profiler.init_app(app) 92 | 93 | 94 | # endpoint declarations after flask_profiler.init_app() will be 95 | # hidden to flask_profiler. 96 | @app.route('/doSomething', methods=['GET']) 97 | def doSomething(): 98 | return "flask-profiler will not measure this." 99 | 100 | 101 | # But in case you want an endpoint to be measured by flask-profiler, 102 | # you can specify this explicitly by using profile() decorator 103 | @app.route('/doSomethingImportant', methods=['GET']) 104 | @flask_profiler.profile() 105 | def doSomethingImportant(): 106 | return "flask-profiler will measure this request." 107 | 108 | if __name__ == '__main__': 109 | app.run(host="127.0.0.1", port=5000) 110 | 111 | 112 | ``` 113 | 114 | Now run your `app.py` 115 | ``` 116 | python app.py 117 | ``` 118 | 119 | And make some requests like: 120 | ```sh 121 | curl http://127.0.0.1:5000/products 122 | curl http://127.0.0.1:5000/product/123 123 | curl -X PUT -d arg1=val1 http://127.0.0.1:5000/product/123 124 | ``` 125 | 126 | If everything is okay, Flask-profiler will measure these requests. You can see the result heading to http://127.0.0.1:5000/flask-profiler/ or get results as JSON http://127.0.0.1:5000/flask-profiler/api/measurements?sort=elapsed,desc 127 | 128 | If you like to initialize your extensions in other files or use factory apps pattern, you can also create a instance of the `Profiler` class, this will register all your endpoints once you app run by first time. E.g: 129 | 130 | ```python 131 | from flask import Flask 132 | from flask_profiler import Profiler 133 | 134 | profiler = Profiler() 135 | 136 | app = Flask(__name__) 137 | 138 | app.config["DEBUG"] = True 139 | 140 | # You need to declare necessary configuration to initialize 141 | # flask-profiler as follows: 142 | app.config["flask_profiler"] = { 143 | "enabled": app.config["DEBUG"], 144 | "storage": { 145 | "engine": "sqlite" 146 | }, 147 | "basicAuth":{ 148 | "enabled": True, 149 | "username": "admin", 150 | "password": "admin" 151 | }, 152 | "ignore": [ 153 | "^/static/.*" 154 | ] 155 | } 156 | 157 | profiler = Profiler() # You can have this in another module 158 | profiler.init_app(app) 159 | # Or just Profiler(app) 160 | 161 | @app.route('/product/', methods=['GET']) 162 | def getProduct(id): 163 | return "product id is " + str(id) 164 | 165 | ``` 166 | 167 | ## Using with different database system 168 | You can use flaskprofiler with **SqlLite**, **MongoDB**, **Postgresql**, **Mysql** or **MongoDB** database systems. However, it is easy to support other database systems. If you would like to have others, please go to contribution documentation. (It is really easy.) 169 | 170 | ### SQLite 171 | In order to use SQLite, just specify it as the value of `storage.engine` directive as follows. 172 | 173 | ```json 174 | app.config["flask_profiler"] = { 175 | "storage": { 176 | "engine": "sqlite", 177 | } 178 | } 179 | ``` 180 | 181 | Below the other options are listed. 182 | 183 | | Filter key | Description | Default | 184 | |----------|-------------|------| 185 | | storage.FILE | SQLite database file name | flask_profiler.sql| 186 | | storage.TABLE | table name in which profiling data will reside | measurements | 187 | 188 | ### MongoDB 189 | In order to use MongoDB, just specify it as the value of `storage.engine` directive as follows. 190 | 191 | ```json 192 | app.config["flask_profiler"] = { 193 | "storage": { 194 | "engine": "mongodb", 195 | } 196 | } 197 | ``` 198 | 199 | ### SQLAchemy 200 | In order to use SQLAchemy, just specify it as the value of `storage.engine` directive as follows. 201 | Also first create an empty database with the name "flask_profiler". 202 | 203 | ```python 204 | app.config["flask_profiler"] = { 205 | "storage": { 206 | "engine": "sqlalchemy", 207 | "db_url": "postgresql://user:pass@localhost:5432/flask_profiler" # optional, if no db_url specified then sqlite will be used. 208 | } 209 | } 210 | ``` 211 | 212 | ### Custom database engine 213 | Specify engine as string module and class path. 214 | 215 | ```json 216 | app.config["flask_profiler"] = { 217 | "storage": { 218 | "engine": "custom.project.flask_profiler.mysql.MysqlStorage", 219 | "MYSQL": "mysql://user:password@localhost/flask_profiler" 220 | } 221 | } 222 | ``` 223 | 224 | The other options are listed below. 225 | 226 | | Filter key | Description | Default 227 | |----------|-------------|------ 228 | | storage.MONGO_URL | mongodb connection string | mongodb://localhost 229 | | storage.DATABASE | database name | flask_profiler 230 | | storage.COLLECTION | collection name | measurements 231 | 232 | ### Sampling 233 | Control the number of samples taken by flask-profiler 234 | 235 | You would want control over how many times should the flask profiler take samples while running in production mode. 236 | You can supply a function and control the sampling according to your business logic. 237 | 238 | Example 1: Sample 1 in 100 times with random numbers 239 | ```python 240 | app.config["flask_profiler"] = { 241 | "sampling_function": lambda: True if random.sample(list(range(1, 101)), 1) == [42] else False 242 | } 243 | ``` 244 | 245 | Example 2: Sample for specific users 246 | ```python 247 | app.config["flask_profiler"] = { 248 | "sampling_function": lambda: True if user is 'divyendu' else False 249 | } 250 | ``` 251 | 252 | If sampling function is not present, all requests will be sampled. 253 | 254 | ### Changing flask-profiler endpoint root 255 | By default, we can access flask-profiler at /flask-profiler 256 | 257 | ```python 258 | app.config["flask_profiler"] = { 259 | "endpointRoot": "secret-flask-profiler" 260 | } 261 | ``` 262 | 263 | ### Ignored endpoints 264 | Flask-profiler will try to track every endpoint defined so far when init_app() is invoked. If you want to exclude some of the endpoints, you can define matching regex for them as follows: 265 | 266 | ```python 267 | app.config["flask_profiler"] = { 268 | "ignore": [ 269 | "^/static/.*", 270 | "/api/users/\w+/password" 271 | ] 272 | } 273 | ``` 274 | 275 | 276 | ## Contributing 277 | 278 | Contributions are welcome! 279 | 280 | Review the [Contributing Guidelines](https://github.com/muatik/flask-profiler/wiki/Development) for details on how to: 281 | 282 | * Submit issues 283 | * Add solutions to existing challenges 284 | * Add new challenges 285 | 286 | ## Authors 287 | * [Musafa Atik](https://www.linkedin.com/in/muatik) 288 | * Fatih Sucu 289 | * [Safa Yasin Yildirim](https://www.linkedin.com/in/safayasinyildirim) 290 | 291 | ## License 292 | MIT 293 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | version 1.8.1 2 | ==================== 3 | - add sqlalchemy to to test requirements 4 | 5 | version 1.6 6 | ==================== 7 | - sqlalchemy support -------------------------------------------------------------------------------- /examples/app.py: -------------------------------------------------------------------------------- 1 | # your app.py 2 | from flask import Flask 3 | import flask_profiler 4 | 5 | app = Flask(__name__) 6 | app.config["DEBUG"] = True 7 | 8 | # You need to declare necessary configuration to initialize 9 | # flask-profiler as follows: 10 | app.config["flask_profiler"] = { 11 | "verbose": True, 12 | "enabled": app.config["DEBUG"], 13 | "storage": { 14 | "engine": "sqlalchmey", 15 | "db_url": "sqlite:///flask_profiler.sql" # optional 16 | }, 17 | "basicAuth":{ 18 | "enabled": True, 19 | "username": "admin", 20 | "password": "admin" 21 | }, 22 | "ignore": [ 23 | "/static/*", 24 | "/secrets/password/" 25 | ] 26 | } 27 | 28 | 29 | @app.route('/product/', methods=['GET']) 30 | def getProduct(id): 31 | return "product id is " + str(id) 32 | 33 | 34 | @app.route('/product/', methods=['PUT']) 35 | def updateProduct(id): 36 | return "product {} is being updated".format(id) 37 | 38 | 39 | @app.route('/products', methods=['GET']) 40 | def listProducts(): 41 | return "suppose I send you product list..." 42 | 43 | 44 | @app.route('/static/photo/', methods=['GET']) 45 | def getPhoto(): 46 | return "your photo" 47 | 48 | 49 | # In order to active flask-profiler, you have to pass flask 50 | # app as an argument to flask-provider. 51 | # All the endpoints declared so far will be tracked by flask-provider. 52 | flask_profiler.init_app(app) 53 | 54 | 55 | # endpoint declarations after flask_profiler.init_app() will be 56 | # hidden to flask_profider. 57 | @app.route('/doSomething', methods=['GET']) 58 | def doSomething(): 59 | return "flask-provider will not measure this." 60 | 61 | 62 | # But in case you want an endpoint to be measured by flask-provider, 63 | # you can specify this explicitly by using profile() decorator 64 | @app.route('/doSomething', methods=['GET']) 65 | @flask_profiler.profile() 66 | def doSomethingImportant(): 67 | return "flask-provider will measure this request." 68 | 69 | if __name__ == '__main__': 70 | app.run(host="127.0.0.1", port=5000) 71 | -------------------------------------------------------------------------------- /flask_profiler/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | from .flask_profiler import ( 3 | measure, 4 | profile, 5 | init_app, 6 | Profiler) 7 | -------------------------------------------------------------------------------- /flask_profiler/flask_profiler.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | 3 | import functools 4 | import re 5 | import time 6 | 7 | from pprint import pprint as pp 8 | 9 | import logging 10 | 11 | from flask import Blueprint 12 | from flask import jsonify 13 | from flask import request 14 | from flask_httpauth import HTTPBasicAuth 15 | 16 | from . import storage 17 | 18 | CONF = {} 19 | collection = None 20 | auth = HTTPBasicAuth() 21 | 22 | logger = logging.getLogger("flask-profiler") 23 | 24 | _is_initialized = lambda: True if CONF else False 25 | 26 | 27 | @auth.verify_password 28 | def verify_password(username, password): 29 | if "basicAuth" not in CONF or not CONF["basicAuth"]["enabled"]: 30 | return True 31 | 32 | c = CONF["basicAuth"] 33 | if username == c["username"] and password == c["password"]: 34 | return True 35 | logging.warn("flask-profiler authentication failed") 36 | return False 37 | 38 | 39 | class Measurement(object): 40 | """represents an endpoint measurement""" 41 | DECIMAL_PLACES = 6 42 | 43 | def __init__(self, name, args, kwargs, method, context=None): 44 | super(Measurement, self).__init__() 45 | self.context = context 46 | self.name = name 47 | self.method = method 48 | self.args = args 49 | self.kwargs = kwargs 50 | self.startedAt = 0 51 | self.endedAt = 0 52 | self.elapsed = 0 53 | 54 | def __json__(self): 55 | return { 56 | "name": self.name, 57 | "args": self.args, 58 | "kwargs": self.kwargs, 59 | "method": self.method, 60 | "startedAt": self.startedAt, 61 | "endedAt": self.endedAt, 62 | "elapsed": self.elapsed, 63 | "context": self.context 64 | } 65 | 66 | def __str__(self): 67 | return str(self.__json__()) 68 | 69 | def start(self): 70 | # we use default_timer to get the best clock available. 71 | # see: http://stackoverflow.com/a/25823885/672798 72 | self.startedAt = time.time() 73 | 74 | def stop(self): 75 | self.endedAt = time.time() 76 | self.elapsed = round( 77 | self.endedAt - self.startedAt, self.DECIMAL_PLACES) 78 | 79 | 80 | def is_ignored(name, conf): 81 | ignore_patterns = conf.get("ignore", []) 82 | for pattern in ignore_patterns: 83 | if re.search(pattern, name): 84 | return True 85 | return False 86 | 87 | 88 | def measure(f, name, method, context=None): 89 | logger.debug("{0} is being processed.".format(name)) 90 | if is_ignored(name, CONF): 91 | logger.debug("{0} is ignored.".format(name)) 92 | return f 93 | 94 | @functools.wraps(f) 95 | def wrapper(*args, **kwargs): 96 | if 'sampling_function' in CONF and not callable(CONF['sampling_function']): 97 | raise Exception( 98 | "if sampling_function is provided to flask-profiler via config, " 99 | "it must be callable, refer to: " 100 | "https://github.com/muatik/flask-profiler#sampling") 101 | 102 | if 'sampling_function' in CONF and not CONF['sampling_function'](): 103 | return f(*args, **kwargs) 104 | 105 | measurement = Measurement(name, args, kwargs, method, context) 106 | measurement.start() 107 | 108 | try: 109 | returnVal = f(*args, **kwargs) 110 | except: 111 | raise 112 | finally: 113 | measurement.stop() 114 | if CONF.get("verbose", False): 115 | pp(measurement.__json__()) 116 | collection.insert(measurement.__json__()) 117 | 118 | return returnVal 119 | 120 | return wrapper 121 | 122 | 123 | def wrapHttpEndpoint(f): 124 | @functools.wraps(f) 125 | def wrapper(*args, **kwargs): 126 | context = { 127 | "url": request.base_url, 128 | "args": dict(request.args.items()), 129 | "form": dict(request.form.items()), 130 | "body": request.data.decode("utf-8", "strict"), 131 | "headers": dict(request.headers.items()), 132 | "func": request.endpoint, 133 | "ip": request.remote_addr 134 | } 135 | endpoint_name = str(request.url_rule) 136 | wrapped = measure(f, endpoint_name, request.method, context) 137 | return wrapped(*args, **kwargs) 138 | 139 | return wrapper 140 | 141 | 142 | def wrapAppEndpoints(app): 143 | """ 144 | wraps all endpoints defined in the given flask app to measure how long time 145 | each endpoints takes while being executed. This wrapping process is 146 | supposed not to change endpoint behaviour. 147 | :param app: Flask application instance 148 | :return: 149 | """ 150 | for endpoint, func in app.view_functions.items(): 151 | app.view_functions[endpoint] = wrapHttpEndpoint(func) 152 | 153 | 154 | def profile(*args, **kwargs): 155 | """ 156 | http endpoint decorator 157 | """ 158 | if _is_initialized(): 159 | def wrapper(f): 160 | return wrapHttpEndpoint(f) 161 | 162 | return wrapper 163 | raise Exception( 164 | "before measuring anything, you need to call init_app()") 165 | 166 | 167 | def registerInternalRouters(app): 168 | """ 169 | These are the endpoints which are used to display measurements in the 170 | flask-profiler dashboard. 171 | 172 | Note: these should be defined after wrapping user defined endpoints 173 | via wrapAppEndpoints() 174 | :param app: Flask application instance 175 | :return: 176 | """ 177 | urlPath = CONF.get("endpointRoot", "flask-profiler") 178 | 179 | fp = Blueprint( 180 | 'flask-profiler', __name__, 181 | url_prefix="/" + urlPath, 182 | static_folder="static/dist/", static_url_path='/static/dist') 183 | 184 | @fp.route("/".format(urlPath)) 185 | @auth.login_required 186 | def index(): 187 | return fp.send_static_file("index.html") 188 | 189 | @fp.route("/api/measurements/".format(urlPath)) 190 | @auth.login_required 191 | def filterMeasurements(): 192 | args = dict(request.args.items()) 193 | measurements = collection.filter(args) 194 | return jsonify({"measurements": list(measurements)}) 195 | 196 | @fp.route("/api/measurements/grouped".format(urlPath)) 197 | @auth.login_required 198 | def getMeasurementsSummary(): 199 | args = dict(request.args.items()) 200 | measurements = collection.getSummary(args) 201 | return jsonify({"measurements": list(measurements)}) 202 | 203 | @fp.route("/api/measurements/".format(urlPath)) 204 | @auth.login_required 205 | def getContext(measurementId): 206 | return jsonify(collection.get(measurementId)) 207 | 208 | @fp.route("/api/measurements/timeseries/".format(urlPath)) 209 | @auth.login_required 210 | def getRequestsTimeseries(): 211 | args = dict(request.args.items()) 212 | return jsonify({"series": collection.getTimeseries(args)}) 213 | 214 | @fp.route("/api/measurements/methodDistribution/".format(urlPath)) 215 | @auth.login_required 216 | def getMethodDistribution(): 217 | args = dict(request.args.items()) 218 | return jsonify({ 219 | "distribution": collection.getMethodDistribution(args)}) 220 | 221 | @fp.route("/db/dumpDatabase") 222 | @auth.login_required 223 | def dumpDatabase(): 224 | response = jsonify({ 225 | "summary": collection.getSummary()}) 226 | response.headers["Content-Disposition"] = "attachment; filename=dump.json" 227 | return response 228 | 229 | @fp.route("/db/deleteDatabase") 230 | @auth.login_required 231 | def deleteDatabase(): 232 | response = jsonify({ 233 | "status": collection.truncate()}) 234 | return response 235 | 236 | @fp.after_request 237 | def x_robots_tag_header(response): 238 | response.headers['X-Robots-Tag'] = 'noindex, nofollow' 239 | return response 240 | 241 | app.register_blueprint(fp) 242 | 243 | 244 | def init_app(app): 245 | global collection, CONF 246 | 247 | try: 248 | CONF = app.config["flask_profiler"] 249 | except: 250 | try: 251 | CONF = app.config["FLASK_PROFILER"] 252 | except: 253 | raise Exception( 254 | "to init flask-profiler, provide " 255 | "required config through flask app's config. please refer: " 256 | "https://github.com/muatik/flask-profiler") 257 | 258 | if not CONF.get("enabled", False): 259 | return 260 | 261 | collection = storage.getCollection(CONF.get("storage", {})) 262 | 263 | wrapAppEndpoints(app) 264 | registerInternalRouters(app) 265 | 266 | basicAuth = CONF.get("basicAuth", None) 267 | if not basicAuth or not basicAuth["enabled"]: 268 | logging.warn(" * CAUTION: flask-profiler is working without basic auth!") 269 | 270 | 271 | class Profiler(object): 272 | """ Wrapper for extension. """ 273 | 274 | def __init__(self, app=None): 275 | self._init_app = init_app 276 | if app is not None: 277 | self.init_app(app) 278 | 279 | def init_app(self, app): 280 | init = functools.partial(self._init_app, app) 281 | app.before_first_request(init) 282 | -------------------------------------------------------------------------------- /flask_profiler/static/dist/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muatik/flask-profiler/6a36526184ad3a903c2d50b3e208ed276d3404f6/flask_profiler/static/dist/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /flask_profiler/static/dist/fonts/glyphicons-halflings-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | -------------------------------------------------------------------------------- /flask_profiler/static/dist/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muatik/flask-profiler/6a36526184ad3a903c2d50b3e208ed276d3404f6/flask_profiler/static/dist/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /flask_profiler/static/dist/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muatik/flask-profiler/6a36526184ad3a903c2d50b3e208ed276d3404f6/flask_profiler/static/dist/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /flask_profiler/static/dist/images/python-flask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muatik/flask-profiler/6a36526184ad3a903c2d50b3e208ed276d3404f6/flask_profiler/static/dist/images/python-flask.png -------------------------------------------------------------------------------- /flask_profiler/static/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 35 |
36 |
37 |
38 |
39 | 40 |
41 |
42 |
43 |

Dashboard

44 | 45 |
46 | 48 | 50 | 52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |

Method distribution

60 |
61 |
62 |
63 |

Request count by time

64 |
65 |
66 |
67 |
68 |
69 | 70 |
71 |
72 |
73 | 74 |
75 |
76 |
77 |
78 | 79 |
80 |
81 |
82 |

Filtering

83 | 84 |
85 |
86 |
87 | 88 | 89 | 90 | 110 | 111 | 112 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 |
91 | 109 | 113 | 114 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |

Settings

133 |
134 |
135 | 138 | 141 | 142 |
143 | 144 |
145 |
146 |
147 | 150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 | 159 |
160 | 161 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | -------------------------------------------------------------------------------- /flask_profiler/static/dist/js/main.js: -------------------------------------------------------------------------------- 1 | var profile={config:{dataLength:0},columnsIndex:{grouped:["method","name","count","avgElapsed","maxElapsed","minElapsed"],filter:["method","name","elapsed","startedAt"]},getData:function(a,b){a=a||"grouped";var c,d=this,b=this.createQueryParams(a);return $.ajax({type:"GET",async:!1,url:"api/measurements/"+("grouped"===a?a+"/":""),dataType:"json",data:b,success:function(a){c=d.dataTableClassifier(a.measurements),d.createdTime=moment()}}),c},dataTableClassifier:function(a){var b=this.dataTableOption;ajaxData=a.measurements||a;var c=Object.keys(ajaxData).length;return c=b.length===c?b.length+b.start+c:b.start+c,{draw:b.draw,recordsFiltered:c,recordsTotal:c,data:ajaxData}},createQueryParams:function(a,b){var c,d=b||this.dataTableOption,e=d.order[0],f={};if("filtered"===a){var g=$("#filteredTable select.method").val();c=this.columnsIndex.filter,"ALL"===g&&(g=""),f.method=g,f.name=$("#filteredTable input.filtered-name").val(),f.elapsed=$("#filteredTable input.elapsed").val()||0}else c=this.columnsIndex.grouped;return f.sort=c[e.column]+","+e.dir,f.skip=d.start,f.limit=d.length,f.startedAt=this.dateTime.startedAt,f.endedAt=this.dateTime.endedAt,f}};window.profile=profile,window.dayFilterValue="day",window.profile.dateTime={startedAt:moment().subtract(6,"days").unix(),endedAt:moment().unix()};var setFilteredTable=function(){var a=$("#filteredTable").DataTable({processing:!0,serverSide:!0,ajax:function(a,b,c){window.profile.dataTableOption=a,b(window.profile.getData("filtered"))},responsive:!1,paging:!0,pageLength:100,dom:"Btrtip",stateSave:!0,order:[3,"desc"],autoWidth:!1,language:{processing:"Loading...",buttons:{colvis:''}},buttons:[{extend:"colvis",columns:[":gt(1)"]}],columns:[{title:"method",data:function(a){return''+a.method+""},"class":"method",orderable:!1},{title:"name",data:function(a,b){var c=document.createElement("div");return c.innerText=a.name,"display"===b?""+c.innerHTML+"":c.innerHTML},"class":"name",orderable:!1},{title:"elapsed",data:function(a){return a.elapsed.toString().slice(0,8)},"class":"elapsed number"},{title:"startedAt",data:function(a){return moment.unix(a.startedAt).format("DD/MM/YYYY h:mm:ss.MS A")},"class":"startedAt"}],initComplete:function(){$("#filteredTable>thead").append($("#filteredTable .filter-row")),$(".filtered-datepicker").daterangepicker({timePicker:!0,timePickerSeconds:!0,startDate:moment.unix(window.profile.dateTime.startedAt).format("MM/DD/YYYY"),endDate:moment.unix(window.profile.dateTime.endedAt).format("MM/DD/YYYY")},function(b,c,d){profile.dateTime={startedAt:b.unix(),endedAt:c.unix()},a.draw()}),$("#filteredTable").removeClass("loading")},drawCallback:function(){$("#filteredTable tbody").on("click","tr",function(){$(".filteredModal .modal-body").JSONView(JSON.stringify($(this).find("[data-json]").data("json"))),$(".filteredModal").modal("show")}),$("#filteredTable").removeClass("loading"),$("html").animate({scrollTop:0},300)}});$("#filteredTable select.method, #filteredTable input.filtered-name, #filteredTable input.elapsed").off().on("input",function(){$("#filteredTable").addClass("loading"),a.draw()}),$(".clear-filter").off().on("click",function(){var b=$(".filtered-datepicker");$("#filteredTable select.method").val("ALL"),$("#filteredTable input.filtered-name").val(""),$("#filteredTable input.elapsed").val(""),b.data("daterangepicker").setStartDate(moment().subtract(7,"days").format("MM/DD/YYYY")),b.data("daterangepicker").setEndDate(moment().format("MM/DD/YYYY")),a.draw()})},getCharts=function(){$.ajax({type:"GET",async:!0,url:"api/measurements/methodDistribution/",dataType:"json",data:{startedAt:window.profile.dateTime.startedAt,endedAt:window.profile.dateTime.endedAt},success:function(a){var b=a.distribution,c=[];for(key in b)b.hasOwnProperty(key)&&c.push([key,b[key]]);c3.generate({bindto:"#pieChart",data:{columns:c,type:"pie",colors:{GET:"#4BB74B",PUT:"#0C8DFB",DELETE:"#FB6464",POST:"#2758E4"}},tooltip:{format:{value:function(a,b,c){return a}}},color:{pattern:["#9A9A9A"]}});$("#pieChart").removeClass("loading")}}),$.ajax({type:"GET",async:!0,url:"api/measurements/timeseries/",dataType:"json",data:{interval:"hours"!==window.dayFilterValue?"daily":"hourly",startedAt:window.profile.dateTime.startedAt,endedAt:window.profile.dateTime.endedAt},success:function(a){var b=a.series,c=["data"],d=[];for(var e in b)c.push(b[e]);if("hours"===window.dayFilterValue)for(var e in Object.keys(b))d.push(Object.keys(b)[e].substr(-2,2));else d=Object.keys(b);c3.generate({bindto:"#lineChart",data:{columns:[c],type:"area"},axis:{x:{type:"category",categories:d}},legend:{show:!1},color:{pattern:["#EC5B19"]}});$("#lineChart").removeClass("loading")}})};$(document).ready(function(){$('a[data-toggle="tab"]').historyTabs(),$("#big-users-table").on("preXhr.dt",function(a,b,c){window.profile.dataTableOption=c;var d=profile.createQueryParams("grouped",c);for(key in d)d.hasOwnProperty(key)&&(c[key]=d[key])});var a=$("#big-users-table").DataTable({processing:!0,serverSide:!0,ajax:{url:"api/measurements/grouped",dataSrc:function(a){var b=profile.dataTableClassifier(a.measurements);return b.data}},responsive:!1,paging:!1,pageLength:1e4,dom:"Btrtip",stateSave:!0,autoWidth:!1,order:[2,"desc"],language:{processing:"Loading...",buttons:{colvis:''}},buttons:[{extend:"colvis",columns:[":gt(1)"]}],columns:[{title:"method",data:function(a){return''+a.method+""},"class":"method",orderable:!1},{title:"name",data:function(a){var b=document.createElement("div");return b.innerText=a.name,b.innerHTML},"class":"name",orderable:!1},{title:"count",data:"count","class":"number"},{title:"avg elapsed",data:function(a){return a.avgElapsed.toString().slice(0,8)},"class":"number"},{title:"max elapsed",data:function(a){return a.maxElapsed.toString().slice(0,8)},"class":"number"},{title:"min elapsed",data:function(a){return a.minElapsed.toString().slice(0,8)},"class":"number"}],drawCallback:function(){$("#big-users-table tbody tr").off().on("click",function(a){if("A"!==$(a.target).prop("tagName")){var b=$(".filtered-datepicker");$("#filteredTable .filter-row .filtered-name").val($(this).find("td.name").text()).trigger("input"),$("#filteredTable .filter-row .method").val($(this).find(".method .row--method").text()).trigger("input"),"object"==typeof b.data("daterangepicker")&&(b.data("daterangepicker").setStartDate(moment.unix(window.profile.dateTime.startedAt).format("MM/DD/YYYY")),b.data("daterangepicker").setEndDate(moment.unix(window.profile.dateTime.endedAt).format("MM/DD/YYYY"))),setFilteredTable(),$('[data-target="#tab-filtering"]').tab("show")}})},initComplete:function(){}});$(document).on("popstate",function(a){console.log(a)}),$('[data-target="#tab-filtering"]').on("show.bs.tab",function(){setFilteredTable()}),$(".day-filter label").on("click",function(b){$("#lineChart, #pieChart").addClass("loading");var c,d=$(this).find("input").val();window.dayFilterValue!==d,window.dayFilterValue=$(this).find("input").val(),c="min"===window.dayFilterValue?{startedAt:moment().subtract(1,"hours").unix(),endedAt:moment().unix()}:"hours"===window.dayFilterValue?{startedAt:moment().subtract(24,"hours").unix(),endedAt:moment().unix()}:"days"===window.dayFilterValue?{startedAt:moment().subtract(7,"days").unix(),endedAt:moment().unix()}:{startedAt:moment().subtract(30,"days").unix(),endedAt:moment().unix()},window.profile.dateTime=c,getCharts(),a.draw()}),getCharts(),function b(){$(".created-time").text("Created "+moment(profile.createdTime).fromNow()),setTimeout(b,5e3)}()}),$(document).on("show.bs.tab",'[data-target="#tab-filtering"]',function(a){setFilteredTable()}); -------------------------------------------------------------------------------- /flask_profiler/static/dist/js/settings.js: -------------------------------------------------------------------------------- 1 | $(document).on('ready', function () { 2 | $('#clear-settings-info').hide(); 3 | 4 | $('.dump-database').click(function (e) { 5 | window.location = 'db/dumpDatabase'; 6 | }); 7 | 8 | $('.delete-database').click(function (e) { 9 | $.get("db/deleteDatabase", function (data) { 10 | if (data.status === true) { 11 | $('#settings-info').html("All database data removed successfully"); 12 | $('#clear-settings-info').show(); 13 | } else { 14 | $('#settings-info').html("Some error occurred while deleting database data."); 15 | $('#clear-settings-info').show(); 16 | } 17 | } 18 | ); 19 | }); 20 | 21 | $('#clear-settings-info').click(function (e) { 22 | $('#settings-info').html(""); 23 | $('#clear-settings-info').hide(); 24 | }); 25 | }); -------------------------------------------------------------------------------- /flask_profiler/storage/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | 3 | import os 4 | import sys 5 | import importlib 6 | from contextlib import contextmanager 7 | 8 | from .base import BaseStorage 9 | 10 | 11 | @contextmanager 12 | def cwd_in_path(): 13 | cwd = os.getcwd() 14 | if cwd in sys.path: 15 | yield 16 | else: 17 | sys.path.insert(0, cwd) 18 | try: 19 | yield cwd 20 | finally: 21 | sys.path.remove(cwd) 22 | 23 | 24 | def getCollection(conf): 25 | engine = conf.get("engine", "") 26 | if engine.lower() == "mongodb": 27 | from .mongo import Mongo 28 | return Mongo(conf) 29 | elif engine.lower() == "sqlite": 30 | from .sqlite import Sqlite 31 | return Sqlite(conf) 32 | elif engine.lower() == "sqlalchemy": 33 | from .sql_alchemy import Sqlalchemy 34 | return Sqlalchemy(conf) 35 | else: 36 | try: 37 | parts = engine.split('.') 38 | if len(parts) < 1: # engine must have at least module name and class 39 | raise ImportError 40 | 41 | module_name = '.'.join(parts[:-1]) 42 | klass_name = parts[-1] 43 | 44 | # we need to make sure that it will be able to find module in your 45 | # project directory 46 | with cwd_in_path(): 47 | module = importlib.import_module(module_name) 48 | 49 | storage = getattr(module, klass_name) 50 | if not issubclass(storage, BaseStorage): 51 | raise ImportError 52 | 53 | except ImportError: 54 | raise ValueError( 55 | ("flask-profiler requires a valid storage engine but it is" 56 | " missing or wrong. provided engine: {}".format(engine))) 57 | 58 | return storage(conf) 59 | -------------------------------------------------------------------------------- /flask_profiler/storage/base.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class BaseStorage(object): 4 | """docstring for BaseStorage""" 5 | def __init__(self): 6 | super(BaseStorage, self).__init__() 7 | 8 | def filter(self, criteria): 9 | raise Exception("Not implemented Error") 10 | 11 | def getSummary(self, criteria): 12 | raise Exception("Not implemented Error") 13 | 14 | def insert(self, measurement): 15 | raise Exception("Not implemented Error") 16 | 17 | def delete(self, measurementId): 18 | raise Exception("Not implemented Error") 19 | 20 | def truncate(self): 21 | raise Exception("Not implemented Error") 22 | -------------------------------------------------------------------------------- /flask_profiler/storage/mongo.py: -------------------------------------------------------------------------------- 1 | import time 2 | import datetime 3 | import pymongo 4 | from .base import BaseStorage 5 | import datetime 6 | from bson.objectid import ObjectId 7 | 8 | 9 | class Mongo(BaseStorage): 10 | """ 11 | To use this class, you have to provide a config dictionary which contains 12 | "MONGO_URL", "DATABASE" and "COLLECTION". 13 | """ 14 | 15 | def __init__(self, config=None): 16 | super(Mongo, self).__init__(), 17 | self.config = config 18 | self.mongo_url = self.config.get("MONGO_URL", "mongodb://localhost") 19 | self.database_name = self.config.get("DATABASE", "flask_profiler") 20 | self.collection_name = self.config.get("COLLECTION", "measurements") 21 | 22 | def createIndex(): 23 | self.collection.ensure_index( 24 | [ 25 | ('startedAt', 1), 26 | ('endedAt', 1), 27 | ('elapsed', 1), 28 | ('name', 1), 29 | ('method', 1)] 30 | ) 31 | 32 | self.client = pymongo.MongoClient(self.mongo_url) 33 | self.db = self.client[self.database_name] 34 | self.collection = self.db[self.collection_name] 35 | createIndex() 36 | 37 | def filter(self, filtering={}): 38 | query = {} 39 | limit = int(filtering.get('limit', 100000)) 40 | skip = int(filtering.get('skip', 0)) 41 | sort = filtering.get('sort', "endedAt,desc").split(",") 42 | 43 | startedAt = datetime.datetime.fromtimestamp(float( 44 | filtering.get('startedAt', time.time() - 3600 * 24 * 7))) 45 | endedAt = datetime.datetime.fromtimestamp( 46 | float(filtering.get('endedAt', time.time()))) 47 | elapsed = float(filtering.get('elapsed', 0)) 48 | name = filtering.get('name', None) 49 | method = filtering.get('method', None) 50 | args = filtering.get('args', None) 51 | kwargs = filtering.get('kwargs', None) 52 | 53 | if sort[1] == "desc": 54 | sort_dir = pymongo.DESCENDING 55 | else: 56 | sort_dir = pymongo.ASCENDING 57 | 58 | if name: 59 | query['name'] = name 60 | if method: 61 | query['method'] = method 62 | if endedAt: 63 | query['endedAt'] = {"$lte": endedAt} 64 | if startedAt: 65 | query['startedAt'] = {"$gt": startedAt} 66 | if elapsed: 67 | query['elapsed'] = {"$gte": elapsed} 68 | if args: 69 | query['args'] = args 70 | if kwargs: 71 | query['kwargs'] = kwargs 72 | 73 | if limit: 74 | cursor = self.collection.find( 75 | query 76 | ).sort(sort[0], sort_dir).skip(skip) 77 | else: 78 | cursor = self.collection.find( 79 | query 80 | ).sort(sort[0], sort_dir).skip(skip).limit(limit) 81 | return (self.clearify(record) for record in cursor) 82 | 83 | def insert(self, measurement): 84 | measurement["startedAt"] = datetime.datetime.fromtimestamp( 85 | measurement["startedAt"]) 86 | measurement["endedAt"] = datetime.datetime.fromtimestamp( 87 | measurement["endedAt"]) 88 | 89 | result = self.collection.insert(measurement) 90 | if result: 91 | return True 92 | return False 93 | 94 | def truncate(self): 95 | result = self.collection.remove() 96 | if result: 97 | return True 98 | return False 99 | 100 | def delete(self, measurementId): 101 | result = self.collection.remove({"_id": ObjectId(measurementId)}) 102 | if result: 103 | return True 104 | return False 105 | 106 | def getSummary(self, filtering={}): 107 | match_condition = {} 108 | endedAt = datetime.datetime.fromtimestamp( 109 | float(filtering.get('endedAt', time.time()))) 110 | startedAt = datetime.datetime.fromtimestamp( 111 | float(filtering.get('startedAt', time.time() - 3600 * 24 * 7))) 112 | elapsed = filtering.get('elapsed', None) 113 | name = filtering.get('name', None) 114 | method = filtering.get('method', None) 115 | sort = filtering.get('sort', "count,desc").split(",") 116 | 117 | if name: 118 | match_condition['name'] = name 119 | if method: 120 | match_condition['method'] = method 121 | if endedAt: 122 | match_condition['endedAt'] = {"$lte": endedAt} 123 | if startedAt: 124 | match_condition['startedAt'] = {"$gt": startedAt} 125 | if elapsed: 126 | match_condition['elapsed'] = {"$gte": elapsed} 127 | 128 | if sort[1] == "desc": 129 | sort_dir = -1 130 | else: 131 | sort_dir = 1 132 | 133 | return self.aggregate([ 134 | {"$match": match_condition}, 135 | { 136 | "$group": { 137 | "_id": { 138 | "method": "$method", 139 | "name": "$name" 140 | }, 141 | "count": {"$sum": 1}, 142 | "minElapsed": {"$min": "$elapsed"}, 143 | "maxElapsed": {"$max": "$elapsed"}, 144 | "avgElapsed": {"$avg": "$elapsed"} 145 | } 146 | }, 147 | { 148 | "$project": { 149 | "_id": 0, 150 | "method": "$_id.method", 151 | "name": "$_id.name", 152 | "count": 1, 153 | "minElapsed": 1, 154 | "maxElapsed": 1, 155 | "avgElapsed": 1 156 | } 157 | }, 158 | { 159 | "$sort": {sort[0]: sort_dir} 160 | } 161 | ]) 162 | 163 | def getMethodDistribution(self, filtering=None): 164 | if not filtering: 165 | filtering = {} 166 | 167 | startedAt = datetime.datetime.fromtimestamp(float( 168 | filtering.get('startedAt', time.time() - 3600 * 24 * 7))) 169 | endedAt = datetime.datetime.fromtimestamp( 170 | float(filtering.get('endedAt', time.time()))) 171 | 172 | match_condition = { 173 | "startedAt": {"$gte": startedAt}, 174 | "endedAt": {"$lte": endedAt} 175 | } 176 | 177 | result = self.aggregate([ 178 | {"$match": match_condition}, 179 | { 180 | "$group": { 181 | "_id": { 182 | "method": "$method" 183 | }, 184 | "count": {"$sum": 1} 185 | } 186 | }, 187 | { 188 | "$project": { 189 | "_id": 0, 190 | "method": "$_id.method", 191 | "count": 1 192 | } 193 | } 194 | ]) 195 | 196 | distribution = dict((i["method"], i["count"]) for i in result) 197 | return distribution 198 | 199 | def getTimeseries(self, filtering=None): 200 | if not filtering: 201 | filtering = {} 202 | if filtering.get('interval', None) == "daily": 203 | dateFormat = '%Y-%m-%d' 204 | interval = 3600 * 24 # daily 205 | groupId = { 206 | "month": {"$month": "$startedAt"}, 207 | "day": {"$dayOfMonth": "$startedAt"}, 208 | "year": {"$year": "$startedAt"}} 209 | 210 | else: 211 | dateFormat = '%Y-%m-%d %H' 212 | interval = 3600 # hourly 213 | groupId = { 214 | "hour": {"$hour": "$startedAt"}, 215 | "day": {"$dayOfMonth": "$startedAt"}, 216 | "month": {"$month": "$startedAt"}, 217 | "year": {"$year": "$startedAt"}} 218 | 219 | startedAt = float( 220 | filtering.get('startedAt', time.time() - 3600 * 24 * 7)) 221 | startedAtF = datetime.datetime.fromtimestamp(startedAt) 222 | endedAt = float(filtering.get('endedAt', time.time())) 223 | endedAtF = datetime.datetime.fromtimestamp(endedAt) 224 | 225 | match_condition = { 226 | "startedAt": {"$gte": startedAtF}, 227 | "endedAt": {"$lte": endedAtF} 228 | } 229 | result = self.aggregate([ 230 | {"$match": match_condition}, 231 | { 232 | "$group": { 233 | "_id": groupId, 234 | "startedAt": {"$first": "$startedAt"}, 235 | "count": {"$sum": 1}, 236 | } 237 | } 238 | ]) 239 | series = {} 240 | for i in range(int(startedAt), int(endedAt) + 1, interval): 241 | series[datetime.datetime.fromtimestamp(i).strftime(dateFormat)] = 0 242 | 243 | for i in result: 244 | series[i["startedAt"].strftime(dateFormat)] = i["count"] 245 | return series 246 | 247 | def clearify(self, obj): 248 | available_types = [int, dict, str, list] 249 | obj["startedAt"] = obj["startedAt"].strftime("%s") 250 | obj["endedAt"] = obj["endedAt"].strftime("%s") 251 | for k, v in obj.items(): 252 | if any([isinstance(v, av_type) for av_type in available_types]): 253 | continue 254 | if k == "_id": 255 | k = "id" 256 | obj.pop("_id") 257 | obj[k] = str(v) 258 | return obj 259 | 260 | def get(self, measurementId): 261 | record = self.collection.find_one({'_id': ObjectId(measurementId)}) 262 | return self.clearify(record) 263 | 264 | def aggregate(self, pipeline, **kwargs): 265 | """Perform an aggregation and make sure that result will be everytime 266 | CommandCursor. Will take care for pymongo version differencies 267 | :param pipeline: {list} of aggregation pipeline stages 268 | :return: {pymongo.command_cursor.CommandCursor} 269 | """ 270 | result = self.collection.aggregate(pipeline, **kwargs) 271 | if pymongo.version_tuple < (3, 0, 0): 272 | result = result['result'] 273 | 274 | return result 275 | -------------------------------------------------------------------------------- /flask_profiler/storage/sql_alchemy.py: -------------------------------------------------------------------------------- 1 | import json 2 | from decimal import Decimal, ROUND_UP 3 | from .base import BaseStorage 4 | from datetime import datetime 5 | import time 6 | from sqlalchemy import create_engine, Text 7 | from sqlalchemy import Column, Integer, Numeric 8 | from sqlalchemy.ext.declarative import declarative_base 9 | from sqlalchemy.orm import sessionmaker 10 | from sqlalchemy import func 11 | 12 | base = declarative_base() 13 | 14 | 15 | def formatDate(timestamp, dateFormat): 16 | return datetime.fromtimestamp(timestamp).strftime(dateFormat) 17 | 18 | 19 | class Measurements(base): 20 | __tablename__ = 'flask_profiler_measurements' 21 | 22 | id = Column(Integer, primary_key=True) 23 | startedAt = Column(Numeric) 24 | endedAt = Column(Numeric) 25 | elapsed = Column(Numeric(10, 4)) 26 | method = Column(Text) 27 | args = Column(Text) 28 | kwargs = Column(Text) 29 | name = Column(Text) 30 | context = Column(Text) 31 | 32 | def __repr__(self): 33 | return "".format( 34 | self.id, 35 | self.startedAt, 36 | self.endedAt, 37 | self.elapsed, 38 | self.method, 39 | self.args, 40 | self.kwargs, 41 | self.name, 42 | self.context 43 | ) 44 | 45 | 46 | class Sqlalchemy(BaseStorage): 47 | 48 | def __init__(self, config=None): 49 | super(Sqlalchemy, self).__init__() 50 | self.config = config 51 | self.db = create_engine( 52 | self.config.get("db_url", "sqlite:///flask_profiler.sql"), 53 | pool_pre_ping=True 54 | ) 55 | self.create_database() 56 | 57 | def __enter__(self): 58 | return self 59 | 60 | def create_database(self): 61 | base.metadata.create_all(self.db) 62 | 63 | def insert(self, kwds): 64 | endedAt = int(kwds.get('endedAt', None)) 65 | startedAt = int(kwds.get('startedAt', None)) 66 | elapsed = Decimal(kwds.get('elapsed', None)) 67 | if elapsed: 68 | elapsed = elapsed.quantize(Decimal('.0001'), rounding=ROUND_UP) 69 | args = json.dumps(list(kwds.get('args', ()))) # tuple -> list -> json 70 | kwargs = json.dumps(kwds.get('kwargs', ())) 71 | context = json.dumps(kwds.get('context', {})) 72 | method = kwds.get('method', None) 73 | name = kwds.get('name', None) 74 | 75 | session = sessionmaker(self.db)() 76 | session.add(Measurements( 77 | endedAt=endedAt, 78 | startedAt=startedAt, 79 | elapsed=elapsed, 80 | args=args, 81 | kwargs=kwargs, 82 | context=context, 83 | method=method, 84 | name=name, 85 | )) 86 | session.commit() 87 | 88 | @staticmethod 89 | def getFilters(kwargs): 90 | filters = {} 91 | filters["sort"] = kwargs.get('sort', "endedAt,desc").split(",") 92 | 93 | # because inserting and filtering may take place at the same moment, 94 | # a very little increment(0.5) is needed to find inserted 95 | # record by sql. 96 | filters["endedAt"] = float( 97 | kwargs.get('endedAt', time.time() + 0.5)) 98 | filters["startedAt"] = float( 99 | kwargs.get('startedAt', time.time() - 3600 * 24 * 7)) 100 | 101 | filters["elapsed"] = kwargs.get('elapsed', None) 102 | filters["method"] = kwargs.get('method', None) 103 | filters["name"] = kwargs.get('name', None) 104 | filters["args"] = json.dumps( 105 | list(kwargs.get('args', ()))) # tuple -> list -> json 106 | filters["kwargs"] = json.dumps(kwargs.get('kwargs', ())) 107 | filters["sort"] = kwargs.get('sort', "endedAt,desc").split(",") 108 | filters["skip"] = int(kwargs.get('skip', 0)) 109 | filters["limit"] = int(kwargs.get('limit', 100)) 110 | return filters 111 | 112 | def filter(self, kwds={}): 113 | # Find Operation 114 | f = Sqlalchemy.getFilters(kwds) 115 | session = sessionmaker(self.db)() 116 | query = session.query(Measurements) 117 | 118 | if f["endedAt"]: 119 | query = query.filter(Measurements.endedAt <= f["endedAt"]) 120 | if f["startedAt"]: 121 | query = query.filter(Measurements.startedAt >= f["startedAt"]) 122 | if f["elapsed"]: 123 | query = query.filter(Measurements.elapsed >= f["elapsed"]) 124 | if f["method"]: 125 | query = query.filter(Measurements.method == f["method"]) 126 | if f["name"]: 127 | query = query.filter(Measurements.name == f["name"]) 128 | 129 | if f["sort"][1] == 'desc': 130 | query = query.order_by(getattr(Measurements, f["sort"][0]).desc()) 131 | else: 132 | query = query.order_by(getattr(Measurements, f["sort"][0]).asc()) 133 | rows = query.limit(f['limit']).offset(f['skip']) 134 | return (Sqlalchemy.jsonify_row(row) for row in rows) 135 | 136 | @staticmethod 137 | def jsonify_row(row): 138 | data = { 139 | "id": row.id, 140 | "startedAt": row.startedAt, 141 | "endedAt": row.endedAt, 142 | "elapsed": row.elapsed, 143 | "method": row.method, 144 | "args": tuple(json.loads(row.args)), # json -> list -> tuple 145 | "kwargs": json.loads(row.kwargs), 146 | "name": row.name, 147 | "context": json.loads(row.context), 148 | } 149 | return data 150 | 151 | def truncate(self): 152 | session = sessionmaker(self.db)() 153 | try: 154 | session.query(Measurements).delete() 155 | session.commit() 156 | return True 157 | except: 158 | session.rollback() 159 | return False 160 | 161 | def delete(self, measurementId): 162 | session = sessionmaker(self.db)() 163 | try: 164 | session.query(Measurements).filter_by(id=measurementId).delete() 165 | session.commit() 166 | return True 167 | except: 168 | session.rollback() 169 | return False 170 | 171 | def getSummary(self, kwds={}): 172 | filters = Sqlalchemy.getFilters(kwds) 173 | session = sessionmaker(self.db)() 174 | count = func.count(Measurements.id).label('count') 175 | min_elapsed = func.min(Measurements.elapsed).label('minElapsed') 176 | max_elapsed = func.max(Measurements.elapsed).label('maxElapsed') 177 | avg_elapsed = func.avg(Measurements.elapsed).label('avgElapsed') 178 | query = session.query( 179 | Measurements.method, 180 | Measurements.name, 181 | count, 182 | min_elapsed, 183 | max_elapsed, 184 | avg_elapsed 185 | ) 186 | 187 | if filters["startedAt"]: 188 | query = query.filter(Measurements.startedAt >= filters["startedAt"]) 189 | if filters["endedAt"]: 190 | query = query.filter(Measurements.endedAt <= filters["endedAt"]) 191 | if filters["elapsed"]: 192 | query = query.filter(Measurements.elapsed >= filters["elapsed"]) 193 | 194 | query = query.group_by(Measurements.method, Measurements.name) 195 | if filters["sort"][1] == 'desc': 196 | if filters["sort"][0] == 'count': 197 | query = query.order_by(count.desc()) 198 | elif filters["sort"][0] == 'minElapsed': 199 | query = query.order_by(min_elapsed.desc()) 200 | elif filters["sort"][0] == 'maxElapsed': 201 | query = query.order_by(max_elapsed.desc()) 202 | elif filters["sort"][0] == 'avgElapsed': 203 | query = query.order_by(avg_elapsed.desc()) 204 | else: 205 | query = query.order_by( 206 | getattr(Measurements, filters["sort"][0]).desc()) 207 | else: 208 | if filters["sort"][0] == 'count': 209 | query = query.order_by(count.asc()) 210 | elif filters["sort"][0] == 'minElapsed': 211 | query = query.order_by(min_elapsed.asc()) 212 | elif filters["sort"][0] == 'maxElapsed': 213 | query = query.order_by(max_elapsed.asc()) 214 | elif filters["sort"][0] == 'avgElapsed': 215 | query = query.order_by(avg_elapsed.asc()) 216 | else: 217 | query = query.order_by( 218 | getattr(Measurements, filters["sort"][0]).asc()) 219 | rows = query.all() 220 | 221 | result = [] 222 | for r in rows: 223 | result.append({ 224 | "method": r[0], 225 | "name": r[1], 226 | "count": r[2], 227 | "minElapsed": r[3], 228 | "maxElapsed": r[4], 229 | "avgElapsed": r[5] 230 | }) 231 | return result 232 | 233 | def getTimeseries(self, kwds={}): 234 | filters = Sqlalchemy.getFilters(kwds) 235 | session = sessionmaker(self.db)() 236 | if kwds.get('interval', None) == "daily": 237 | interval = 3600 * 24 # daily 238 | dateFormat = "%Y-%m-%d" 239 | format = "day" 240 | else: 241 | interval = 3600 # hourly 242 | dateFormat = "%Y-%m-%d %H" 243 | format = "hour" 244 | endedAt, startedAt = filters["endedAt"], filters["startedAt"] 245 | 246 | rows = session.query( 247 | Measurements.startedAt, 248 | ).filter( 249 | Measurements.endedAt <= endedAt, 250 | Measurements.startedAt >= startedAt 251 | ).order_by( 252 | Measurements.startedAt.asc() 253 | ).all() 254 | 255 | rows = [datetime.utcfromtimestamp(row[0]).strftime(dateFormat) for row in rows] 256 | temp = set(rows) 257 | rows = [(time, rows.count(time)) for time in temp] 258 | series = {} 259 | 260 | for i in range(int(startedAt), int(endedAt) + 1, interval): 261 | series[formatDate(i, dateFormat)] = 0 262 | 263 | for row in rows: 264 | series[ 265 | formatDate( 266 | datetime.strptime(row[0], dateFormat).timestamp(), 267 | dateFormat 268 | ) 269 | ] = row[1] 270 | return series 271 | 272 | def getMethodDistribution(self, kwds=None): 273 | if not kwds: 274 | kwds = {} 275 | f = Sqlalchemy.getFilters(kwds) 276 | session = sessionmaker(self.db)() 277 | endedAt, startedAt = f["endedAt"], f["startedAt"] 278 | 279 | rows = session.query( 280 | Measurements.method, 281 | func.count(Measurements.id) 282 | ).filter( 283 | Measurements.endedAt <= endedAt, 284 | Measurements.startedAt >= startedAt 285 | ).group_by( 286 | Measurements.method 287 | ).all() 288 | 289 | results = {} 290 | for row in rows: 291 | results[row[0]] = row[1] 292 | return results 293 | 294 | def __exit__(self, exc_type, exc_value, traceback): 295 | return self.db 296 | -------------------------------------------------------------------------------- /flask_profiler/storage/sqlite.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import json 3 | from .base import BaseStorage 4 | from datetime import datetime 5 | from timeit import default_timer 6 | import time 7 | # from time import perf_counter 8 | import threading 9 | 10 | 11 | def formatDate(timestamp, dateFormat): 12 | return datetime.fromtimestamp(timestamp).strftime(dateFormat) 13 | 14 | 15 | class Sqlite(BaseStorage): 16 | """docstring for Sqlite""" 17 | def __init__(self, config=None): 18 | super(Sqlite, self).__init__() 19 | self.config = config 20 | self.sqlite_file = self.config.get("FILE", "flask_profiler.sql") 21 | self.table_name = self.config.get("TABLE", "measurements") 22 | 23 | self.startedAt_head = 'startedAt' # name of the column 24 | self.endedAt_head = 'endedAt' # name of the column 25 | self.elapsed_head = 'elapsed' # name of the column 26 | self.method_head = 'method' 27 | self.args_head = 'args' 28 | self.kwargs_head = 'kwargs' 29 | self.name_head = 'name' 30 | self.context_head = 'context' 31 | 32 | self.connection = sqlite3.connect( 33 | self.sqlite_file, check_same_thread=False) 34 | self.cursor = self.connection.cursor() 35 | 36 | self.lock = threading.Lock() 37 | try: 38 | self.create_database() 39 | except sqlite3.OperationalError as e: 40 | if "already exists" not in str(e): 41 | raise e 42 | 43 | def __enter__(self): 44 | return self 45 | 46 | @staticmethod 47 | def getFilters(kwargs): 48 | filters = {} 49 | filters["sort"] = kwargs.get('sort', "endedAt,desc").split(",") 50 | 51 | # because inserting and filtering may take place at the same moment, 52 | # a very little increment(0.5) is needed to find inserted 53 | # record by sql. 54 | filters["endedAt"] = float( 55 | kwargs.get('endedAt', time.time() + 0.5)) 56 | filters["startedAt"] = float( 57 | kwargs.get('startedAt', time.time() - 3600 * 24 * 7)) 58 | 59 | filters["elapsed"] = kwargs.get('elapsed', None) 60 | filters["method"] = kwargs.get('method', None) 61 | filters["name"] = kwargs.get('name', None) 62 | filters["args"] = json.dumps( 63 | list(kwargs.get('args', ()))) # tuple -> list -> json 64 | filters["kwargs"] = json.dumps(kwargs.get('kwargs', ())) 65 | filters["sort"] = kwargs.get('sort', "endedAt,desc").split(",") 66 | filters["skip"] = int(kwargs.get('skip', 0)) 67 | filters["limit"] = int(kwargs.get('limit', 100)) 68 | return filters 69 | 70 | def create_database(self): 71 | with self.lock: 72 | sql = '''CREATE TABLE {table_name} 73 | ( 74 | ID Integer PRIMARY KEY AUTOINCREMENT, 75 | {startedAt} REAL, 76 | {endedAt} REAL, 77 | {elapsed} REAL, 78 | {args} TEXT, 79 | {kwargs} TEXT, 80 | {method} TEXT, 81 | {context} TEXT, 82 | {name} TEXT 83 | ); 84 | '''.format( 85 | table_name=self.table_name, 86 | startedAt=self.startedAt_head, 87 | endedAt=self.endedAt_head, 88 | elapsed=self.elapsed_head, 89 | args=self.args_head, 90 | kwargs=self.kwargs_head, 91 | method=self.method_head, 92 | context=self.context_head, 93 | name=self.name_head 94 | ) 95 | self.cursor.execute(sql) 96 | 97 | sql = """ 98 | CREATE INDEX measurement_index ON {table_name} 99 | ({startedAt}, {endedAt}, {elapsed}, {name}, {method}); 100 | """.format( 101 | startedAt=self.startedAt_head, 102 | endedAt=self.endedAt_head, 103 | elapsed=self.elapsed_head, 104 | name=self.name_head, 105 | method=self.method_head, 106 | table_name=self.table_name) 107 | self.cursor.execute(sql) 108 | 109 | self.connection.commit() 110 | 111 | def insert(self, kwds): 112 | endedAt = float(kwds.get('endedAt', None)) 113 | startedAt = float(kwds.get('startedAt', None)) 114 | elapsed = kwds.get('elapsed', None) 115 | args = json.dumps(list(kwds.get('args', ()))) # tuple -> list -> json 116 | kwargs = json.dumps(kwds.get('kwargs', ())) 117 | context = json.dumps(kwds.get('context', {})) 118 | method = kwds.get('method', None) 119 | name = kwds.get('name', None) 120 | 121 | sql = """INSERT INTO {0} VALUES ( 122 | null, ?, ?, ?, ?,?, ?, ?, ?)""".format(self.table_name) 123 | 124 | with self.lock: 125 | self.cursor.execute(sql, ( 126 | startedAt, 127 | endedAt, 128 | elapsed, 129 | args, 130 | kwargs, 131 | method, 132 | context, 133 | name)) 134 | 135 | self.connection.commit() 136 | 137 | def getTimeseries(self, kwds={}): 138 | filters = Sqlite.getFilters(kwds) 139 | 140 | if kwds.get('interval', None) == "daily": 141 | interval = 3600 * 24 # daily 142 | dateFormat = '%Y-%m-%d' 143 | else: 144 | interval = 3600 # hourly 145 | dateFormat = '%Y-%m-%d %H' 146 | 147 | endedAt, startedAt = filters["endedAt"], filters["startedAt"] 148 | 149 | conditions = "where endedAt<={0} AND startedAt>={1} ".format( 150 | endedAt, startedAt) 151 | with self.lock: 152 | sql = '''SELECT 153 | startedAt, count(id) as count 154 | FROM "{table_name}" {conditions} 155 | group by strftime("{dateFormat}", datetime(startedAt, 'unixepoch')) 156 | order by startedAt asc 157 | '''.format( 158 | table_name=self.table_name, 159 | dateFormat=dateFormat, 160 | conditions=conditions 161 | ) 162 | 163 | self.cursor.execute(sql) 164 | rows = self.cursor.fetchall() 165 | 166 | series = {} 167 | for i in range(int(startedAt), int(endedAt) + 1, interval): 168 | series[formatDate(i, dateFormat)] = 0 169 | 170 | for row in rows: 171 | series[formatDate(row[0], dateFormat)] = row[1] 172 | return series 173 | 174 | def getMethodDistribution(self, kwds=None): 175 | if not kwds: 176 | kwds = {} 177 | f = Sqlite.getFilters(kwds) 178 | endedAt, startedAt = f["endedAt"], f["startedAt"] 179 | conditions = "where endedAt<={0} AND startedAt>={1} ".format( 180 | endedAt, startedAt) 181 | 182 | with self.lock: 183 | sql = '''SELECT 184 | method, count(id) as count 185 | FROM "{table_name}" {conditions} 186 | group by method 187 | '''.format( 188 | table_name=self.table_name, 189 | conditions=conditions 190 | ) 191 | 192 | self.cursor.execute(sql) 193 | rows = self.cursor.fetchall() 194 | 195 | results = {} 196 | for row in rows: 197 | results[row[0]] = row[1] 198 | return results 199 | 200 | def filter(self, kwds={}): 201 | # Find Operation 202 | f = Sqlite.getFilters(kwds) 203 | 204 | conditions = "WHERE 1=1 AND " 205 | 206 | if f["endedAt"]: 207 | conditions = conditions + 'endedAt<={0} AND '.format(f["endedAt"]) 208 | if f["startedAt"]: 209 | conditions = conditions + 'startedAt>={0} AND '.format( 210 | f["startedAt"]) 211 | if f["elapsed"]: 212 | conditions = conditions + 'elapsed>={0} AND '.format(f["elapsed"]) 213 | if f["method"]: 214 | conditions = conditions + 'method="{0}" AND '.format(f["method"]) 215 | if f["name"]: 216 | conditions = conditions + 'name="{0}" AND '.format(f["name"]) 217 | 218 | conditions = conditions.rstrip(" AND") 219 | 220 | with self.lock: 221 | sql = '''SELECT * FROM "{table_name}" {conditions} 222 | order by {sort_field} {sort_direction} 223 | limit {limit} OFFSET {skip} '''.format( 224 | table_name=self.table_name, 225 | conditions=conditions, 226 | sort_field=f["sort"][0], 227 | sort_direction=f["sort"][1], 228 | limit=f['limit'], 229 | skip=f['skip'] 230 | ) 231 | 232 | self.cursor.execute(sql) 233 | rows = self.cursor.fetchall() 234 | return (self.jsonify_row(row) for row in rows) 235 | 236 | def get(self, measurementId): 237 | with self.lock: 238 | self.cursor.execute( 239 | 'SELECT * FROM "{table_name}" WHERE ID={measurementId}'.format( 240 | table_name=self.table_name, 241 | measurementId=measurementId 242 | ) 243 | ) 244 | rows = self.cursor.fetchall() 245 | record = rows[0] 246 | return self.jsonify_row(record) 247 | 248 | def truncate(self): 249 | with self.lock: 250 | self.cursor.execute("DELETE FROM {0}".format(self.table_name)) 251 | self.connection.commit() 252 | # Making the api match with mongo collection, this function must return 253 | # True or False based on success of this delete operation 254 | return True if self.cursor.rowcount else False 255 | 256 | def delete(self, measurementId): 257 | with self.lock: 258 | self.cursor.execute( 259 | 'DELETE FROM "{table_name}" WHERE ID={measurementId}'.format( 260 | table_name=self.table_name, 261 | measurementId=measurementId 262 | ) 263 | ) 264 | return self.connection.commit() 265 | 266 | def jsonify_row(self, row): 267 | data = { 268 | "id": row[0], 269 | "startedAt": row[1], 270 | "endedAt": row[2], 271 | "elapsed": row[3], 272 | "args": tuple(json.loads(row[4])), # json -> list -> tuple 273 | "kwargs": json.loads(row[5]), 274 | "method": row[6], 275 | "context": json.loads(row[7]), 276 | "name": row[8] 277 | } 278 | 279 | return data 280 | 281 | def getSummary(self, kwds={}): 282 | filters = Sqlite.getFilters(kwds) 283 | 284 | conditions = "WHERE 1=1 and " 285 | 286 | if filters["startedAt"]: 287 | conditions = conditions + "startedAt>={0} AND ".format( 288 | filters["startedAt"]) 289 | if filters["endedAt"]: 290 | conditions = conditions + "endedAt<={0} AND ".format( 291 | filters["endedAt"]) 292 | if filters["elapsed"]: 293 | conditions = conditions + "elapsed>={0} AND".format( 294 | filters["elapsed"]) 295 | 296 | conditions = conditions.rstrip(" AND") 297 | with self.lock: 298 | sql = '''SELECT 299 | method, name, 300 | count(id) as count, 301 | min(elapsed) as minElapsed, 302 | max(elapsed) as maxElapsed, 303 | avg(elapsed) as avgElapsed 304 | FROM "{table_name}" {conditions} 305 | group by method, name 306 | order by {sort_field} {sort_direction} 307 | '''.format( 308 | table_name=self.table_name, 309 | conditions=conditions, 310 | sort_field=filters["sort"][0], 311 | sort_direction=filters["sort"][1] 312 | ) 313 | 314 | self.cursor.execute(sql) 315 | rows = self.cursor.fetchall() 316 | 317 | result = [] 318 | for r in rows: 319 | result.append({ 320 | "method": r[0], 321 | "name": r[1], 322 | "count": r[2], 323 | "minElapsed": r[3], 324 | "maxElapsed": r[4], 325 | "avgElapsed": r[5] 326 | }) 327 | return result 328 | 329 | def __exit__(self, exc_type, exc_value, traceback): 330 | return self.connection.close() 331 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click==6.7 2 | Flask==1.0 3 | Flask-HTTPAuth==3.2.3 4 | Flask-Testing==0.6.2 5 | itsdangerous==0.24 6 | Jinja2==2.9.6 7 | MarkupSafe==1.0 8 | Werkzeug==0.15.3 9 | sqlalchemy==1.1.14 10 | simplejson==3.13.2 11 | -------------------------------------------------------------------------------- /resources/dashboard_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muatik/flask-profiler/6a36526184ad3a903c2d50b3e208ed276d3404f6/resources/dashboard_screen.png -------------------------------------------------------------------------------- /resources/filtering_all_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muatik/flask-profiler/6a36526184ad3a903c2d50b3e208ed276d3404f6/resources/filtering_all_screen.png -------------------------------------------------------------------------------- /resources/filtering_detail_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muatik/flask-profiler/6a36526184ad3a903c2d50b3e208ed276d3404f6/resources/filtering_detail_screen.png -------------------------------------------------------------------------------- /resources/filtering_method_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muatik/flask-profiler/6a36526184ad3a903c2d50b3e208ed276d3404f6/resources/filtering_method_screen.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Flask-Profiler 3 | ------------- 4 | 5 | Flask Profiler 6 | 7 | Links 8 | ````` 9 | 10 | * `development version ` 11 | 12 | """ 13 | import sys 14 | from setuptools import setup 15 | 16 | 17 | tests_require = [ 18 | "Flask-Testing", 19 | "simplejson", 20 | "sqlalchemy" 21 | ] 22 | 23 | install_requires = [ 24 | 'Flask', 25 | 'Flask-HTTPAuth', 26 | 'simplejson' 27 | ] 28 | 29 | setup( 30 | name='flask_profiler', 31 | version='1.8', 32 | url='https://github.com/muatik/flask-profiler', 33 | license=open('LICENSE').read(), 34 | author='Mustafa Atik', 35 | author_email='muatik@gmail.com', 36 | description='API endpoint profiler for Flask framework', 37 | keywords=[ 38 | 'profiler', 'flask', 'performance', 'optimization' 39 | ], 40 | long_description=open('README.md').read(), 41 | packages=['flask_profiler'], 42 | package_data={ 43 | 'flask_profiler': [ 44 | 'storage/*', 45 | 'static/dist/fonts/*', 46 | 'static/dist/css/*', 47 | 'static/dist/js/*', 48 | 'static/dist/images/*', 49 | 'static/dist/js/*' 50 | 'static/dist/*', 51 | 'static/dist/index.html', 52 | ] 53 | }, 54 | test_suite="tests.suite", 55 | zip_safe=False, 56 | platforms='any', 57 | install_requires=install_requires, 58 | tests_require=tests_require, 59 | classifiers=[ 60 | 'Environment :: Web Environment', 61 | 'Intended Audience :: Developers', 62 | 'License :: OSI Approved :: MIT License', 63 | 'Operating System :: OS Independent', 64 | 'Programming Language :: Python', 65 | 'Programming Language :: Python :: 3', 66 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 67 | 'Topic :: Software Development :: Libraries :: Python Modules' 68 | ] 69 | ) 70 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import logging 4 | 5 | import sys 6 | 7 | from .test_endpoint_ignore import EndpointIgnoreTestCase 8 | from .test_measurement import MeasurementTest 9 | from .test_measure_endpoint import EndpointMeasurementTest, EndpointMeasurementTest2 10 | 11 | logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) 12 | 13 | def suite(): 14 | suite = unittest.TestSuite() 15 | suite.addTest(unittest.makeSuite(MeasurementTest)) 16 | suite.addTest(unittest.makeSuite(EndpointMeasurementTest)) 17 | suite.addTest(unittest.makeSuite(EndpointIgnoreTestCase)) 18 | suite.addTest(unittest.makeSuite(EndpointMeasurementTest2)) 19 | return suite 20 | -------------------------------------------------------------------------------- /tests/basetest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | import unittest 3 | import sys 4 | from os import environ, path 5 | 6 | from flask import Flask 7 | 8 | sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) 9 | 10 | from flask_profiler import flask_profiler, measure 11 | from flask_profiler import storage 12 | 13 | _CONFS = { 14 | "mongodb": { 15 | "enabled": True, 16 | "storage": { 17 | "engine": "mongodb", 18 | "DATABASE": "flask_profiler_test", 19 | "COLLECTION": "profiler", 20 | "MONGO_URL": "mongodb://localhost" 21 | }, 22 | "ignore": [ 23 | "^/static/.*" 24 | ] 25 | }, 26 | "sqlite": { 27 | "enabled": True, 28 | "storage": { 29 | "engine": "sqlite" 30 | }, 31 | "ignore": [ 32 | "^/static/.*" 33 | ] 34 | }, 35 | "sqlalchemy": { 36 | "enabled": True, 37 | "storage": { 38 | "engine": "sqlalchemy", 39 | "db_url": "sqlite:///flask_profiler.sql" 40 | }, 41 | "ignore": [ 42 | "^/static/.*" 43 | ] 44 | } 45 | } 46 | CONF = _CONFS[environ.get('FLASK_PROFILER_TEST_CONF', 'sqlalchemy')] 47 | 48 | 49 | class BasetTest(unittest.TestCase): 50 | 51 | def setUp(cls): 52 | flask_profiler.collection.truncate() 53 | 54 | @classmethod 55 | def setUpClass(cls): 56 | 57 | flask_profiler.collection = storage.getCollection(CONF["storage"]) 58 | 59 | def create_app(self): 60 | app = Flask(__name__) 61 | app.config["flask_profiler"] = CONF 62 | app.config['TESTING'] = True 63 | 64 | @app.route("/api/people/") 65 | def sayHello(firstname): 66 | return firstname 67 | 68 | @app.route("/static/photo/") 69 | def getStaticPhoto(): 70 | return "your static photo" 71 | 72 | @app.route("/static/") 73 | def getStatic(): 74 | return "your static" 75 | 76 | @app.route("/api/static/") 77 | def getApiStatic(): 78 | return "your api static" 79 | 80 | @app.route("/api/settings/system/secret/") 81 | def getSystemSettingsSecret(): 82 | return "your system settings secret" 83 | 84 | @app.route("/api/settings/personal/secret/") 85 | def getPersonalSettingsSecret(): 86 | return "your personal settings secret" 87 | 88 | @app.route("/api/settings/personal/name/") 89 | def getPersonalSettingsName(): 90 | return "your personal settings name" 91 | 92 | flask_profiler.init_app(app) 93 | 94 | @app.route("/api/without/profiler") 95 | def withoutProfiler(): 96 | return "without profiler" 97 | 98 | @app.route("/api/with/profiler/") 99 | @flask_profiler.profile() 100 | def customProfilerEP(message): 101 | return "with profiler" 102 | 103 | return app 104 | 105 | 106 | class BaseTest2(unittest.TestCase): 107 | 108 | def setUp(cls): 109 | flask_profiler.collection.truncate() 110 | 111 | @classmethod 112 | def setUpClass(cls): 113 | 114 | flask_profiler.collection = storage.getCollection(CONF["storage"]) 115 | 116 | def create_app(self): 117 | app = Flask(__name__) 118 | app.config["flask_profiler"] = CONF 119 | app.config['TESTING'] = True 120 | profiler = flask_profiler.Profiler() 121 | profiler.init_app(app) 122 | 123 | @app.route("/api/people/") 124 | def sayHello(firstname): 125 | return firstname 126 | 127 | @app.route("/api/with/profiler/") 128 | def customProfilerEP(message): 129 | return "with profiler" 130 | 131 | return app -------------------------------------------------------------------------------- /tests/module_accounts.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | import unittest 3 | from basetest import BaseTest 4 | from bson.objectid import ObjectId 5 | import arrow 6 | from app.modules.accounts import Accounts 7 | 8 | 9 | class TestAccount(BaseTest, unittest.TestCase): 10 | 11 | ACCOUNTS = [{ 12 | "id": None, 13 | "fname": "Mustafa", 14 | "lname": "Atik", 15 | "email": "mm@aasscsaccccmm.com", 16 | "gcmId": "code1", 17 | "alarms": [ 18 | { 19 | "location": { 20 | "country": "United Kingdom", 21 | "state": "", 22 | "city": "London", 23 | }, 24 | "keywords": [ 25 | "speaker", 26 | "scientist", 27 | "developer" 28 | ] 29 | }, { 30 | "location": { 31 | "country": "United Kingdom", 32 | "state": "", 33 | "city": "Manchester", 34 | }, 35 | "keywords": [ 36 | "manager" 37 | ] 38 | } 39 | ] 40 | }, { 41 | "id": None, 42 | "fname": "John", 43 | "lname": "Smith", 44 | "email": "jmm@jaasscsaccccmm.com", 45 | "gcmId": "code2", 46 | "alarms": [ 47 | { 48 | "location": { 49 | "country": "Ireland", 50 | "state": "", 51 | "city": "Dublin", 52 | }, 53 | "keywords": [ 54 | "waiter", 55 | "repair" 56 | ] 57 | } 58 | ] 59 | }, { 60 | "id": None, 61 | "fname": "George", 62 | "lname": "Mich", 63 | "email": "gmm@gaasscsaccccmm.com", 64 | "gcmId": "code3", 65 | "alarms": [ 66 | { 67 | "location": { 68 | "country": "United Kingdom", 69 | "state": "", 70 | "city": "Manchester", 71 | }, 72 | "keywords": [ 73 | "developer" 74 | ] 75 | }, { 76 | "location": { 77 | "country": "United Kingdom", 78 | "state": "", 79 | "city": "London", 80 | }, 81 | "keywords": [ 82 | "manager" 83 | ] 84 | } 85 | ] 86 | } 87 | ] 88 | 89 | @classmethod 90 | def setUpClass(cls): 91 | cls.accounts = Accounts(cls.config) 92 | 93 | def test_00_insert(self): 94 | for account in self.ACCOUNTS: 95 | record = self.accounts.insert(account) 96 | account['id'] = record['id'] 97 | self.assertIsInstance(record['id'], ObjectId) 98 | 99 | def test_01_getWithoutFilter(self): 100 | accountsFound = self.accounts.get(filtering={}) 101 | self.assertEqual(accountsFound.count(), len(self.ACCOUNTS)) 102 | 103 | def test_02_getOne(self): 104 | account = self.ACCOUNTS[0] 105 | accountFound = self.accounts.getOne(account['id']) 106 | self.assertEqual(account['id'], accountFound['id']) 107 | 108 | def test_0401_getByLocationFilter(self): 109 | def doFilter(account, count, countWithState): 110 | location = account["alarms"][0]["location"] 111 | filtering = { 112 | "location": { 113 | "country": location["country"].upper(), 114 | "city": location["city"].lower(), 115 | } 116 | } 117 | 118 | accountsFound = self.accounts.get(filtering=filtering) 119 | self.assertEqual(count, accountsFound.count()) 120 | 121 | filtering['location']['state'] = location["state"].upper() 122 | accountsFound = self.accounts.get(filtering=filtering) 123 | self.assertEqual(countWithState, accountsFound.count()) 124 | 125 | doFilter(self.ACCOUNTS[0], 2, 2) # 3 london accounts 126 | doFilter(self.ACCOUNTS[1], 1, 1) # 1 dublin account, 1 central account 127 | 128 | def test_0402_getByKeywordFilter(self): 129 | def doFilter(keyword, count): 130 | filtering = { 131 | "keyword": [keyword] 132 | } 133 | accountsFound = self.accounts.get(filtering=filtering) 134 | self.assertEqual(count, accountsFound.count()) 135 | doFilter("developer", 2) 136 | doFilter("scientist", 1) 137 | 138 | def test_0405_getByCombinedFilter(self): 139 | def doFilter(alarm, keyword, count, x=False): 140 | location = alarm["location"] 141 | filtering = { 142 | "location": { 143 | "country": location["country"].upper(), 144 | "city": location["city"].lower(), 145 | }, 146 | "keyword": [keyword] 147 | } 148 | accountsFound = self.accounts.get(filtering=filtering) 149 | self.assertEqual(count, accountsFound.count()) 150 | 151 | alarm = self.ACCOUNTS[0]["alarms"][0] 152 | doFilter(alarm, "developer", 1) # 1 developer in London 153 | doFilter(alarm, "waiter", 0) # 0 waiter in Dublin 154 | 155 | alarm = self.ACCOUNTS[2]["alarms"][0] 156 | doFilter(alarm, "developer", 1) 157 | doFilter(alarm, "manager", 1) 158 | 159 | def test_0406_getByGcm(self): 160 | account = self.ACCOUNTS[1] 161 | accountFound = self.accounts.getByGcm(account["gcmId"]) 162 | self.assertEqual(account["id"], accountFound["id"]) 163 | 164 | def test_0500_insertAlarm(self): 165 | account = self.ACCOUNTS[1] 166 | newAlarm = { 167 | "location": { 168 | "country": "France", 169 | "state": "", 170 | "city": "Paris", 171 | }, 172 | "keywords": [ 173 | "artist" 174 | ] 175 | } 176 | 177 | self.accounts.insertAlarm(account["id"], newAlarm) 178 | accountsFound = self.accounts.get(filtering=newAlarm) 179 | self.assertEqual(1, accountsFound.count()) 180 | 181 | def test_0600_removeAlarm(self): 182 | account = self.ACCOUNTS[0] 183 | alarms = account["alarms"] 184 | initialAlarmsCount = len(alarms) 185 | 186 | self.accounts.removeAlarm(account["id"], alarms[0]) 187 | account = self.accounts.getOne(account["id"]) 188 | self.assertEqual(initialAlarmsCount - 1, len(account["alarms"])) 189 | 190 | def test_0900_deleteAccount(self): 191 | account = self.ACCOUNTS[0] 192 | self.accounts.delete(account["id"]) 193 | accountsFound = self.accounts.get() 194 | self.assertEqual(len(self.ACCOUNTS) - 1, accountsFound.count()) 195 | 196 | def test_0700_starredJob(self): 197 | account = self.ACCOUNTS[0] 198 | jobId = ObjectId() 199 | 200 | self.accounts.insertStarredJob(account["id"], jobId) 201 | account = self.accounts.getOne(account["id"]) 202 | self.assertIn(jobId, account["jobs"]["starred"]) 203 | 204 | self.accounts.removeStarredJob(account["id"], jobId) 205 | account = self.accounts.getOne(account["id"]) 206 | self.assertNotIn(jobId, account["jobs"]["starred"]) 207 | 208 | unittest.main() 209 | -------------------------------------------------------------------------------- /tests/test_endpoint_ignore.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | import unittest 3 | 4 | from flask_testing import TestCase as FlaskTestCase 5 | 6 | from flask_profiler.flask_profiler import is_ignored 7 | from .basetest import BasetTest, flask_profiler 8 | 9 | 10 | class EndpointIgnoreTestCase(BasetTest, FlaskTestCase): 11 | 12 | def tearDown(self): 13 | pass 14 | 15 | def test_01__is_ignored(self): 16 | conf = { 17 | "ignore": [ 18 | "^/static/.*", 19 | "^/api/settings/\w+/secret/" 20 | ] 21 | } 22 | 23 | ignored_routes = [ 24 | "/static/file", 25 | "/static/", 26 | "/static/a/b/", 27 | "/api/settings/system/secret/", 28 | "/api/settings/common/secret/" 29 | ] 30 | 31 | for s in ignored_routes: 32 | self.assertEqual(is_ignored(s, conf), True, "{} needs to be ignored.".format(s)) 33 | 34 | not_ignored_routes = [ 35 | "/static", 36 | "/api/static/", 37 | "/api/settings/system/name/" 38 | ] 39 | 40 | for s in not_ignored_routes: 41 | self.assertEqual(is_ignored(s, conf), False, "{} cannot be ignored.".format(s)) 42 | 43 | def test_02_ignored_endpoints(self): 44 | ignored_routes = [ 45 | "/static/file", 46 | "/static/", 47 | "/static/a/b/", 48 | "/api/settings/system/name/" 49 | ] 50 | for s in ignored_routes: 51 | self.client.get(s) 52 | 53 | measurements = list(flask_profiler.collection.filter()) 54 | self.assertEqual(len(measurements), 0) 55 | 56 | not_ignored_routes = [ 57 | "/api/settings/personal/name/", 58 | "/api/static/" 59 | ] 60 | for s in not_ignored_routes: 61 | print(self.client.get(s)) 62 | 63 | measurements = list(flask_profiler.collection.filter()) 64 | self.assertEqual(len(measurements), 2) 65 | 66 | 67 | if __name__ == '__main__': 68 | unittest.main() 69 | -------------------------------------------------------------------------------- /tests/test_measure_endpoint.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | import unittest 3 | 4 | from flask_testing import TestCase as FlaskTestCase 5 | 6 | from .basetest import BasetTest, BaseTest2, flask_profiler 7 | 8 | 9 | class EndpointMeasurementTest(BasetTest, FlaskTestCase): 10 | 11 | def tearDown(self): 12 | pass 13 | 14 | def test_01_return_value(self): 15 | name = "john" 16 | response = self.client.get("/api/people/{}".format(name)) 17 | # converting because in python 3, response data becomes binary not utf-8 18 | r = response.data.decode("utf-8", "strict") 19 | self.assertEqual(r, name) 20 | 21 | def test_02_without_profiler(self): 22 | response = self.client.get("/api/without/profiler") 23 | # converting because in python 3, response data becomes binary not utf-8 24 | r = response.data.decode("utf-8", "strict") 25 | 26 | self.assertEqual(r, "without profiler") 27 | measurements = list(flask_profiler.collection.filter()) 28 | self.assertEqual(len(measurements), 0) 29 | 30 | def test_02_with_profiler(self): 31 | response = self.client.get("/api/with/profiler/hello?q=1") 32 | # converting because in python 3, response data becomes binary not utf-8 33 | r = response.data.decode("utf-8", "strict") 34 | self.assertEqual(r, "with profiler") 35 | 36 | measurements = list(flask_profiler.collection.filter()) 37 | self.assertEqual(len(measurements), 1) 38 | m = measurements[0] 39 | self.assertEqual(m["name"], "/api/with/profiler/") 40 | self.assertEqual(m["method"], "GET") 41 | self.assertEqual(m["kwargs"], {"message": "hello"}) 42 | self.assertEqual(m["context"]["args"], {"q": "1"}) 43 | 44 | 45 | class EndpointMeasurementTest2(BaseTest2, FlaskTestCase): 46 | 47 | def test_01_profiler(self): 48 | name = "foo" 49 | response = self.client.get("/api/people/{}".format(name)) 50 | measurements = list(flask_profiler.collection.filter()) 51 | self.assertEqual(len(measurements), 1) 52 | r = response.data.decode("utf-8", "strict") 53 | self.assertEqual(r, name) 54 | 55 | def test_02_profiler(self): 56 | self.client.get("/api/people/foo") 57 | self.client.get("/api/people/foo") 58 | self.client.get("/api/with/profiler/hello?q=2") 59 | measurements = list(flask_profiler.collection.filter()) 60 | self.assertEqual(len(measurements), 3) 61 | test_flag = False 62 | for list_element in measurements: 63 | if list_element["name"] == "/api/with/profiler/": 64 | test_flag = True 65 | self.assertEqual(list_element["name"], "/api/with/profiler/") 66 | self.assertEqual(list_element["method"], "GET") 67 | self.assertEqual(list_element["kwargs"], {"message": "hello"}) 68 | self.assertEqual(list_element["context"]["args"], {"q": "2"}) 69 | self.assertEqual(True, test_flag) 70 | 71 | 72 | if __name__ == '__main__': 73 | unittest.main() 74 | -------------------------------------------------------------------------------- /tests/test_measurement.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | import time 3 | import unittest 4 | 5 | from .basetest import BasetTest, measure, flask_profiler 6 | 7 | 8 | def doWait(seconds, **kwargs): 9 | time.sleep(seconds) 10 | return seconds 11 | 12 | 13 | class MeasurementTest(BasetTest): 14 | 15 | def setUp(self): 16 | flask_profiler.collection.truncate() 17 | 18 | def test_01_returnValue(self): 19 | wrapped = measure(doWait, "doWait", "call", context=None) 20 | waitSeconds = 1 21 | result = wrapped(waitSeconds) 22 | self.assertEqual(waitSeconds, result) 23 | 24 | def test_02_measurement(self): 25 | wrapped = measure(doWait, "doWait", "call", context=None) 26 | waitSeconds = 2 27 | result = wrapped(waitSeconds) 28 | m = list(flask_profiler.collection.filter())[0] 29 | self.assertEqual(m["name"], "doWait") 30 | self.assertEqual(float(m["elapsed"]) >= waitSeconds, True) 31 | 32 | def test_03_measurement_params(self): 33 | context = {"token": "x"} 34 | name = "name_of_func" 35 | method = "invoke" 36 | wrapped = measure(doWait, name, method, context=context) 37 | 38 | waitSeconds = 1 39 | kwargs = {"k1": "kval1", "k2": "kval2"} 40 | result = wrapped(waitSeconds, **kwargs) 41 | m = list(flask_profiler.collection.filter())[0] 42 | self.assertEqual(m["name"], name) 43 | self.assertEqual(m["method"], method) 44 | self.assertEqual(m["args"][0], waitSeconds) 45 | self.assertEqual(m["kwargs"], kwargs) 46 | self.assertEqual(m["context"], context) 47 | self.assertTrue(float(m["elapsed"]) >= waitSeconds) 48 | 49 | if __name__ == '__main__': 50 | unittest.main() 51 | --------------------------------------------------------------------------------