├── .gitattributes ├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── python-publish.yml │ └── python-tests.yml ├── .gitignore ├── .readthedocs.yaml ├── LICENSE ├── MANIFEST.in ├── Procfile ├── README.md ├── codecov.yml ├── config.cfg ├── config └── install.sh ├── docs ├── Makefile ├── README.md ├── README.rst ├── changelog.rst ├── conf.py ├── configuration.rst ├── contact.rst ├── developing.rst ├── functionality.rst ├── img │ ├── architecture.png │ ├── database_scheme.png │ ├── fmd_db.png │ ├── fmd_video.gif │ ├── header.png │ ├── monitoring_levels.png │ ├── ss1.png │ ├── ss2.png │ ├── ss3.png │ ├── ss4.png │ └── ss5.png ├── index.rst ├── installation.rst ├── known_issues.rst ├── migration.rst ├── requirements.txt └── todo.rst ├── flask_monitoringdashboard ├── __init__.py ├── cli.py ├── constants.json ├── controllers │ ├── __init__.py │ ├── endpoints.py │ ├── exceptions.py │ ├── outliers.py │ ├── profiler.py │ ├── requests.py │ └── versions.py ├── core │ ├── __init__.py │ ├── auth.py │ ├── blueprints.py │ ├── cache.py │ ├── colors.py │ ├── config │ │ ├── __init__.py │ │ └── parser.py │ ├── custom_graph │ │ └── __init__.py │ ├── database_pruning.py │ ├── date_interval.py │ ├── exceptions │ │ ├── __init__.py │ │ ├── exception_collector.py │ │ ├── stack_frame_parsing.py │ │ ├── stack_trace_hashing.py │ │ └── text_hash.py │ ├── get_ip.py │ ├── group_by.py │ ├── logger.py │ ├── measurement.py │ ├── profiler │ │ ├── __init__.py │ │ ├── base_profiler.py │ │ ├── outlier_profiler.py │ │ ├── performance_profiler.py │ │ ├── stacktrace_profiler.py │ │ └── util │ │ │ ├── __init__.py │ │ │ ├── grouped_stack_line.py │ │ │ ├── path_hash.py │ │ │ └── string_hash.py │ ├── reporting │ │ ├── __init__.py │ │ └── questions │ │ │ ├── __init__.py │ │ │ ├── median_latency.py │ │ │ ├── report_question.py │ │ │ └── status_code_distribution.py │ ├── rules.py │ ├── telemetry.py │ ├── timezone.py │ └── utils.py ├── database │ ├── __init__.py │ ├── auth.py │ ├── code_line.py │ ├── count.py │ ├── count_group.py │ ├── custom_graph.py │ ├── data_grouped.py │ ├── endpoint.py │ ├── exception_frame.py │ ├── exception_message.py │ ├── exception_occurrence.py │ ├── exception_stack_line.py │ ├── exception_type.py │ ├── file_path.py │ ├── function_definition.py │ ├── function_location.py │ ├── outlier.py │ ├── request.py │ ├── stack_line.py │ ├── stack_trace_snapshot.py │ └── versions.py ├── frontend │ ├── .babelrc │ ├── js │ │ ├── app.js │ │ ├── controllers │ │ │ ├── OverviewController.js │ │ │ ├── apiPerformance.js │ │ │ ├── configuration.js │ │ │ ├── customGraph.js │ │ │ ├── dailyUtilization.js │ │ │ ├── databaseManagementController.js │ │ │ ├── endpointException.js │ │ │ ├── endpointGroupedProfiler.js │ │ │ ├── endpointHourlyLoad.js │ │ │ ├── endpointOutlier.js │ │ │ ├── endpointProfiler.js │ │ │ ├── endpointUsers.js │ │ │ ├── endpointVersion.js │ │ │ ├── endpointVersionIP.js │ │ │ ├── endpointVersionUser.js │ │ │ ├── exception.js │ │ │ ├── hourlyLoad.js │ │ │ ├── monitorLevel.js │ │ │ ├── multiVersion.js │ │ │ ├── reporting.js │ │ │ ├── statusCodeDistribution.js │ │ │ ├── telemetryController.js │ │ │ └── util.js │ │ ├── directives.js │ │ ├── filters.js │ │ └── services │ │ │ ├── endpoint.js │ │ │ ├── form.js │ │ │ ├── info.js │ │ │ ├── menu.js │ │ │ ├── modal.js │ │ │ ├── pagination.js │ │ │ └── plotly.js │ ├── package-lock.json │ ├── package.json │ ├── sass │ │ ├── app.scss │ │ └── custom.css │ └── webpack.config.js ├── main.py ├── static │ ├── css │ │ └── prism.css │ ├── elements │ │ ├── endpointDetails.html │ │ ├── menu.html │ │ ├── modal.html │ │ ├── monitorLevel.html │ │ └── pagination.html │ ├── fonts │ │ └── fa-solid-900.woff2 │ ├── img │ │ ├── favicon.ico │ │ └── header.png │ ├── js │ │ └── prism.js │ └── pages │ │ ├── configuration.html │ │ ├── database_management.html │ │ ├── exception_overview.html │ │ ├── exceptions.html │ │ ├── grouped_profiler.html │ │ ├── outliers.html │ │ ├── overview.html │ │ ├── plotly_graph.html │ │ ├── profiler.html │ │ ├── reporting.html │ │ ├── status_code_distribution.html │ │ └── telemetry.html ├── templates │ ├── fmd_base.html │ └── fmd_login.html └── views │ ├── __init__.py │ ├── auth.py │ ├── custom.py │ ├── deployment.py │ ├── endpoint.py │ ├── exception.py │ ├── outlier.py │ ├── profiler.py │ ├── pruning.py │ ├── reporting.py │ ├── request.py │ ├── telemetry.py │ └── version.py ├── migration ├── migrate_sqlite_mysql.py ├── migrate_v1_to_v2.py └── migrate_v2_to_v3.py ├── requirements-dev.txt ├── requirements-micro.txt ├── requirements.txt ├── setup.py └── tests ├── __init__.py ├── api ├── __init__.py ├── test_auth.py ├── test_custom.py ├── test_deployment.py ├── test_endpoint.py ├── test_outlier.py ├── test_profiler.py ├── test_reporting.py ├── test_request.py └── test_version.py ├── conftest.py ├── fixtures ├── __init__.py ├── dashboard.py ├── database.py └── models.py └── unit ├── __init__.py ├── core ├── __init__.py ├── config │ ├── __init__.py │ └── test_config.py ├── profiler │ ├── __init__.py │ ├── test_profiler.py │ ├── test_stacktrace_profiler.py │ └── util │ │ ├── __init__.py │ │ ├── test_grouped_stack_line.py │ │ ├── test_path_hash.py │ │ ├── test_string_hash.py │ │ └── test_util.py ├── test_blueprints.py ├── test_colors.py ├── test_group_by.py ├── test_measurement.py ├── test_rules.py └── test_timezone.py └── database ├── __init__.py ├── test_auth.py ├── test_codeline.py ├── test_count.py ├── test_count_group.py ├── test_data_grouped.py ├── test_endpoint.py ├── test_exception_message.py ├── test_exception_occurrence.py ├── test_exception_stack_line.py ├── test_exception_type.py ├── test_function_definition.py ├── test_outlier.py ├── test_request.py ├── test_stack_trace_snapshot.py └── test_stackline.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # exclude minified js from being counted as source code 2 | /flask_monitoringdashboard/static/* linguist-generated 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. MacOS 10.14.3] 28 | - Browser [e.g. chrome, safari] 29 | - FMD Version [e.g. 2.1.4] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - flask_monitoringdashboard/constants.json 9 | workflow_dispatch: 10 | 11 | jobs: 12 | 13 | test: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v1 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install setuptools --upgrade 30 | pip install -r requirements-dev.txt 31 | - name: Lint with flake8 32 | run: | 33 | flake8 . --exit-zero --max-line-length=100 --ignore=F401,W503 --max-complexity=10 --max-line-length=100 34 | - name: Test with pytest 35 | run: | 36 | pytest 37 | 38 | 39 | publish: 40 | runs-on: ubuntu-latest 41 | needs: [test] 42 | steps: 43 | - uses: actions/checkout@v2 44 | - name: Set up Python 45 | uses: actions/setup-python@v1 46 | with: 47 | python-version: '3.x' 48 | - name: Install dependencies 49 | run: | 50 | python -m pip install --upgrade pip 51 | pip install setuptools wheel twine 52 | - name: Build the front-end 53 | run: | 54 | cd flask_monitoringdashboard 55 | cd frontend 56 | npm i 57 | npm run build 58 | - name: Build and publish 59 | env: 60 | TWINE_USERNAME: __token__ 61 | TWINE_PASSWORD: ${{ secrets.PYPI_PUBLISH_TOKEN }} 62 | run: | 63 | python setup.py sdist bdist_wheel 64 | twine upload dist/* 65 | -------------------------------------------------------------------------------- /.github/workflows/python-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Python Tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v1 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install setuptools --upgrade 23 | pip install -r requirements-dev.txt 24 | - name: Lint with flake8 25 | run: | 26 | flake8 . --exit-zero --max-line-length=100 --ignore=F401,W503 --max-complexity=10 --max-line-length=100 27 | - name: Test with pytest 28 | run: | 29 | pytest --cov --cov-report=xml 30 | - name: Upload coverage to Codecov 31 | uses: codecov/codecov-action@v1 32 | with: 33 | file: ./coverage.xml 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | .venv 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # IPython Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # dotenv 80 | .env 81 | 82 | # virtualenv 83 | venv/ 84 | ENV/ 85 | 86 | # OS X 87 | .DS_Store 88 | 89 | # Idea 90 | .idea/ 91 | 92 | # config file 93 | *.cfg 94 | 95 | # database file 96 | *.db 97 | 98 | # Spyder project settings 99 | .spyderproject 100 | 101 | # Rope project settings 102 | .ropeproject 103 | 104 | # own scripts 105 | flask_monitoringdashboard/make_requests.py 106 | flask_monitoringdashboard/session_test.py 107 | flask_monitoringdashboard/frontend/node_modules/ 108 | frontend/build/ 109 | 110 | # webpack generated 111 | flask_monitoringdashboard/static/css/main.css 112 | flask_monitoringdashboard/static/js/app.js 113 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.12" 12 | # You can also specify other tool versions: 13 | # nodejs: "20" 14 | # rust: "1.70" 15 | # golang: "1.20" 16 | 17 | # Build documentation in the "docs/" directory with Sphinx 18 | sphinx: 19 | configuration: docs/conf.py 20 | # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs 21 | # builder: "dirhtml" 22 | # Fail on all warnings to avoid broken references 23 | # fail_on_warning: true 24 | 25 | # Optionally build your docs in additional formats such as PDF and ePub 26 | # formats: 27 | # - pdf 28 | # - epub 29 | 30 | # Optional but recommended, declare the Python requirements required 31 | # to build your documentation 32 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 33 | python: 34 | install: 35 | - requirements: docs/requirements.txt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Flask-Monitoring-Dashboard 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include flask_monitoringdashboard/static * 2 | recursive-include flask_monitoringdashboard/templates * 3 | include requirements.txt 4 | include README.md 5 | include docs/changelog.rst 6 | include docs/README.rst 7 | include flask_monitoringdashboard/constants.json 8 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: pip install gunicorn && gunicorn flask_monitoringdashboard.main:app --log-file=- -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | token: bda4fd4f-e8ae-4cdd-8b9a-3bb3a0932003 3 | 4 | coverage: 5 | status: 6 | patch: off 7 | -------------------------------------------------------------------------------- /config.cfg: -------------------------------------------------------------------------------- 1 | [dashboard] 2 | APP_VERSION=1.0 3 | GIT=//.git/ 4 | BLUEPRINT_NAME=dashboard 5 | CUSTOM_LINK=dashboard 6 | MONITOR_LEVEL=1 7 | OUTLIER_DETECTION_CONSTANT=2.5 8 | BRAND_NAME=Flask Monitoring Dashboard 9 | TITLE_NAME=Flask-MonitoringDashboard 10 | DESCRIPTION=Automatically monitor the evolving performance of Flask/Python web services 11 | SHOW_LOGIN_BANNER=True 12 | SHOW_LOGIN_FOOTER=True 13 | 14 | [authentication] 15 | USERNAME=admin 16 | PASSWORD=admin 17 | GUEST_USERNAME=guest 18 | GUEST_PASSWORD=['dashboardguest!', 'second_pw!'] 19 | SECURITY_TOKEN=cc83733cb0af8b884ff6577086b87909 20 | 21 | [database] 22 | DATABASE=sqlite://///dashboard.db 23 | 24 | [visualization] 25 | TIMEZONE=Europe/Amsterdam 26 | COLORS={'main':'[0,97,255]', 27 | 'static':'[255,153,0]'} 28 | -------------------------------------------------------------------------------- /config/install.sh: -------------------------------------------------------------------------------- 1 | # Run from the root directory with ./config/install.sh 2 | 3 | python -m venv env 4 | source env/bin/activate 5 | pip install -r requirements-dev.txt 6 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | The documentation is generated using [Sphinx](http://www.sphinx-doc.org/en/master/). 3 | 4 | If you want to generate the documentation, you can run: 5 | Linux: 6 | ``` 7 | make html 8 | ``` 9 | 10 | Windows: 11 | ``` 12 | sphinx-build -b html . _build 13 | ``` 14 | 15 | # Installation 16 | The following packages are required to generate the documentation: 17 | ``` 18 | pip install Sphinx 19 | pip install Flask-Sphinx-Themes 20 | ``` 21 | 22 | # Read The Docs 23 | You can also find the documentation on see [this site](http://flask-monitoringdashboard.readthedocs.io). -------------------------------------------------------------------------------- /docs/contact.rst: -------------------------------------------------------------------------------- 1 | Contact 2 | ======= 3 | This page provides information about how to ask a question, or post an issue. 4 | 5 | Developing-team 6 | --------------- 7 | Currently, the team consists of three active developers: 8 | 9 | .. table:: 10 | :widths: 2, 7 11 | 12 | +------------+----------------------------------------------------------------------+ 13 | | |Picture1| | | **Patrick Vogel** | 14 | | | | | 15 | | | | Project Leader | 16 | | | | | 17 | | | | **E-mail:** `patrickvogel@live.nl `_. | 18 | +------------+----------------------------------------------------------------------+ 19 | | |Picture2| | | **Bogdan Petre** | 20 | | | | | 21 | | | | Core Developer | 22 | +------------+----------------------------------------------------------------------+ 23 | | |Picture3| | | **Thijs Klooster** | 24 | | | | | 25 | | | | Test Monitor Specialist | 26 | +------------+----------------------------------------------------------------------+ 27 | 28 | .. |Picture1| image:: https://avatars2.githubusercontent.com/u/17162650?s=460&v=4 29 | ..:width: 100px 30 | 31 | .. |Picture2| image:: https://avatars2.githubusercontent.com/u/7281856?s=400&v=4 32 | ..:width: 100px 33 | 34 | .. |Picture3| image:: https://avatars3.githubusercontent.com/u/17165311?s=400&v=4 35 | ..:width: 100px 36 | 37 | 38 | Found a bug? 39 | ------------ 40 | Post an `issue on Github `_. 41 | You can use the template below for the right formatting: 42 | 43 | .. topic:: Issue Template 44 | 45 | - Expected Behavior 46 | 47 | *Tell us what should happen* 48 | 49 | - Current Behavior 50 | 51 | *Tell us what happens instead of the expected behavior* 52 | 53 | - Possible Solution 54 | 55 | *Not obligatory, but suggest a fix/reason for the bug* 56 | 57 | - Steps to Reproduce 58 | 59 | *Provide a link to a live example, or an unambiguous set of steps to reproduce this bug. Include code to reproduce, if relevant* 60 | 61 | 1. 62 | 63 | 2. 64 | 65 | 3. 66 | 67 | 4. 68 | 69 | - Context (Environment) 70 | 71 | *How has this issue affected you? What are you trying to accomplish? 72 | Providing context helps us come up with a solution that is most useful in the real world* 73 | 74 | - Detailed Description 75 | 76 | *Provide a detailed description of the change or addition you are proposing* 77 | 78 | - Possible Implementation 79 | 80 | *Not obligatory, but suggest an idea for implementing addition or change* 81 | 82 | -------------------------------------------------------------------------------- /docs/img/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flask-dashboard/Flask-MonitoringDashboard/3cebbd62a8fd248313eeb630375d5dd07a6ff572/docs/img/architecture.png -------------------------------------------------------------------------------- /docs/img/database_scheme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flask-dashboard/Flask-MonitoringDashboard/3cebbd62a8fd248313eeb630375d5dd07a6ff572/docs/img/database_scheme.png -------------------------------------------------------------------------------- /docs/img/fmd_db.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flask-dashboard/Flask-MonitoringDashboard/3cebbd62a8fd248313eeb630375d5dd07a6ff572/docs/img/fmd_db.png -------------------------------------------------------------------------------- /docs/img/fmd_video.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flask-dashboard/Flask-MonitoringDashboard/3cebbd62a8fd248313eeb630375d5dd07a6ff572/docs/img/fmd_video.gif -------------------------------------------------------------------------------- /docs/img/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flask-dashboard/Flask-MonitoringDashboard/3cebbd62a8fd248313eeb630375d5dd07a6ff572/docs/img/header.png -------------------------------------------------------------------------------- /docs/img/monitoring_levels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flask-dashboard/Flask-MonitoringDashboard/3cebbd62a8fd248313eeb630375d5dd07a6ff572/docs/img/monitoring_levels.png -------------------------------------------------------------------------------- /docs/img/ss1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flask-dashboard/Flask-MonitoringDashboard/3cebbd62a8fd248313eeb630375d5dd07a6ff572/docs/img/ss1.png -------------------------------------------------------------------------------- /docs/img/ss2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flask-dashboard/Flask-MonitoringDashboard/3cebbd62a8fd248313eeb630375d5dd07a6ff572/docs/img/ss2.png -------------------------------------------------------------------------------- /docs/img/ss3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flask-dashboard/Flask-MonitoringDashboard/3cebbd62a8fd248313eeb630375d5dd07a6ff572/docs/img/ss3.png -------------------------------------------------------------------------------- /docs/img/ss4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flask-dashboard/Flask-MonitoringDashboard/3cebbd62a8fd248313eeb630375d5dd07a6ff572/docs/img/ss4.png -------------------------------------------------------------------------------- /docs/img/ss5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flask-dashboard/Flask-MonitoringDashboard/3cebbd62a8fd248313eeb630375d5dd07a6ff572/docs/img/ss5.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. figure :: img/header.png 2 | :width: 100% 3 | 4 | **Automatically monitor the evolving performance of Flask/Python web services** 5 | 6 | What is Flask-MonitoringDashboard? 7 | --------------------------------------- 8 | The Flask Monitoring Dashboard is designed to easily monitor your Flask application. 9 | 10 | 11 | Functionality 12 | ------------- 13 | The Flask Monitoring Dashboard is an extension that offers 4 main functionalities with little effort from the Flask developer: 14 | 15 | - **Monitor the performance and utilization:** 16 | The Dashboard allows you to see which endpoints process a lot of requests and how fast. 17 | Additionally, it provides information about the evolving performance of an endpoint throughout different versions if you're using git. 18 | 19 | - **Profile requests and endpoints:** 20 | The execution path of every request is tracked and stored into the database. This allows you to gain 21 | insight over which functions in your code take the most time to execute. Since all requests for an 22 | endpoint are also merged together, the Dashboard provides an overview of which functions are used in 23 | which endpoint. 24 | 25 | - **Collect extra information about outliers:** 26 | Outliers are requests that take much longer to process than regular requests. 27 | The Dashboard automatically detects that a request is an outlier and stores extra information about it (stack trace, request values, Request headers, Request environment). 28 | 29 | - **Collect additional information about your Flask-application:** 30 | Suppose you have an User-table and you want to know how many users are registered on your Flask-application. 31 | Then, you can run the following query: 'SELECT Count(*) FROM USERS;'. But this is just annoying to do regularly. 32 | Therefore, you can configure this in the Flask-MonitoringDashboard, which will provide you this information per day (or other time interval). 33 | 34 | - **Track exceptions:** 35 | If your application has unhandled exceptions they will automatically be captured together with full stack traces and 36 | displayed in the dashboard, if you have monitoring level 1 or above. You can also explicitly capture individual 37 | exceptions with the `capture_exception` function. 38 | 39 | 40 | For more advanced documentation, have a look at the `the detailed functionality page `_. 41 | 42 | User's Guide 43 | ------------ 44 | If you are interested in the Flask-MonitoringDashboard, you can find more information in the links below: 45 | 46 | .. toctree:: 47 | :maxdepth: 2 48 | 49 | installation 50 | 51 | configuration 52 | 53 | functionality 54 | 55 | known_issues 56 | 57 | Developer information 58 | --------------------- 59 | .. toctree:: 60 | :maxdepth: 2 61 | 62 | contact 63 | 64 | developing 65 | 66 | migration 67 | 68 | todo 69 | 70 | changelog 71 | -------------------------------------------------------------------------------- /docs/known_issues.rst: -------------------------------------------------------------------------------- 1 | Known Issues 2 | =============== 3 | 4 | This page provides an overview of known bugs, workarounds, and limitations of the 5 | Flask Monitoring Dashboard. 6 | 7 | Deploying with mod_wsgi (Apache) 8 | --------------------------------- 9 | 10 | 11 | The FMD relies on the ``scipy`` package for some of the statistical tests 12 | used in the *Reports* feature. This package is incompatible with 13 | ``mod_wsgi`` by default, causing the deployment to fail. This is a common 14 | issue `[1] `_ 15 | `[2] `_ 16 | and can be solved by setting the 17 | 18 | .. code-block:: xml 19 | 20 | WSGIApplicationGroup %{GLOBAL} 21 | 22 | directive in your WSGI configuration file, as described in the linked posts. -------------------------------------------------------------------------------- /docs/migration.rst: -------------------------------------------------------------------------------- 1 | Migration 2 | ========= 3 | 4 | Migrating from 1.X.X to 2.0.0 5 | ----------------------------- 6 | Version 2.0.0 offers a lot more functionality, including Request- and Endpoint-profiling. 7 | 8 | There are two migrations that you have to do, before you can use version 2.0.0. 9 | 10 | 1. **Migrate the database:** Since version 2.0.0 has a different database scheme, the 11 | Flask-MonitoringDashboard cannot automatically migrate to this version. 12 | 13 | We have created a script for you that can achieve this. It migrates the data in the existing 14 | database into a new database, without removing the existing database. 15 | 16 | You can find `the migration script here`_. 17 | 18 | .. _`the migration script here`: https://github.com/flask-dashboard/Flask-MonitoringDashboard/tree/master/migration/migrate_v1_to_v2.py 19 | 20 | If you want to run this script, you need to be aware of the following: 21 | 22 | - If you already have version 1.X.X of the Flask-MonitoringDashboard installed, first update to 23 | 2.0.0 before running this script. You can update a package by: 24 | 25 | .. code-block:: bash 26 | 27 | pip install flask_monitoringdashboard --upgrade 28 | 29 | - set **OLD_DB_URL** on line 16, such that it points to your existing database. 30 | 31 | - set **NEW_DB_URL** on line 17, to a new database name version. Note that they can't be the same. 32 | 33 | - Once you have migrated your database, you have to update the database location in your configuration-file. 34 | 35 | 36 | 2. **Migrate the configuration file**: You also have to update the configuration file completely, since we've 37 | re factored this to make it more clear. The main difference is that several properties have been re factored 38 | to a new header-name. 39 | 40 | For an example of a new configuration file, see `this configuration file`_. 41 | 42 | .. _`this configuration file`: https://github.com/flask-dashboard/Flask-MonitoringDashboard/tree/master/config.cfg 43 | 44 | 45 | Migrating from 2.X.X to 3.0.0 46 | ----------------------------- 47 | Version 3.0.0 adds functionality for tracking return status codes for each endpoint. 48 | 49 | This requires a minimal change to the database: adding the 'status_code' (INT) field to the Request table. 50 | 51 | You can add the field by hand, or you can run `the corresponding migration script`_: 52 | 53 | .. _`the corresponding migration script`: https://github.com/flask-dashboard/Flask-MonitoringDashboard/tree/master/migration/migrate_v2_to_v3.py 54 | 55 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx 2 | Flask-Sphinx-Themes 3 | -------------------------------------------------------------------------------- /docs/todo.rst: -------------------------------------------------------------------------------- 1 | TODO List 2 | ========================================================================= 3 | 4 | All things that can be improved in Flask-MonitoringDashboard are listed below. 5 | 6 | Features to be implemented 7 | -------------------------- 8 | - create a memory usage graph for the outliers, similar to the cpu usage one 9 | - allow for endpoint purge and/or deletion 10 | - allow for merging of endpoints 11 | - improve code readability through comments 12 | 13 | Work in progress 14 | ---------------- 15 | 16 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/cli.py: -------------------------------------------------------------------------------- 1 | """Contains custom commands for the Flask-MonitoringDashboard 2 | For a list of all commands, open a terminal and type: 3 | 4 | >>> flask fmd --help 5 | """ 6 | 7 | import click 8 | from flask.cli import with_appcontext 9 | 10 | 11 | @click.group() 12 | def fmd(): 13 | pass 14 | 15 | 16 | @fmd.command() 17 | @with_appcontext 18 | def init_db(): 19 | # Importing the database package is enough 20 | import flask_monitoringdashboard.database 21 | 22 | print('Flask-MonitoringDashboard database has been created') 23 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/constants.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "4.0.4", 3 | "author": "The FMD Contributors", 4 | "email": "flask.monitoringdashboard@gmail.com" 5 | } 6 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/controllers/__init__.py: -------------------------------------------------------------------------------- 1 | """This directory is used for implementing the logic between the database and the API.""" 2 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/controllers/outliers.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | from flask_monitoringdashboard.core.logger import log 4 | from flask_monitoringdashboard.core.colors import get_color 5 | from flask_monitoringdashboard.core.timezone import to_local_datetime 6 | from flask_monitoringdashboard.core.utils import simplify 7 | from flask_monitoringdashboard.database import row2dict 8 | from flask_monitoringdashboard.database.outlier import get_outliers_cpus, get_outliers_sorted 9 | 10 | 11 | def get_outlier_graph(session, endpoint_id): 12 | """ 13 | :param session: session for the database 14 | :param endpoint_id: id of the endpoint 15 | :return: a list with data about each CPU performance 16 | """ 17 | all_cpus = get_outliers_cpus(session, endpoint_id) 18 | cpu_data = [ast.literal_eval(cpu) for cpu in all_cpus] 19 | 20 | return [ 21 | {'name': 'CPU core %d' % idx, 'values': simplify(data, 50), 'color': get_color(idx)} 22 | for idx, data in enumerate(zip(*cpu_data)) 23 | ] 24 | 25 | 26 | def get_outlier_table(session, endpoint_id, offset, per_page): 27 | """ 28 | :param session: session for the database 29 | :param endpoint_id: id of the endpoint 30 | :param offset: number of items to be skipped 31 | :param per_page: maximum number of items to be returned 32 | :return: a list of length at most 'per_page' with data about each outlier 33 | """ 34 | table = get_outliers_sorted(session, endpoint_id, offset, per_page) 35 | for idx, row in enumerate(table): 36 | row.request.time_requested = to_local_datetime(row.request.time_requested) 37 | try: 38 | row.request_url = row.request_url.decode('utf-8') 39 | except Exception as e: 40 | log(e) 41 | dict_request = row2dict(row.request) 42 | table[idx] = row2dict(row) 43 | table[idx]['request'] = dict_request 44 | return table 45 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/controllers/profiler.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | import numpy 4 | 5 | from flask_monitoringdashboard.core.profiler.util import PathHash 6 | from flask_monitoringdashboard.core.timezone import to_local_datetime 7 | from flask_monitoringdashboard.database import row2dict 8 | from flask_monitoringdashboard.database.stack_line import ( 9 | get_profiled_requests, 10 | get_grouped_profiled_requests, 11 | ) 12 | 13 | 14 | def get_profiler_table(session, endpoint_id, offset, per_page): 15 | """ 16 | :param session: session for the database 17 | :param endpoint_id: endpoint to filter on 18 | :param offset: number of items that are skipped 19 | :param per_page: number of items that are returned (at most) 20 | """ 21 | table = get_profiled_requests(session, endpoint_id, offset, per_page) 22 | 23 | for idx, row in enumerate(table): 24 | row.time_requested = to_local_datetime(row.time_requested) 25 | table[idx] = row2dict(row) 26 | stack_lines = [] 27 | for line in row.stack_lines: 28 | obj = row2dict(line) 29 | obj['code'] = row2dict(line.code) 30 | stack_lines.append(obj) 31 | table[idx]['stack_lines'] = stack_lines 32 | return table 33 | 34 | 35 | def get_grouped_profiler(session, endpoint_id): 36 | """ 37 | :param session: session for the database 38 | :param endpoint_id: endpoint to filter on 39 | :return: 40 | """ 41 | requests = get_grouped_profiled_requests(session, endpoint_id) 42 | session.expunge_all() 43 | 44 | histogram = defaultdict(list) # path -> [list of values] 45 | path_hash = PathHash() 46 | 47 | for r in requests: 48 | for index, stack_line in enumerate(r.stack_lines): 49 | key = path_hash.get_stacklines_path(r.stack_lines, index) 50 | histogram[key].append(stack_line.duration) 51 | 52 | table = [] 53 | for key, duration_list in sorted(histogram.items(), key=lambda row: row[0]): 54 | table.append( 55 | { 56 | 'indent': path_hash.get_indent(key) - 1, 57 | 'code': path_hash.get_code(key), 58 | 'hits': len(duration_list), 59 | 'duration': sum(duration_list), 60 | 'std': numpy.std(duration_list), 61 | 'total_hits': len(requests), 62 | } 63 | ) 64 | return table 65 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/controllers/versions.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | 3 | from flask_monitoringdashboard.database import Request 4 | from flask_monitoringdashboard.database.count_group import get_value, count_requests_group 5 | from flask_monitoringdashboard.database.data_grouped import get_two_columns_grouped 6 | from flask_monitoringdashboard.database.endpoint import get_endpoint_by_name 7 | from flask_monitoringdashboard.database.versions import get_first_requests 8 | 9 | 10 | def get_2d_version_data(session, endpoint_id, versions, column_data, column): 11 | """ 12 | :param session: session for the database 13 | :param endpoint_id: id of the endpoint 14 | :param versions: a list of versions 15 | :param column_data: a is of the other column 16 | :param column: column from the Request table 17 | :return: a dict with 2d information about the version and another column 18 | """ 19 | first_request = get_first_requests(session, endpoint_id) 20 | values = get_two_columns_grouped(session, column, Request.endpoint_id == endpoint_id) 21 | data = [[get_value(values, (data, v)) for v in versions] for data in column_data] 22 | 23 | return { 24 | 'versions': [{'version': v, 'date': get_value(first_request, v)} for v in versions], 25 | 'data': data, 26 | } 27 | 28 | 29 | def get_version_user_data(session, endpoint_id, versions, users): 30 | return get_2d_version_data(session, endpoint_id, versions, users, Request.group_by) 31 | 32 | 33 | def get_version_ip_data(session, endpoint_id, versions, ips): 34 | return get_2d_version_data(session, endpoint_id, versions, ips, Request.ip) 35 | 36 | 37 | def get_multi_version_data(session, endpoints, versions): 38 | """ 39 | :param session: session for the database 40 | :param endpoints: a list of all endpoints for which the data must be 41 | collected (represented by their name) 42 | :param versions: a list of versions 43 | :return: a 2d list of data 44 | """ 45 | endpoints = [get_endpoint_by_name(session, name) for name in endpoints] 46 | requests = [count_requests_group(session, Request.version_requested == v) for v in versions] 47 | 48 | total_hits = numpy.zeros(len(versions)) 49 | hits = numpy.zeros((len(endpoints), len(versions))) 50 | 51 | for i, _ in enumerate(versions): 52 | total_hits[i] = max(1, sum([value for key, value in requests[i]])) 53 | 54 | for j, _ in enumerate(endpoints): 55 | for i, _ in enumerate(versions): 56 | hits[j][i] = get_value(requests[i], endpoints[j].id) * 100 / total_hits[i] 57 | return hits.tolist() 58 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/core/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Core files for the Flask Monitoring Dashboard. It contains the following packages: 3 | - config handles the configuration-file 4 | - forms handles generating and processing WTF-forms 5 | - plot handles the plotly library 6 | - profiler handles profiling requests 7 | 8 | It also contains the following files 9 | - auth.py handles authentication 10 | - colors.py handles color-hash 11 | - group_by.py handles the group_by functionality 12 | - info_box.py handles the generation of information box, one for each graph 13 | - measurements.py contains a number of wrappers, one for each monitoring level 14 | - rules.py includes a function that retrieves all rules of the entire Flask 15 | application, or for a specific endpoint. 16 | - timezone: handles utc-timezone <==> local-timezone 17 | - utils: for other functions 18 | """ 19 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/core/auth.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | from flask import session, redirect, url_for 4 | 5 | from flask_monitoringdashboard import config 6 | 7 | 8 | def admin_secure(func): 9 | """ 10 | When the user is not logged into the system, the user is requested to the login page. 11 | There are two types of user-modes: 12 | - admin: Can be visited with this wrapper. 13 | - guest: Cannot be visited with this wrapper. 14 | :param func: the endpoint to be wrapped. 15 | """ 16 | 17 | @wraps(func) 18 | def wrapper(*args, **kwargs): 19 | if session and session.get(config.link + '_logged_in'): 20 | if session.get(config.link + '_admin'): 21 | return func(*args, **kwargs) 22 | return redirect(url_for(config.blueprint_name + '.login')) 23 | 24 | return wrapper 25 | 26 | 27 | def secure(func): 28 | """ 29 | When the user is not logged into the system, the user is requested to the login page. 30 | There are two types of user-modes: 31 | - admin: Can be visited with this wrapper. 32 | - guest: Can be visited with this wrapper. 33 | :param func: the endpoint to be wrapped. 34 | """ 35 | 36 | @wraps(func) 37 | def wrapper(*args, **kwargs): 38 | if session and session.get(config.link + '_logged_in'): 39 | return func(*args, **kwargs) 40 | return redirect(url_for(config.blueprint_name + '.login')) 41 | 42 | return wrapper 43 | 44 | 45 | def is_admin(): 46 | return session and session.get(config.link + '_admin') 47 | 48 | 49 | def on_login(user): 50 | session[config.link + '_user_id'] = user.id 51 | session[config.link + '_logged_in'] = True 52 | if user.is_admin: 53 | session[config.link + '_admin'] = True 54 | 55 | 56 | def on_logout(): 57 | session.pop(config.link + '_logged_in', None) 58 | session.pop(config.link + '_admin', None) 59 | return redirect(url_for(config.blueprint_name + '.login')) 60 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/core/blueprints.py: -------------------------------------------------------------------------------- 1 | def get_blueprint(endpoint_name): 2 | blueprint = endpoint_name 3 | if '.' in endpoint_name: 4 | blueprint = endpoint_name.split('.')[0] 5 | return blueprint 6 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/core/colors.py: -------------------------------------------------------------------------------- 1 | from colorhash import ColorHash 2 | from flask_monitoringdashboard import config 3 | import re 4 | 5 | 6 | def get_color(hash): 7 | """ 8 | Returns an rgb-color (as string, which can be using in plotly) from a given hash, 9 | if no color for that string was specified in the config file. 10 | :param hash: the string that is translated into a color 11 | :return: a color (as string) 12 | """ 13 | if hash in config.colors: 14 | rgb = re.findall(r'\d+', config.colors[hash]) 15 | else: 16 | rgb = ColorHash(hash).rgb 17 | return 'rgb({0}, {1}, {2})'.format(rgb[0], rgb[1], rgb[2]) 18 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/core/custom_graph/__init__.py: -------------------------------------------------------------------------------- 1 | import atexit 2 | import os 3 | 4 | from apscheduler.schedulers import SchedulerAlreadyRunningError 5 | from apscheduler.schedulers.background import BackgroundScheduler 6 | 7 | from flask_monitoringdashboard.database import session_scope 8 | from flask_monitoringdashboard.database.custom_graph import ( 9 | add_value, 10 | get_graph_id_from_name, 11 | get_graphs, 12 | ) 13 | 14 | scheduler = BackgroundScheduler() 15 | 16 | 17 | def init(app): 18 | if not app.debug or os.environ.get('WERKZEUG_RUN_MAIN') == 'true': 19 | try: 20 | scheduler.start() 21 | print('Scheduler started') 22 | atexit.register(lambda: scheduler.shutdown()) 23 | except SchedulerAlreadyRunningError as err: 24 | print(err) 25 | 26 | 27 | def register_graph(name): 28 | with session_scope() as session: 29 | return get_graph_id_from_name(session, name) 30 | 31 | 32 | def add_background_job(func, graph_id, trigger, **schedule): 33 | def add_data(): 34 | with session_scope() as session: 35 | add_value(session, graph_id, func()) 36 | 37 | add_data() # already call once, so it can be verified that the function works 38 | scheduler.add_job(func=add_data, trigger=trigger, **schedule) 39 | 40 | 41 | def get_custom_graphs(): 42 | with session_scope() as session: 43 | result = get_graphs(session) 44 | session.expunge_all() 45 | return result 46 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/core/date_interval.py: -------------------------------------------------------------------------------- 1 | class DateInterval(object): 2 | def __init__(self, start_date, end_date): 3 | if start_date > end_date: 4 | raise ValueError('start_date must be before or equals to end_date') 5 | 6 | self._start_date = start_date 7 | self._end_date = end_date 8 | 9 | def start_date(self): 10 | return self._start_date 11 | 12 | def end_date(self): 13 | return self._end_date 14 | 15 | def __repr__(self): 16 | return str((self._start_date, self._end_date)) 17 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/core/exceptions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flask-dashboard/Flask-MonitoringDashboard/3cebbd62a8fd248313eeb630375d5dd07a6ff572/flask_monitoringdashboard/core/exceptions/__init__.py -------------------------------------------------------------------------------- /flask_monitoringdashboard/core/exceptions/exception_collector.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | import copy 3 | 4 | from sqlalchemy.orm import Session 5 | 6 | 7 | class ExceptionCollector: 8 | """ 9 | This class is for logging user captured exceptions, in the scope of the current request. 10 | It is just a DTO for transmitting the user captured exceptions and uncaught exceptions to the exception logger. 11 | """ 12 | 13 | def __init__(self) -> None: 14 | self.user_captured_exceptions: list[BaseException] = [] 15 | self.uncaught_exception: Union[BaseException, None] = None 16 | 17 | def add_user_captured_exc(self, e: BaseException): 18 | e_copy = _get_copy_of_exception(e) 19 | self.user_captured_exceptions.append(e_copy) 20 | 21 | def set_uncaught_exc(self, e: BaseException): 22 | e_copy = _get_copy_of_exception(e) 23 | self.uncaught_exception = e_copy 24 | 25 | def save_to_db(self, request_id: int, session: Session): 26 | 27 | from flask_monitoringdashboard.database.exception_occurrence import ( 28 | save_exception_occurence_to_db, 29 | ) 30 | 31 | """ 32 | Iterates over all the user captured exceptions and also a possible uncaught one, and saves each exception to the DB 33 | """ 34 | for e in self.user_captured_exceptions: 35 | save_exception_occurence_to_db( 36 | request_id, session, e, type(e), e.__traceback__, True 37 | ) 38 | 39 | e = self.uncaught_exception 40 | if e is not None: 41 | if e.__traceback__ is not None: 42 | # We have to choose the next frame as else it will include the evaluate function from measurement.py in the traceback 43 | # where it was temporaritly captured for logging by the ExceptionCollector, before getting reraised later 44 | e = e.with_traceback(e.__traceback__.tb_next) 45 | 46 | save_exception_occurence_to_db( 47 | request_id, session, e, type(e), e.__traceback__, False 48 | ) 49 | 50 | 51 | def _get_copy_of_exception(e: BaseException): 52 | """ 53 | Helper function to reraise the uncaught exception with its original traceback, 54 | The copy is made in order to preserve the original exception's stack trace 55 | """ 56 | if e is None: 57 | return None 58 | 59 | try: 60 | new_exc = e.__class__(*e.args) 61 | except Exception: 62 | try: 63 | new_exc = copy.deepcopy(e) 64 | except Exception: 65 | new_exc = e.__class__() 66 | 67 | if e.__traceback__: 68 | return new_exc.with_traceback(e.__traceback__) 69 | return new_exc 70 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/core/exceptions/stack_frame_parsing.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from types import FrameType 3 | 4 | from flask_monitoringdashboard.core.exceptions.text_hash import text_hash 5 | from flask_monitoringdashboard.database import FunctionDefinition 6 | 7 | 8 | def get_function_definition_from_frame(frame: FrameType) -> FunctionDefinition: 9 | 10 | f_def = FunctionDefinition() 11 | f_def.code = inspect.getsource(frame.f_code) 12 | f_def.code_hash = text_hash(f_def.code) 13 | f_def.name = frame.f_code.co_name[:256] 14 | return f_def 15 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/core/exceptions/stack_trace_hashing.py: -------------------------------------------------------------------------------- 1 | from flask_monitoringdashboard.core.exceptions.text_hash import text_hash 2 | 3 | import traceback 4 | from types import TracebackType 5 | from typing import Union 6 | 7 | from flask_monitoringdashboard.core.exceptions.stack_frame_parsing import ( 8 | get_function_definition_from_frame, 9 | ) 10 | 11 | 12 | def hash_stack_trace(exc, tb): 13 | """ 14 | Hashes the stack trace of an exception including the function definition 15 | of each frame in thehash_stack_trace python traceback-type. 16 | """ 17 | 18 | # Using the triple argument version to be compatible with Python 3.9 19 | stack_trace_string = "".join(traceback.format_exception(type(exc), exc, tb)) 20 | stack_trace_hash = text_hash(stack_trace_string) 21 | return _hash_traceback_type_object(stack_trace_hash, tb) 22 | 23 | 24 | def _hash_traceback_type_object(h: str, tb: Union[TracebackType, None]): 25 | if tb is None: 26 | return h 27 | 28 | f_def = get_function_definition_from_frame(tb.tb_frame) 29 | new_hash = text_hash(h + f_def.code_hash) 30 | 31 | return _hash_traceback_type_object(new_hash, tb.tb_next) 32 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/core/exceptions/text_hash.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | 4 | def text_hash(s: str): 5 | return hashlib.sha256(s.encode("utf-8")).hexdigest() 6 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/core/get_ip.py: -------------------------------------------------------------------------------- 1 | from flask import request 2 | 3 | from flask_monitoringdashboard import config 4 | from flask_monitoringdashboard.core.logger import log 5 | 6 | def get_ip(): 7 | """ 8 | :return: the ip address associated with the current request context 9 | """ 10 | if config.get_ip: 11 | try: 12 | return config.get_ip() 13 | except Exception as e: 14 | log('Failed to execute provided get_ip function: {}'.format(e)) 15 | # This is a reasonable fallback, but will not work for clients behind proxies. 16 | return request.environ['REMOTE_ADDR'] 17 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/core/group_by.py: -------------------------------------------------------------------------------- 1 | from flask_monitoringdashboard import config 2 | from flask_monitoringdashboard.core.logger import log 3 | 4 | PRIMITIVES = (bool, bytes, float, int, str) 5 | 6 | 7 | def recursive_group_by(argument): 8 | """ 9 | Returns the result of the given argument. The result is computed as: 10 | - If the argument is a primitive (i.e. str, bool, int, ...) return its value. 11 | - If the argument is a function, call the function. 12 | - If the argument is iterable (i.e. list or tuple), compute the result by iterating over the 13 | argument 14 | Return type is always a string 15 | """ 16 | 17 | if type(argument) in PRIMITIVES: 18 | return str(argument) 19 | 20 | if callable(argument): 21 | return recursive_group_by(argument()) 22 | 23 | # Try if the argument is iterable (i.e. tuple or list) 24 | try: 25 | result_list = [recursive_group_by(i) for i in argument] 26 | result_string = ','.join(result_list) 27 | return '({})'.format(result_string) 28 | except TypeError: 29 | # Cannot deal with this 30 | return str(argument) 31 | 32 | 33 | def get_group_by(): 34 | """ 35 | :return: a string with the value 36 | """ 37 | group_by = None 38 | try: 39 | if config.group_by: 40 | group_by = recursive_group_by(config.group_by) 41 | except Exception as e: 42 | log('Can\'t execute group_by function: {}'.format(e)) 43 | return str(group_by) 44 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/core/logger.py: -------------------------------------------------------------------------------- 1 | def log(string): 2 | """ 3 | Only print output if this is specified in the configuration 4 | :param string: string to be printed 5 | """ 6 | from flask_monitoringdashboard import config 7 | 8 | if config.enable_logging: 9 | print(string) 10 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/core/profiler/__init__.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from flask_monitoringdashboard import ExceptionCollector 4 | from flask_monitoringdashboard.core.get_ip import get_ip 5 | from flask_monitoringdashboard.core.group_by import get_group_by 6 | from flask_monitoringdashboard.core.profiler.base_profiler import BaseProfiler 7 | from flask_monitoringdashboard.core.profiler.outlier_profiler import OutlierProfiler 8 | from flask_monitoringdashboard.core.profiler.performance_profiler import ( 9 | PerformanceProfiler, 10 | ) 11 | from flask_monitoringdashboard.core.profiler.stacktrace_profiler import ( 12 | StacktraceProfiler, 13 | ) 14 | 15 | 16 | def start_thread_last_requested(endpoint): 17 | """ 18 | Starts a thread that updates the last_requested time in the database. 19 | :param endpoint: Endpoint object 20 | """ 21 | BaseProfiler(endpoint).start() 22 | 23 | 24 | def start_performance_thread( 25 | endpoint, duration, status_code, e_collector: ExceptionCollector 26 | ): 27 | """ 28 | Starts a thread that updates performance, utilization and last_requested in the database. 29 | :param endpoint: Endpoint object 30 | :param duration: duration of the request 31 | :param status_code: HTTP status code of the request 32 | """ 33 | group_by = get_group_by() 34 | PerformanceProfiler( 35 | endpoint, get_ip(), duration, group_by, e_collector, status_code 36 | ).start() 37 | 38 | 39 | def start_profiler_thread(endpoint): 40 | """Starts a thread that profiles the main thread.""" 41 | current_thread = threading.current_thread().ident 42 | group_by = get_group_by() 43 | thread = StacktraceProfiler(current_thread, endpoint, get_ip(), group_by) 44 | thread.start() 45 | return thread 46 | 47 | 48 | def start_outlier_thread(endpoint): 49 | """Starts a thread that collects outliers.""" 50 | current_thread = threading.current_thread().ident 51 | group_by = get_group_by() 52 | thread = OutlierProfiler(current_thread, endpoint, get_ip(), group_by) 53 | thread.start() 54 | return thread 55 | 56 | 57 | def start_profiler_and_outlier_thread(endpoint): 58 | """Starts two threads: PerformanceProfiler and StacktraceProfiler.""" 59 | current_thread = threading.current_thread().ident 60 | ip = get_ip() 61 | group_by = get_group_by() 62 | outlier = OutlierProfiler(current_thread, endpoint, ip, group_by) 63 | thread = StacktraceProfiler(current_thread, endpoint, ip, group_by, outlier) 64 | thread.start() 65 | outlier.start() 66 | return thread 67 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/core/profiler/base_profiler.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from flask_monitoringdashboard.core.cache import update_last_requested_cache 4 | 5 | 6 | class BaseProfiler(threading.Thread): 7 | """ 8 | Only updates the last_accessed time in the database for a certain endpoint. 9 | Used for monitoring-level == 0 10 | """ 11 | 12 | def __init__(self, endpoint): 13 | self._endpoint = endpoint 14 | threading.Thread.__init__(self) 15 | 16 | def run(self): 17 | update_last_requested_cache(endpoint_name=self._endpoint.name) 18 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/core/profiler/performance_profiler.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from flask_monitoringdashboard import ExceptionCollector 4 | from flask_monitoringdashboard.core.cache import update_duration_cache 5 | from flask_monitoringdashboard.core.profiler.base_profiler import BaseProfiler 6 | from flask_monitoringdashboard.database import session_scope 7 | from flask_monitoringdashboard.database.request import add_request 8 | 9 | 10 | class PerformanceProfiler(BaseProfiler): 11 | """ 12 | Used for updating the performance and utilization of the endpoint in the database. 13 | Used when monitoring-level == 1 14 | """ 15 | 16 | def __init__( 17 | self, 18 | endpoint, 19 | ip, 20 | duration, 21 | group_by, 22 | e_collector: ExceptionCollector, 23 | status_code=200, 24 | ): 25 | super(PerformanceProfiler, self).__init__(endpoint) 26 | self._ip = ip 27 | self._duration = duration * 1000 # Conversion from sec to ms 28 | self._endpoint = endpoint 29 | self._group_by = group_by 30 | self._status_code = status_code 31 | self.e_collector: ExceptionCollector = e_collector 32 | 33 | def run(self): 34 | update_duration_cache( 35 | endpoint_name=self._endpoint.name, duration=self._duration 36 | ) 37 | with session_scope() as session: 38 | request_id = add_request( 39 | session, 40 | duration=self._duration, 41 | endpoint_id=self._endpoint.id, 42 | ip=self._ip, 43 | group_by=self._group_by, 44 | status_code=self._status_code, 45 | ) 46 | self.e_collector.save_to_db(request_id, session) 47 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/core/profiler/util/__init__.py: -------------------------------------------------------------------------------- 1 | from flask_monitoringdashboard.core.profiler.util.path_hash import PathHash 2 | 3 | 4 | def order_histogram(items, path=''): 5 | """ 6 | Finds the order of self._text_dict and assigns this order to self._lines_body 7 | :param items: list of key, value. Obtained by histogram.items() 8 | :param path: used to filter the results 9 | :return The items, but sorted 10 | """ 11 | sorted_list = [] 12 | indent = PathHash.get_indent(path) + 1 13 | 14 | order = sorted( 15 | [ 16 | (key, value) 17 | for key, value in items 18 | if key[0][: len(path)] == path and PathHash.get_indent(key[0]) == indent 19 | ], 20 | key=lambda row: row[0][1], 21 | ) 22 | for key, value in order: 23 | sorted_list.append((key, value)) 24 | sorted_list.extend(order_histogram(items=items, path=key[0])) 25 | return sorted_list 26 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/core/profiler/util/grouped_stack_line.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | from numpy import std 3 | 4 | 5 | class GroupedStackLine(object): 6 | def __init__(self, indent, code, values, total_sum, total_hits): 7 | self.indent = indent 8 | self.code = code 9 | self.values = values 10 | self.total_sum = total_sum 11 | self.total_hits = total_hits 12 | self.index = 0 13 | 14 | @property 15 | def hits(self): 16 | return len(self.values) 17 | 18 | @property 19 | def sum(self): 20 | return sum(self.values) 21 | 22 | @property 23 | def standard_deviation(self): 24 | return std(self.values) 25 | 26 | @property 27 | def hits_percentage(self): 28 | return self.hits / self.total_hits 29 | 30 | @property 31 | def percentage(self): 32 | return self.sum / self.total_sum 33 | 34 | @property 35 | def average(self): 36 | return self.sum / self.hits 37 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/core/profiler/util/string_hash.py: -------------------------------------------------------------------------------- 1 | """ 2 | Class used for hashing the paths. 3 | """ 4 | 5 | 6 | class StringHash(object): 7 | def __init__(self): 8 | self._h = {} 9 | 10 | def hash(self, string): 11 | """ 12 | Performs the following reduction: 13 | 14 | hash('abc') ==> 0 15 | hash('def') ==> 1 16 | hash('abc') ==> 0 17 | 18 | :param string: the string to be hashed 19 | :return: a unique int for every string. 20 | """ 21 | if string in self._h: 22 | return self._h[string] 23 | self._h[string] = len(self._h) 24 | return self._h[string] 25 | 26 | def unhash(self, hash): 27 | """ Opposite of hash. 28 | 29 | unhash(hash('abc')) == 'abc 30 | 31 | :param hash: string to be unhashed 32 | :return: the value that corresponds to the given hash 33 | """ 34 | 35 | for k, v in self._h.items(): 36 | if v == hash: 37 | return k 38 | 39 | raise ValueError('Value not possible to unhash: {}'.format(hash)) 40 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/core/reporting/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flask-dashboard/Flask-MonitoringDashboard/3cebbd62a8fd248313eeb630375d5dd07a6ff572/flask_monitoringdashboard/core/reporting/__init__.py -------------------------------------------------------------------------------- /flask_monitoringdashboard/core/reporting/questions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flask-dashboard/Flask-MonitoringDashboard/3cebbd62a8fd248313eeb630375d5dd07a6ff572/flask_monitoringdashboard/core/reporting/questions/__init__.py -------------------------------------------------------------------------------- /flask_monitoringdashboard/core/reporting/questions/report_question.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | 3 | 4 | class ReportAnswer: 5 | __metaclass__ = ABCMeta 6 | 7 | def __init__(self, type): 8 | self.type = type 9 | 10 | @abstractmethod 11 | def is_significant(self): 12 | pass 13 | 14 | @abstractmethod 15 | def meta(self): 16 | """ 17 | Should return a `dict` that contains any additional information required on the 18 | frontend. This will be included in the response. 19 | """ 20 | pass 21 | 22 | def serialize(self): 23 | base = dict(is_significant=self.is_significant(), type=self.type) 24 | base.update(self.meta()) 25 | 26 | return base 27 | 28 | 29 | class ReportQuestion: 30 | __metaclass__ = ABCMeta 31 | 32 | @abstractmethod 33 | def get_answer(self, endpoint, requests_criterion, baseline_requests_criterion): 34 | pass 35 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/core/rules.py: -------------------------------------------------------------------------------- 1 | def get_rules(endpoint_name=None): 2 | """ 3 | :param endpoint_name: if specified, only return the available rules to that endpoint 4 | :return: A list of the current rules in the attached Flask app 5 | """ 6 | from flask_monitoringdashboard import config 7 | 8 | try: 9 | rules = config.app.url_map.iter_rules(endpoint=endpoint_name) 10 | except KeyError: 11 | return [] 12 | return [ 13 | r 14 | for r in rules 15 | if not r.rule.startswith('/' + config.link) 16 | and not r.rule.startswith('/static-' + config.link) 17 | and not r.endpoint == 'static' 18 | ] 19 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/core/timezone.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | 4 | def to_local_datetime(dt): 5 | """ 6 | Convert datetime (UTC) to local datetime based on the configuration. 7 | :param dt: UTC datetime object 8 | :return local datetime 9 | """ 10 | from flask_monitoringdashboard import config 11 | 12 | if dt: 13 | return dt + config.timezone.utcoffset(datetime.datetime.utcnow()) 14 | return None 15 | 16 | 17 | def to_utc_datetime(dt): 18 | """ 19 | Convert datetime (local) to UTC datetime based on the configuration. 20 | :param dt: local datetime object 21 | :return UTC datetime 22 | """ 23 | from flask_monitoringdashboard import config 24 | 25 | if dt: 26 | return dt - config.timezone.utcoffset(datetime.datetime.utcnow()) 27 | return None 28 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/database/auth.py: -------------------------------------------------------------------------------- 1 | from flask_monitoringdashboard.database import User, session_scope 2 | from flask_monitoringdashboard import config 3 | 4 | 5 | def get_user(username, password): 6 | """Validates the username and password and returns an User-object if both are valid. 7 | In case the User-table is empty, a user with default credentials is added. 8 | """ 9 | with session_scope() as session: 10 | if session.query(User).count() == 0: 11 | user = User(username=config.username, is_admin=True) 12 | user.set_password(password=config.password) 13 | session.add(user) 14 | 15 | user = session.query(User).filter(User.username == username).one_or_none() 16 | if user is not None: 17 | if user.check_password(password=password): 18 | session.expunge_all() 19 | return user 20 | 21 | return None 22 | 23 | 24 | def get_all_users(session): 25 | users = session.query(User).order_by(User.id).all() 26 | 27 | return [ 28 | { 29 | "id": user.id, 30 | "username": user.username, 31 | "is_admin": user.is_admin, 32 | } 33 | for user in users 34 | ] 35 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/database/code_line.py: -------------------------------------------------------------------------------- 1 | from flask_monitoringdashboard.database import CodeLine 2 | 3 | 4 | def get_code_line(session, fn, ln, name, code): 5 | """ 6 | Get a CodeLine object from a given quadruple of fn, ln, name, code. If the CodeLine object 7 | doesn't already exist, a new one is created in the database. 8 | :param session: session for the database 9 | :param fn: filename (string) 10 | :param ln: line_number of the code (int) 11 | :param name: function name (string) 12 | :param code: line of code (string) 13 | :return: a CodeLine object 14 | """ 15 | result = ( 16 | session.query(CodeLine) 17 | .filter( 18 | CodeLine.filename == fn, 19 | CodeLine.line_number == ln, 20 | CodeLine.function_name == name, 21 | CodeLine.code == code, 22 | ) 23 | .first() 24 | ) 25 | if not result: 26 | result = CodeLine(filename=fn, line_number=ln, function_name=name, code=code) 27 | session.add(result) 28 | session.flush() 29 | 30 | return result 31 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/database/count.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import func, distinct 2 | 3 | from flask_monitoringdashboard.database import Request, StackLine 4 | 5 | 6 | def count_rows(session, column, *criterion): 7 | """ 8 | Count the number of rows of a specified column. 9 | :param session: session for the database 10 | :param column: column to count 11 | :param criterion: where-clause of the query 12 | :return: number of rows 13 | """ 14 | return session.query(func.count(distinct(column))).filter(*criterion).scalar() 15 | 16 | 17 | def count_requests(session, endpoint_id, *where): 18 | """ 19 | Return the number of hits for a specific endpoint (possible with more filter arguments). 20 | :param session: session for the database 21 | :param endpoint_id: id of the endpoint 22 | :param where: additional arguments 23 | """ 24 | return count_rows(session, Request.id, Request.endpoint_id == endpoint_id, *where) 25 | 26 | 27 | def count_total_requests(session, *where): 28 | """ 29 | Return the number of total hits 30 | :param session: session for the database 31 | :param where: additional arguments 32 | """ 33 | return count_rows(session, Request.id, *where) 34 | 35 | 36 | def count_outliers(session, endpoint_id): 37 | """ 38 | :param session: session for the database 39 | :param endpoint_id: id of the endpoint 40 | :return: An integer with the number of rows in the Outlier-table. 41 | """ 42 | return count_rows( 43 | session, Request.id, Request.endpoint_id == endpoint_id, Request.outlier 44 | ) 45 | 46 | 47 | def count_profiled_requests(session, endpoint_id): 48 | """ 49 | Count the number of profiled requests for a certain endpoint 50 | :param session: session for the database 51 | :param endpoint_id: id of the endpoint 52 | :return: An integer 53 | """ 54 | return ( 55 | session.query(func.count(distinct(StackLine.request_id))) 56 | .filter(Request.endpoint_id == endpoint_id) 57 | .join(Request.stack_lines) 58 | .scalar() 59 | ) 60 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/database/count_group.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from sqlalchemy import func 4 | 5 | from flask_monitoringdashboard.core.timezone import to_utc_datetime 6 | from flask_monitoringdashboard.database import Request 7 | 8 | 9 | def count_rows_group(session, column, *criterion): 10 | """ 11 | Count the number of rows of a specified column 12 | :param session: session for the database 13 | :param column: column to count 14 | :param criterion: where-clause of the query 15 | :return list with the number of rows per endpoint 16 | """ 17 | return ( 18 | session.query(Request.endpoint_id, func.count(column)) 19 | .filter(*criterion) 20 | .group_by(Request.endpoint_id) 21 | .all() 22 | ) 23 | 24 | 25 | def get_value(tuples, name, default=0): 26 | """ 27 | :param tuples: must be structured as: [(a, b), (c, d), ..] 28 | :param name: name to filter on, e.g.: if name == a, it returns b 29 | :param default: returned if the name was not found in the list 30 | :return value corresponding to the name in the list. 31 | """ 32 | for key, value in tuples: 33 | if key == name: 34 | return value 35 | return default 36 | 37 | 38 | def count_requests_group(session, *where): 39 | """ 40 | Return the number of hits for all endpoints (possible with more filter arguments). 41 | :param session: session for the database 42 | :param where: additional arguments 43 | """ 44 | return count_rows_group(session, Request.id, *where) 45 | 46 | 47 | def count_requests_per_day(session, list_of_days): 48 | """ Return the number of hits for all endpoints per day. 49 | :param session: session for the database 50 | :param list_of_days: list with datetime.datetime objects. """ 51 | result = [] 52 | for day in list_of_days: 53 | dt_begin = to_utc_datetime(datetime.datetime.combine(day, datetime.time(0, 0, 0))) 54 | dt_end = dt_begin + datetime.timedelta(days=1) 55 | 56 | result.append( 57 | count_rows_group( 58 | session, 59 | Request.id, 60 | Request.time_requested >= dt_begin, 61 | Request.time_requested < dt_end, 62 | ) 63 | ) 64 | return result 65 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/database/custom_graph.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from sqlalchemy.orm.exc import NoResultFound 4 | 5 | from flask_monitoringdashboard.database import CustomGraph, CustomGraphData, row2dict 6 | 7 | 8 | def get_graph_id_from_name(session, name): 9 | """ 10 | :param session: session for the database 11 | :param name: name of the graph (must be unique) 12 | :return: the graph_id corresponding to the name. If the name does not exists in the db, 13 | a new graph is added to the database. 14 | """ 15 | try: 16 | result = session.query(CustomGraph).filter(CustomGraph.title == name).one() 17 | except NoResultFound: 18 | result = CustomGraph(title=name) 19 | session.add(result) 20 | session.flush() 21 | session.expunge(result) 22 | return result.graph_id 23 | 24 | 25 | def add_value(session, graph_id, value): 26 | data = CustomGraphData(graph_id=graph_id, value=value) 27 | session.add(data) 28 | 29 | 30 | def get_graphs(session): 31 | return session.query(CustomGraph).all() 32 | 33 | 34 | def get_graph_data(session, graph_id, start_date, end_date): 35 | """ 36 | :param session: session for the database 37 | :param graph_id: id to filter on 38 | :param start_date: Datetime object that denotes the beginning of the interval 39 | :param end_date: Datetime object that denotes the end of the interval 40 | :return: A list with values retrieved from the database 41 | """ 42 | return [ 43 | row2dict(row) 44 | for row in session.query(CustomGraphData) 45 | .filter( 46 | CustomGraphData.graph_id == graph_id, 47 | CustomGraphData.time >= start_date, 48 | CustomGraphData.time < end_date + timedelta(days=1), 49 | ) 50 | .all() 51 | ] 52 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/database/exception_frame.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | from sqlalchemy.orm import Session 3 | from flask_monitoringdashboard.database import ExceptionFrame, FunctionLocation 4 | 5 | 6 | def add_exception_frame( 7 | session: Session, 8 | function_location_id: int, 9 | line_number: int, 10 | ): 11 | """ 12 | Adds a ExceptionFrame to the database if it does not already exist. 13 | :param session: Session for the database 14 | :param function_location_id: The ID of the FunctionLocation of the frame. 15 | :param line_number: The line number of the frame. 16 | :return: The ID of the existing or newly added ExceptionFrame. 17 | """ 18 | 19 | existing_exception_frame = ( 20 | session.query(ExceptionFrame) 21 | .filter(ExceptionFrame.function_location_id == function_location_id) 22 | .filter(ExceptionFrame.line_number == line_number) 23 | .first() 24 | ) 25 | if existing_exception_frame is not None: 26 | return existing_exception_frame.id 27 | else: 28 | frame = ExceptionFrame( 29 | function_location_id=function_location_id, line_number=line_number 30 | ) 31 | session.add(frame) 32 | session.flush() 33 | return frame.id 34 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/database/exception_message.py: -------------------------------------------------------------------------------- 1 | from flask_monitoringdashboard.database import ExceptionMessage 2 | 3 | 4 | def add_exception_message(session, message) -> int: 5 | """ 6 | Adds an ExceptionMessage to the database if it does not already exist. Returns the id. 7 | """ 8 | exception_message = ( 9 | session.query(ExceptionMessage).filter_by(message=message).first() 10 | ) 11 | 12 | if exception_message is None: 13 | exception_message = ExceptionMessage(message=message) 14 | session.add(exception_message) 15 | session.flush() 16 | 17 | return exception_message.id 18 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/database/exception_stack_line.py: -------------------------------------------------------------------------------- 1 | from flask_monitoringdashboard.database import ExceptionStackLine 2 | 3 | def add_exception_stack_line( 4 | session, 5 | stack_trace_snapshot_id, 6 | exception_frame_id, 7 | position, 8 | ): 9 | """ 10 | Adds a ExceptionStackLine to the database 11 | :param session: Session for the database 12 | :param stack_trace_snapshot_id: id of the stack trace snapshot 13 | :param exception_frame_id: id of the ExceptionFrame 14 | :param position: position of the ExceptionStackLine in the stack trace 15 | """ 16 | session.add( 17 | ExceptionStackLine( 18 | stack_trace_snapshot_id=stack_trace_snapshot_id, 19 | exception_frame_id=exception_frame_id, 20 | position=position, 21 | ) 22 | ) 23 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/database/exception_type.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | from flask_monitoringdashboard.database import ExceptionType 3 | 4 | 5 | def add_exception_type(session: Session, type: str) -> int: 6 | """ 7 | Adds an ExceptionType to the database if it does not already exist. Returns the id. 8 | """ 9 | type = type[:256] # To avoid error if larger than allowed in db 10 | exception_type = session.query(ExceptionType).filter_by(type=type).first() 11 | 12 | if exception_type is None: 13 | exception_type = ExceptionType(type=type) 14 | session.add(exception_type) 15 | session.flush() 16 | 17 | return exception_type.id 18 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/database/file_path.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | from flask_monitoringdashboard.database import FilePath 3 | 4 | 5 | def add_file_path(session: Session, path: str) -> int: 6 | """ 7 | Adds an FilePath to the database if it does not already exist. Returns the id. 8 | """ 9 | path = path[:256] # To avoid error if larger than allowed in db 10 | file_path = session.query(FilePath).filter_by(path=path).first() 11 | 12 | if file_path is None: 13 | file_path = FilePath(path=path) 14 | session.add(file_path) 15 | session.flush() 16 | 17 | return file_path.id 18 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/database/function_definition.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | from sqlalchemy.orm import Session 3 | from flask_monitoringdashboard.database import ( 4 | CodeLine, 5 | ExceptionStackLine, 6 | FunctionDefinition, 7 | ) 8 | 9 | 10 | def add_function_definition(session: Session, f_def: FunctionDefinition) -> int: 11 | """ 12 | Adds a FunctionDefinition to the database if it does not already exist. 13 | :param session: Session for the database 14 | :param f_def: The FunctionDefinition object to be added 15 | :return: The ID of the existing or newly added FunctionDefinition. 16 | """ 17 | result: Union[FunctionDefinition, None] = ( 18 | session.query(FunctionDefinition) 19 | .filter(FunctionDefinition.code_hash == f_def.code_hash) 20 | .first() 21 | ) 22 | if result is not None: 23 | return result.id 24 | else: 25 | session.add(f_def) 26 | session.flush() 27 | return f_def.id 28 | 29 | 30 | def get_function_definition_from_id( 31 | session: Session, function_id: int 32 | ) -> Union[FunctionDefinition, None]: 33 | return ( 34 | session.query(FunctionDefinition) 35 | .filter(FunctionDefinition.id == function_id) 36 | .first() 37 | ) 38 | 39 | def get_function_definition_code_from_id(session: Session, function_id: int) -> Union[str, None]: 40 | """ 41 | Retrieves the code of a function definition from the database using its ID. 42 | :param session: Session for the database 43 | :param function_id: ID of the FunctionDefinition 44 | :return: The code of the function definition if found, otherwise None. 45 | """ 46 | result: Union[FunctionDefinition, None] = get_function_definition_from_id(session, function_id) 47 | if result is not None: 48 | return result.code 49 | else: 50 | return None 51 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/database/function_location.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Session 2 | from flask_monitoringdashboard.database import FunctionLocation 3 | 4 | def add_function_location(session: Session, file_path_id: int, function_definition_id: int, function_start_line_number: int) -> int: 5 | """ 6 | Adds a FunctionLocation to the database if it does not already exist. 7 | :param session: Session for the database 8 | :param file_path_id: The ID of the FilePath of the function. 9 | :param function_definition_id: The ID of the FunctionDefinition of the function. 10 | :param function_start_line_number: The starting line number of the function in the source file. 11 | :return: The ID of the existing or newly added FunctionDefinition. 12 | """ 13 | 14 | existing_function_location = ( 15 | session.query(FunctionLocation) 16 | .filter(FunctionLocation.file_path_id == file_path_id) 17 | .filter(FunctionLocation.function_definition_id == function_definition_id) 18 | .filter(FunctionLocation.function_start_line_number == function_start_line_number) 19 | .first() 20 | ) 21 | if existing_function_location is not None: 22 | return existing_function_location.id 23 | else: 24 | f_location = FunctionLocation(file_path_id=file_path_id, function_definition_id=function_definition_id, function_start_line_number=function_start_line_number) 25 | session.add(f_location) 26 | session.flush() 27 | return f_location.id 28 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/database/outlier.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import desc 2 | from sqlalchemy.orm import joinedload 3 | 4 | from flask_monitoringdashboard.database import Outlier, Request 5 | 6 | 7 | def add_outlier(session, request_id, cpu_percent, memory, stacktrace, request): 8 | """ 9 | Adds an Outlier object in the database. 10 | :param session: session for the database 11 | :param request_id: id of the request 12 | :param cpu_percent: cpu load of the server when processing the request 13 | :param memory: memory load of the server when processing the request 14 | :param stacktrace: stack trace of the request 15 | :param request: triple containing the headers, environment and url 16 | """ 17 | headers, environ, url = request 18 | outlier = Outlier( 19 | request_id=request_id, 20 | request_header=headers, 21 | request_environment=environ, 22 | request_url=url, 23 | cpu_percent=cpu_percent, 24 | memory=memory, 25 | stacktrace=stacktrace, 26 | ) 27 | session.add(outlier) 28 | 29 | 30 | def get_outliers_sorted(session, endpoint_id, offset, per_page): 31 | """ 32 | Gets a list of Outlier objects for a certain endpoint, sorted by most recent request time 33 | :param session: session for the database 34 | :param endpoint_id: id of the endpoint for filtering the requests 35 | :param offset: number of items to skip 36 | :param per_page: number of items to return 37 | :return list of Outlier objects of a specific endpoint 38 | """ 39 | result = ( 40 | session.query(Outlier) 41 | .join(Outlier.request) 42 | .options(joinedload(Outlier.request).joinedload(Request.endpoint)) 43 | .filter(Request.endpoint_id == endpoint_id) 44 | .order_by(desc(Request.time_requested)) 45 | .offset(offset) 46 | .limit(per_page) 47 | .all() 48 | ) 49 | session.expunge_all() 50 | return result 51 | 52 | 53 | def get_outliers_cpus(session, endpoint_id): 54 | """ 55 | Gets list of CPU loads of all outliers of a certain endpoint 56 | :param session: session for the database 57 | :param endpoint_id: id of the endpoint 58 | :return list of cpu percentages as strings 59 | """ 60 | outliers = ( 61 | session.query(Outlier.cpu_percent) 62 | .join(Outlier.request) 63 | .filter(Request.endpoint_id == endpoint_id) 64 | .all() 65 | ) 66 | return [outlier[0] for outlier in outliers] 67 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/database/request.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contains all functions that access a Request object. 3 | """ 4 | import time 5 | 6 | from sqlalchemy import and_, func 7 | 8 | from flask_monitoringdashboard.database import Request 9 | 10 | 11 | def get_latencies_sample(session, endpoint_id, criterion, sample_size=500): 12 | query = ( 13 | session.query(Request.duration).filter(Request.endpoint_id == endpoint_id, 14 | *criterion) 15 | ) 16 | # return random rows: See https://stackoverflow.com/a/60815 17 | dialect = session.bind.dialect.name 18 | 19 | if dialect == 'sqlite': 20 | query = query.order_by(func.random()) 21 | elif dialect == 'mysql': 22 | query = query.order_by(func.rand()) 23 | 24 | query = query.limit(sample_size) 25 | 26 | return [item.duration for item in query.all()] 27 | 28 | 29 | def add_request(session, duration, endpoint_id, ip, group_by, status_code): 30 | """ Adds a request to the database. Returns the id. 31 | :param status_code: status code of the request 32 | :param session: session for the database 33 | :param duration: duration of the request 34 | :param endpoint_id: id of the endpoint 35 | :param ip: IP address of the requester 36 | :param group_by: a criteria by which the requests can be grouped 37 | :return the id of the request after it was stored in the database 38 | """ 39 | request = Request( 40 | endpoint_id=endpoint_id, 41 | duration=duration, 42 | ip=ip, 43 | group_by=group_by, 44 | status_code=status_code, 45 | ) 46 | session.add(request) 47 | session.commit() 48 | return request.id 49 | 50 | 51 | def get_date_of_first_request(session): 52 | """ Returns the date (as unix timestamp) of the first request since FMD was deployed. 53 | :param session: session for the database 54 | :return time of the first request 55 | """ 56 | result = session.query(Request.time_requested).order_by( 57 | Request.time_requested).first() 58 | if result: 59 | return int(time.mktime(result[0].timetuple())) 60 | return -1 61 | 62 | 63 | def create_time_based_sample_criterion(start_date, end_date): 64 | return and_(Request.time_requested > start_date, Request.time_requested <= end_date) 65 | 66 | 67 | def get_date_of_first_request_version(session, version): 68 | """ Returns the date (as unix timestamp) of the first request in the current FMD version. 69 | :param session: session for the database 70 | :param version: version of the dashboard 71 | :return time of the first request in that version 72 | """ 73 | result = ( 74 | session.query(Request.time_requested) 75 | .filter(Request.version_requested == version) 76 | .order_by(Request.time_requested) 77 | .first() 78 | ) 79 | if result: 80 | return int(time.mktime(result[0].timetuple())) 81 | return -1 82 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/database/stack_trace_snapshot.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | from sqlalchemy.orm import Session 3 | from flask_monitoringdashboard.database import ExceptionStackLine, FilePath, StackTraceSnapshot, FunctionDefinition, FunctionLocation, ExceptionFrame 4 | from sqlalchemy import desc 5 | 6 | 7 | def get_stack_trace_by_hash( 8 | session: Session, stack_trace_snapshot_hash: str 9 | ) -> Union[StackTraceSnapshot, None]: 10 | """ 11 | Get StackTraceSnapshot record by its hash. 12 | """ 13 | result = ( 14 | session.query(StackTraceSnapshot) 15 | .filter_by(hash=stack_trace_snapshot_hash) 16 | .first() 17 | ) 18 | return result 19 | 20 | 21 | def add_stack_trace_snapshot(session: Session, stack_trace_snapshot_hash: str) -> int: 22 | """ 23 | Add a new StackTraceSnapshot record. Returns the id. 24 | """ 25 | existing_trace = get_stack_trace_by_hash(session, stack_trace_snapshot_hash) 26 | if existing_trace is not None: 27 | return int(existing_trace.id) 28 | 29 | result = StackTraceSnapshot(hash=stack_trace_snapshot_hash) 30 | session.add(result) 31 | session.flush() 32 | 33 | return int(result.id) 34 | 35 | 36 | def get_stacklines_from_stack_trace_snapshot_id( 37 | session: Session, stack_trace_snapshot_id: int 38 | ): 39 | """ 40 | Gets all the stack lines referred to by a stack_trace. 41 | :param session: session for the database 42 | :param stack_trace_snapshot_id: Filter ExceptionStackLines on this stack trace id 43 | return: A list of dicts. Each dict contains: 44 | - position (int) in the stack trace 45 | - path (str) to the file 46 | - line_number (int) in the file 47 | - name (str) of the function 48 | - function_start_line_number (int) 49 | - function_definition_id (int) of the function 50 | """ 51 | result = ( 52 | session.query( 53 | ExceptionStackLine.position, 54 | FilePath.path, 55 | ExceptionFrame.line_number, 56 | FunctionDefinition.name, 57 | FunctionLocation.function_start_line_number, 58 | FunctionLocation.function_definition_id, 59 | ) 60 | .join(ExceptionFrame, ExceptionStackLine.exception_frame) 61 | .join(FunctionLocation, ExceptionFrame.function_location) 62 | .join(FunctionDefinition, FunctionLocation.function_definition) 63 | .join(FilePath, FunctionLocation.file_path) 64 | .filter(ExceptionStackLine.stack_trace_snapshot_id == stack_trace_snapshot_id) 65 | .order_by(desc(ExceptionStackLine.position)) 66 | .all() 67 | ) 68 | return result 69 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/database/versions.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import func, desc 2 | 3 | from flask_monitoringdashboard.database import Request 4 | 5 | 6 | def get_versions(session, endpoint_id=None, limit=None): 7 | """ 8 | Returns a list of length 'limit' with the versions that are used in the application 9 | :param session: session for the database 10 | :param endpoint_id: only get the version that are used in this endpoint 11 | :param limit: only return the most recent versions 12 | :return: a list of tuples with the versions (as a string) and dates, from oldest to newest 13 | """ 14 | query = session.query(Request.version_requested, func.min(Request.time_requested)) 15 | if endpoint_id: 16 | query = query.filter(Request.endpoint_id == endpoint_id) 17 | query = query.group_by(Request.version_requested) 18 | query = query.order_by(func.min(Request.time_requested).desc()) 19 | if limit: 20 | query = query.limit(limit) 21 | return query.all() 22 | 23 | 24 | def get_first_requests(session, endpoint_id, limit=None): 25 | """ 26 | Returns a list with all versions and when they're first used 27 | :param session: session for the database 28 | :param limit: only return the most recent versions 29 | :param endpoint_id: id of the endpoint 30 | :return list of tuples with versions 31 | """ 32 | query = ( 33 | session.query( 34 | Request.version_requested, func.min(Request.time_requested).label('first_used') 35 | ) 36 | .filter(Request.endpoint_id == endpoint_id) 37 | .group_by(Request.version_requested) 38 | .order_by(desc('first_used')) 39 | ) 40 | if limit: 41 | query = query.limit(limit) 42 | return query.all() 43 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/frontend/js/controllers/apiPerformance.js: -------------------------------------------------------------------------------- 1 | export function ApiPerformanceController($scope, $http, menuService, formService, infoService, 2 | plotlyService, endpointService) { 3 | endpointService.reset(); 4 | menuService.reset('api_performance'); 5 | $scope.title = 'API Performance'; 6 | 7 | // Set the information box 8 | infoService.axesText = 'The X-axis presents the execution time in ms. The Y-axis presents every endpoint of ' + 9 | 'the Flask application.'; 10 | infoService.contentText = 'In this graph, it is easy to compare the execution times of different endpoints. ' + 11 | 'This information can be used to discover which endpoints need to be improved in terms ' + 12 | 'of response times.'; 13 | 14 | // Set the form handler 15 | formService.clear(); 16 | formService.addEndpoints(); 17 | 18 | formService.setReload(function () { 19 | $http.post('api/api_performance', { 20 | data: { 21 | endpoints: formService.getMultiSelection('endpoints') 22 | } 23 | }).then(function (response) { 24 | let data = response.data.map(obj => { 25 | return { 26 | x: obj.values, 27 | type: 'box', 28 | name: obj.name, 29 | }; 30 | }); 31 | 32 | plotlyService.chart(data, { 33 | xaxis: { 34 | title: 'Execution time (ms)', 35 | }, 36 | yaxis: { 37 | type: 'category' 38 | } 39 | }); 40 | }); 41 | }); 42 | } -------------------------------------------------------------------------------- /flask_monitoringdashboard/frontend/js/controllers/customGraph.js: -------------------------------------------------------------------------------- 1 | export function CustomGraphController($scope, $http, infoService, endpointService, 2 | menuService, formService, plotlyService) { 3 | endpointService.reset(); 4 | menuService.reset('custom_graph' + endpointService.graphId); 5 | $scope.title = endpointService.getGraphTitle(); 6 | endpointService.onNameChanged = function (name) { 7 | $scope.title = name; 8 | }; 9 | 10 | // Set the information box 11 | infoService.axesText = ''; 12 | infoService.contentText = ''; 13 | 14 | formService.clear(); 15 | let startDate = formService.addDate('Start date'); 16 | startDate.value.setDate(startDate.value.getDate() - 14); 17 | formService.addDate('End date'); 18 | 19 | formService.setReload(function () { 20 | let start = formService.getDate('Start date'); 21 | let end = formService.getDate('End date'); 22 | 23 | $http.get('api/custom_graph/' + endpointService.graphId + '/' + start + '/' + end) 24 | .then(function (response) { 25 | let values = response.data.map(o => o.value); 26 | let times = response.data.map(o => o.time); 27 | plotlyService.chart([{ 28 | x: times, 29 | y: values, 30 | type: 'bar', 31 | name: $scope.title 32 | }], { 33 | yaxis: { 34 | title: 'Values', 35 | }, 36 | margin: { 37 | l: 100 38 | } 39 | }); 40 | }); 41 | }); 42 | formService.reload(); 43 | } -------------------------------------------------------------------------------- /flask_monitoringdashboard/frontend/js/controllers/dailyUtilization.js: -------------------------------------------------------------------------------- 1 | export function DailyUtilizationController($scope, $http, menuService, formService, infoService, 2 | plotlyService, endpointService) { 3 | endpointService.reset(); 4 | menuService.reset('daily_load'); 5 | $scope.title = 'Daily API Utilization'; 6 | 7 | // Set the information box 8 | infoService.axesText = 'The X-axis presents the amount of requests. The Y-axis presents a number of days.'; 9 | infoService.contentText = 'This graph presents a horizontal stacked barplot. Each endpoint is represented ' + 10 | 'by a color. In the legend on the right, you can disable a certain endpoint by clicking on it. You can ' + 11 | 'also show in the information of a single endpoint by double clicking that endpoint in the legend. The ' + 12 | 'information from this graph can be used to see on which days (a subset of) the endpoints are used the most.'; 13 | 14 | // Set the form handler 15 | formService.clear(); 16 | let start = formService.addDate('Start date'); 17 | start.value.setDate(start.value.getDate() - 10); 18 | formService.addDate('End date'); 19 | 20 | formService.setReload(function () { 21 | let start = formService.getDate('Start date'); 22 | let end = formService.getDate('End date'); 23 | 24 | $http.get('api/requests/' + start + '/' + end).then(function (response) { 25 | let data = response.data.data.map(obj => { 26 | return { 27 | x: obj.values, 28 | y: response.data.days, 29 | name: obj.name, 30 | type: 'bar', 31 | orientation: 'h' 32 | }; 33 | }); 34 | 35 | plotlyService.chart(data, { 36 | barmode: 'stack', 37 | xaxis: { 38 | title: 'Number of requests', 39 | }, 40 | yaxis: { 41 | type: 'category', 42 | autorange: 'reversed' 43 | } 44 | }); 45 | }); 46 | }); 47 | formService.reload(); 48 | } -------------------------------------------------------------------------------- /flask_monitoringdashboard/frontend/js/controllers/databaseManagementController.js: -------------------------------------------------------------------------------- 1 | export function DatabaseManagementController($scope, $http, menuService, endpointService) { 2 | endpointService.reset(); 3 | menuService.reset('database_management'); 4 | 5 | $scope.databaseSize = 'N/A'; 6 | 7 | $scope.getDatabaseSize = function () { 8 | $http.get('/dashboard/database_pruning/get_database_size') 9 | .then(function (response) { 10 | $scope.databaseSize = response.data.size; 11 | }, function (error) { 12 | console.error('Error fetching database size:', error.data); 13 | }); 14 | }; 15 | 16 | $scope.getDatabaseSize(); 17 | 18 | 19 | // Initialize the configuration for pruning 20 | $scope.pruneOnDemandConfig = { 21 | ageThresholdWeeks: 1, // Default value or null if you prefer no default 22 | deleteCustomGraphData: false 23 | }; 24 | 25 | // Variables for feedback messages 26 | $scope.pruneOnDemandMessage = ''; 27 | $scope.pruneOnDemandIsSuccess = false; 28 | 29 | $scope.pageSize = '10'; 30 | 31 | // Function to prune the database 32 | $scope.pruneDatabase = function () { 33 | let weekOrWeeks = $scope.pruneOnDemandConfig.ageThresholdWeeks === 1 ? ' week' : ' weeks'; 34 | let confirmationMessage = 'Are you sure you want to prune all request data older than ' 35 | + $scope.pruneOnDemandConfig.ageThresholdWeeks + weekOrWeeks + '?'; 36 | if (!confirm(confirmationMessage)) { 37 | return; // Stop the function if the user clicks 'Cancel' 38 | } 39 | 40 | // Confirmation dialog 41 | const pruneData = { 42 | age_threshold_weeks: $scope.pruneOnDemandConfig.ageThresholdWeeks, 43 | delete_custom_graph_data: $scope.pruneOnDemandConfig.deleteCustomGraphData 44 | }; 45 | 46 | $http.post('/dashboard/database_pruning/prune_on_demand', pruneData) 47 | .then(function (response) { 48 | $scope.pruneOnDemandIsSuccess = true; 49 | $scope.pruneOnDemandMessage = 'Database pruning complete.'; 50 | }, function (error) { 51 | $scope.pruneOnDemandIsSuccess = false; 52 | $scope.pruneOnDemandMessage = 'Error pruning database: ' + (error.data.error || 'Unknown error'); 53 | }); 54 | }; 55 | 56 | // Function to fetch the pruning schedule 57 | $scope.getPruningSchedule = function () { 58 | $http.get('/dashboard/database_pruning/get_pruning_schedule') 59 | .then(function (response) { 60 | $scope.pruningSchedule = response.data; 61 | }, function (error) { 62 | }); 63 | }; 64 | $scope.getPruningSchedule(); 65 | } 66 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/frontend/js/controllers/endpointHourlyLoad.js: -------------------------------------------------------------------------------- 1 | export function EndpointHourlyLoadController($scope, $http, menuService, endpointService, 2 | infoService, formService, plotlyService, $filter) { 3 | endpointService.reset(); 4 | menuService.reset('endpoint_hourly'); 5 | endpointService.onNameChanged = function (name) { 6 | $scope.title = 'Hourly API Utilization for ' + name; 7 | }; 8 | 9 | // Set the information box 10 | infoService.axesText = 'The X-axis represents the dates. The Y-axis presents the hours of the day.'; 11 | infoService.contentText = 'The cell color represents the number of requests that the application ' + 12 | 'received in a single hour for this endpoint. The darker the cell, the more requests it has processed.' + 13 | ' This information can be used to discover the peak usage hours of this endpoint.'; 14 | 15 | // Set the form handler 16 | formService.clear(); 17 | let start = formService.addDate('Start date'); 18 | formService.addDate('End date'); 19 | start.value.setDate(start.value.getDate() - 14); 20 | 21 | formService.setReload(function () { 22 | let start = formService.getDate('Start date'); 23 | let end = formService.getDate('End date'); 24 | let times = [...Array(24).keys()].map(d => d + ":00 "); 25 | 26 | $http.get('api/hourly_load/' + start + '/' + end + '/' + endpointService.info.id) 27 | .then(function (response) { 28 | let x = response.data.days; 29 | let y = times; 30 | let z = response.data.data; 31 | let hover_text = z.map((row, i) => row.map((item, j) => { 32 | return `Date: ${$filter('dateShort')(x[j])}
Time: ${i + ':00'}
Requests: ${item}` 33 | })); 34 | plotlyService.heatmap(x, y, z, { 35 | 36 | xaxis: { 37 | type: 'category', 38 | title: 'Versions' 39 | }, 40 | yaxis: { 41 | type: 'category', 42 | autorange: 'reversed' 43 | }, 44 | margin: {l: 50} 45 | }, hover_text); 46 | }); 47 | }); 48 | formService.reload(); 49 | } -------------------------------------------------------------------------------- /flask_monitoringdashboard/frontend/js/controllers/endpointOutlier.js: -------------------------------------------------------------------------------- 1 | export function OutlierController($scope, $http, endpointService, menuService, 2 | paginationService, plotlyService) { 3 | $scope.table = []; 4 | 5 | endpointService.reset(); 6 | menuService.reset('endpoint_outlier'); 7 | endpointService.onNameChanged = function (name) { 8 | $scope.title = 'Outliers for ' + name; 9 | }; 10 | 11 | // Pagination 12 | paginationService.init('outliers'); 13 | $http.get('api/num_outliers/' + endpointService.info.id).then(function (response) { 14 | paginationService.setTotal(response.data); 15 | }); 16 | paginationService.onReload = function () { 17 | $http.get('api/outlier_table/' + endpointService.info.id + '/' + 18 | paginationService.getLeft() + '/' + paginationService.perPage).then(function (response) { 19 | $scope.table = response.data; 20 | }); 21 | }; 22 | 23 | $http.get('api/outlier_graph/' + endpointService.info.id).then(function (response) { 24 | plotlyService.chart(response.data.map(row => { 25 | return { 26 | x: row.values, 27 | type: 'box', 28 | name: row.name, 29 | marker: {color: row.color} 30 | }; 31 | }), { 32 | xaxis: { 33 | title: 'CPU loads (%)', 34 | }, 35 | yaxis: { 36 | type: 'category', 37 | autorange: 'reversed' 38 | } 39 | }); 40 | }); 41 | } -------------------------------------------------------------------------------- /flask_monitoringdashboard/frontend/js/controllers/endpointUsers.js: -------------------------------------------------------------------------------- 1 | export function EndpointUsersController($scope, $http, infoService, endpointService, 2 | menuService, formService, plotlyService) { 3 | endpointService.reset(); 4 | menuService.reset('endpoint_user'); 5 | endpointService.onNameChanged = function (name) { 6 | $scope.title = 'Per-User Performance for ' + name; 7 | }; 8 | 9 | // Set the information box 10 | infoService.axesText = 'The X-axis presents the execution time in ms. The Y-axis presents the versions that are used.'; 11 | infoService.contentText = 'This graph shows a horizontal boxplot for the versions that are used. With this ' + 12 | 'graph you can found out whether the performance changes across different versions.'; 13 | 14 | formService.clear(); 15 | formService.addUsers(); 16 | 17 | formService.setReload(function () { 18 | $http.post('api/endpoint_users/' + endpointService.info.id, { 19 | data: { 20 | users: formService.getMultiSelection('users'), 21 | } 22 | }).then(function (response) { 23 | plotlyService.chart(response.data.map(row => { 24 | return { 25 | x: row.values, 26 | type: 'box', 27 | name: row.user, 28 | marker: {color: row.color} 29 | }; 30 | }), { 31 | xaxis: { 32 | title: 'Execution time (ms)', 33 | }, 34 | yaxis: { 35 | type: 'category', 36 | autorange: 'reversed' 37 | }, 38 | margin: { 39 | l: 200 40 | } 41 | }); 42 | }); 43 | }); 44 | } -------------------------------------------------------------------------------- /flask_monitoringdashboard/frontend/js/controllers/endpointVersion.js: -------------------------------------------------------------------------------- 1 | export function EndpointVersionController($scope, $http, infoService, endpointService, 2 | menuService, formService, plotlyService, $filter) { 3 | endpointService.reset(); 4 | menuService.reset('endpoint_version'); 5 | endpointService.onNameChanged = function (name) { 6 | $scope.title = 'Per-Version Performance for ' + name; 7 | }; 8 | 9 | // Set the information box 10 | infoService.axesText = 'The X-axis presents the execution time in ms. The Y-axis presents the versions that are used.'; 11 | infoService.contentText = 'This graph shows a horizontal boxplot for the versions that are used. With this ' + 12 | 'graph you can found out whether the performance changes across different versions.'; 13 | 14 | formService.clear(); 15 | formService.addVersions(endpointService.info.id); 16 | 17 | formService.setReload(function () { 18 | $http.post('api/endpoint_versions/' + endpointService.info.id, { 19 | data: { 20 | versions: formService.getMultiSelection('versions'), 21 | } 22 | }).then(function (response) { 23 | plotlyService.chart(response.data.map(row => { 24 | return { 25 | x: row.values, 26 | type: 'box', 27 | name: row.version + '
' + ($filter('dateLayout')(row.date)), 28 | marker: {color: row.color} 29 | }; 30 | }), { 31 | xaxis: { 32 | title: 'Execution time (ms)', 33 | }, 34 | yaxis: { 35 | type: 'category', 36 | autorange: 'reversed' 37 | }, 38 | margin: { 39 | l: 200 40 | } 41 | }); 42 | }); 43 | }); 44 | } -------------------------------------------------------------------------------- /flask_monitoringdashboard/frontend/js/controllers/endpointVersionIP.js: -------------------------------------------------------------------------------- 1 | export function EndpointVersionIPController($scope, $http, infoService, endpointService, 2 | menuService, formService, plotlyService, $filter) { 3 | endpointService.reset(); 4 | menuService.reset('endpoint_ip'); 5 | endpointService.onNameChanged = function (name) { 6 | $scope.title = 'IP-Focused Multi-Version Performance for ' + name; 7 | }; 8 | 9 | // Set the information box 10 | infoService.axesText = 'In this graph, the X-axis presents the versions that are used. The Y-axis presents ' + 11 | '(a subset of) all IP-addresses. You can use the slider to select a subset of the all IP-addresses.'; 12 | infoService.contentText = 'The cell color represents the average response time (expressed in ms) of this endpoint ' + 13 | 'per IP per version.'; 14 | 15 | formService.clear(); 16 | formService.addIP(); 17 | formService.addVersions(endpointService.info.id); 18 | 19 | formService.setReload(function () { 20 | $http.post('api/version_ip/' + endpointService.info.id, { 21 | data: { 22 | versions: formService.getMultiSelection('versions'), 23 | ip: formService.getMultiSelection('IP-addresses') 24 | } 25 | }).then(function (response) { 26 | let versions = response.data.versions.map( 27 | obj => obj.version + '
' + $filter('dateLayout')(obj.date) 28 | ); 29 | plotlyService.heatmap(versions, 30 | formService.getMultiSelection('IP-addresses'), response.data.data, { 31 | xaxis: { 32 | type: 'category', 33 | title: 'Versions' 34 | }, 35 | yaxis: { 36 | type: 'category', 37 | title: 'IP-addresses' 38 | } 39 | }); 40 | }); 41 | }); 42 | } -------------------------------------------------------------------------------- /flask_monitoringdashboard/frontend/js/controllers/endpointVersionUser.js: -------------------------------------------------------------------------------- 1 | export function EndpointVersionUserController($scope, $http, infoService, endpointService, 2 | menuService, formService, plotlyService, $filter) { 3 | endpointService.reset(); 4 | menuService.reset('endpoint_user_version'); 5 | endpointService.onNameChanged = function (name) { 6 | $scope.title = 'User-Focused Multi-Version Performance for ' + name; 7 | }; 8 | 9 | // Set the information box 10 | infoService.axesText = 'In this graph, the X-axis presents the versions that are used. The Y-axis ' + 11 | 'presents (a subset of) all unique users, as specified by "dashboard.config.group_by". You can ' + 12 | 'use the slider to select a subset of the all unique users.'; 13 | infoService.contentText = 'The cell color represents the average response time (expressed in ms) of this endpoint ' + 14 | 'per user per version.'; 15 | 16 | formService.clear(); 17 | formService.addUsers(); 18 | formService.addVersions(endpointService.info.id); 19 | 20 | formService.setReload(function () { 21 | $http.post('api/version_user/' + endpointService.info.id, { 22 | data: { 23 | versions: formService.getMultiSelection('versions'), 24 | users: formService.getMultiSelection('users') 25 | } 26 | }).then(function (response) { 27 | let versions = response.data.versions.map( 28 | obj => obj.version + '
' + $filter('dateLayout')(obj.date) 29 | ); 30 | plotlyService.heatmap(versions, 31 | formService.getMultiSelection('users'), 32 | response.data.data, { 33 | xaxis: { 34 | type: 'category', 35 | title: 'Versions' 36 | }, 37 | yaxis: { 38 | type: 'category', 39 | title: 'Users', 40 | autorange: 'reversed' 41 | } 42 | }); 43 | }); 44 | }); 45 | } -------------------------------------------------------------------------------- /flask_monitoringdashboard/frontend/js/controllers/exception.js: -------------------------------------------------------------------------------- 1 | export function ExceptionController($scope, $http, menuService, paginationService, endpointService) { 2 | endpointService.reset(); 3 | menuService.reset('exception_overview'); 4 | 5 | $scope.table = []; 6 | 7 | paginationService.init('exceptions'); 8 | $http.get('api/num_exceptions').then(function (response) { 9 | paginationService.setTotal(response.data); 10 | }); 11 | 12 | paginationService.onReload = function () { 13 | $http.get('api/exception_occurrence/' + paginationService.getLeft() + '/' + paginationService.perPage).then(function (response) { 14 | $scope.table = response.data; 15 | }); 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/frontend/js/controllers/hourlyLoad.js: -------------------------------------------------------------------------------- 1 | export function HourlyLoadController($scope, $http, menuService, plotlyService, infoService, 2 | formService, endpointService, $filter) { 3 | endpointService.reset(); 4 | menuService.reset('hourly_load'); 5 | $scope.title = 'Hourly API Utilization'; 6 | 7 | // Set the information box 8 | infoService.axesText = 'The X-axis represents the dates. The Y-axis presents the hours of the day.'; 9 | infoService.contentText = 'The cell color represents the number of requests that the application ' + 10 | 'received in a single hour. The darker the cell, the more requests it has processed. This information ' + 11 | 'can be used to to discover the peak usage hours of the Flask application.'; 12 | 13 | // Set the form handler 14 | formService.clear(); 15 | let start = formService.addDate('Start date'); 16 | formService.addDate('End date'); 17 | start.value.setDate(start.value.getDate() - 14); 18 | 19 | formService.setReload(function () { 20 | let start = formService.getDate('Start date'); 21 | let end = formService.getDate('End date'); 22 | let times = [...Array(24).keys()].map(d => d + ":00"); 23 | $http.get('api/hourly_load/' + start + '/' + end).then(function (response) { 24 | let x = response.data.days; 25 | let y = times; 26 | let z = response.data.data; 27 | let hover_text = z.map((row, i) => row.map((item, j) => { 28 | return `Date: ${$filter('dateShort')(x[j])}
Time: ${i + ':00'}
Requests: ${item}` 29 | })); 30 | 31 | plotlyService.heatmap(x, y, z, { 32 | yaxis: { 33 | autorange: 'reversed' 34 | }, 35 | margin: {l: 50} 36 | }, hover_text); 37 | }); 38 | }); 39 | formService.reload(); 40 | } 41 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/frontend/js/controllers/monitorLevel.js: -------------------------------------------------------------------------------- 1 | export function MonitorLevelController($scope, $http) { 2 | 3 | $scope.sendForm = function (value) { 4 | $scope.value = value; 5 | $http.post('api/set_rule', 6 | $.param({ 7 | 'name': $scope.name, 8 | 'value': value 9 | }), 10 | { 11 | headers: { 12 | 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8;' 13 | } 14 | }); 15 | }; 16 | 17 | $scope.computeColor = function (level) { 18 | let a = 0.2; 19 | if ($scope.value === level) { 20 | a = 1; 21 | } 22 | 23 | let red = [230, 74, 54]; 24 | let green = [237, 255, 77]; 25 | 26 | // level 0 = total green, level 3 = total red 27 | let percentage = level / 3.0; 28 | let r = red[0] * percentage + green[0] * (1 - percentage); 29 | let g = red[1] * percentage + green[1] * (1 - percentage); 30 | let b = red[2] * percentage + green[2] * (1 - percentage); 31 | 32 | return 'rgba(' + r + ', ' + g + ', ' + b + ', ' + a + ')'; 33 | }; 34 | } 35 | 36 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/frontend/js/controllers/multiVersion.js: -------------------------------------------------------------------------------- 1 | export function MultiVersionController($scope, $http, menuService, formService, infoService, 2 | plotlyService, endpointService) { 3 | endpointService.reset(); 4 | menuService.reset('multi_version'); 5 | $scope.title = 'Multi version API Utilization'; 6 | 7 | // Set the information box 8 | infoService.axesText = 'The X-axis presents the versions that are used. The Y-axis presents the' + 9 | 'endpoints that are found in the Flask application.'; 10 | infoService.contentText = 'The color of the cell presents the distribution of the amount of requests ' + 11 | 'that the application received in a single version for a single endpoint. The darker the cell, ' + 12 | 'the more requests a certain endpoint has processed in that version. Since it displays the ' + 13 | 'distribution of the load, each column sums up to 100%. This information can be used to discover ' + 14 | 'which endpoints process the most requests.'; 15 | 16 | // Set the form handler 17 | formService.clear(); 18 | formService.addVersions(); 19 | formService.addEndpoints(); 20 | 21 | formService.setReload(function () { 22 | let versions = formService.getMultiSelection('versions'); 23 | let endpoints = formService.getMultiSelection('endpoints'); 24 | $http.post('api/multi_version', { 25 | data: { 26 | versions: versions, 27 | endpoints: endpoints 28 | } 29 | }).then(function (response) { 30 | plotlyService.heatmap(versions, endpoints, 31 | response.data.map(l => l.map(i => i == 0 ? 'NaN' : i)), { 32 | xaxis: { 33 | type: 'category', 34 | title: 'Versions' 35 | }, 36 | yaxis: { 37 | type: 'category' 38 | } 39 | }); 40 | }); 41 | }); 42 | } -------------------------------------------------------------------------------- /flask_monitoringdashboard/frontend/js/controllers/statusCodeDistribution.js: -------------------------------------------------------------------------------- 1 | export function StatusCodeDistributionController($scope, $http, infoService, endpointService, menuService, formService, plotlyService) { 2 | menuService.reset('status_code_distribution'); 3 | endpointService.reset(); 4 | 5 | formService.setReload(function () { 6 | const endpointId = endpointService.info.id; 7 | 8 | $http.get('api/endpoint_status_code_summary/' + endpointId).then(function (response) { 9 | 10 | const layout = { 11 | height: 400, 12 | width: 500, 13 | }; 14 | 15 | const distribution = response.data.distribution; 16 | 17 | const statusCodes = Object.keys(distribution); 18 | 19 | const data = [{ 20 | values: statusCodes.map(statusCode => distribution[statusCode]), 21 | labels: statusCodes, 22 | type: 'pie' 23 | }]; 24 | 25 | $scope.error_requests = response.data.error_requests; 26 | 27 | plotlyService.chart(data, layout); 28 | }); 29 | }); 30 | 31 | formService.reload(); 32 | } -------------------------------------------------------------------------------- /flask_monitoringdashboard/frontend/js/controllers/telemetryController.js: -------------------------------------------------------------------------------- 1 | export function TelemetryController($scope, $http, $window) { 2 | 3 | // Check if telemetry response is already stored in local storage 4 | const telemetryAnswered = $window.localStorage.getItem('telemetryAnswered') === 'true'; 5 | 6 | // Control the visibility of the telemetry prompt based on previous response 7 | $scope.telemetryShow = !telemetryAnswered; 8 | $scope.followUpShow = false; 9 | 10 | // Function to fetch telemetry consent status from database 11 | $scope.fetchTelemetryAnswered = function () { 12 | $http.get(`/dashboard/telemetry/get_is_telemetry_answered`) 13 | .then(function (response) { 14 | $scope.telemetryShow = !response.data.is_telemetry_answered; 15 | }, function (error) { 16 | console.error('Error fetching telemetry consent:', error); 17 | }); 18 | }; 19 | $scope.fetchTelemetryAnswered(); 20 | 21 | // Function to handle user response to telemetry prompt 22 | $scope.handleTelemetry = function (consent) { 23 | $scope.telemetryShow = false; 24 | $scope.followUpShow = !consent; 25 | 26 | $http.post('/dashboard/telemetry/accept_telemetry_consent', { 'consent': consent }) 27 | .then(function (response) { 28 | $scope.telemetryShow = false; 29 | $window.localStorage.setItem('telemetryAnswered', 'true'); 30 | }, function (error) { 31 | console.error('Error updating telemetry consent:', error); 32 | }); 33 | }; 34 | 35 | // Object to track reasons for declining telemetry 36 | $scope.reasons = { 37 | privacy: false, 38 | performance: false, 39 | trust: false, 40 | other: false 41 | }; 42 | $scope.customReason = ''; 43 | 44 | // Function to submit follow-up feedback 45 | $scope.submitFollowUp = function () { 46 | $scope.followUpShow = false; 47 | 48 | var feedback = []; 49 | for (var key in $scope.reasons) { 50 | if ($scope.reasons[key]) { 51 | if (key === 'other' && $scope.customReason.trim() !== '') { 52 | feedback.push({ key: 'other', other_reason: $scope.customReason }); 53 | } else { 54 | feedback.push({ key: key }); 55 | } 56 | } 57 | } 58 | 59 | $http.post('/dashboard/telemetry/submit_follow_up', { feedback: feedback }) 60 | .then(function (response) { 61 | }, function (error) { 62 | console.error('Error sending feedback:', error); 63 | }); 64 | }; 65 | } -------------------------------------------------------------------------------- /flask_monitoringdashboard/frontend/js/controllers/util.js: -------------------------------------------------------------------------------- 1 | export function MenuController($scope, menuService) { 2 | $scope.menu = menuService; 3 | } 4 | 5 | export function InfoController($scope, infoService) { 6 | $scope.info = infoService; 7 | } 8 | 9 | export function FormController($scope, formService) { 10 | $scope.handler = formService; 11 | } 12 | 13 | export function EndpointController($scope, endpointService) { 14 | $scope.endpoint = endpointService; 15 | } 16 | 17 | export function PaginationController($scope, paginationService) { 18 | $scope.pagination = paginationService; 19 | } 20 | 21 | export function ModalController($scope, $window, $browser, modalService){ 22 | modalService.setConfirm('logout', function(){ 23 | $window.location.href = $browser.baseHref() + 'logout'; 24 | }); 25 | 26 | $scope.modal = modalService; 27 | } -------------------------------------------------------------------------------- /flask_monitoringdashboard/frontend/js/directives.js: -------------------------------------------------------------------------------- 1 | export default function applyDirectives(app) { 2 | app.directive('pagination', function () { 3 | return { 4 | templateUrl: 'static/elements/pagination.html', 5 | controller: 'PaginationController' 6 | } 7 | }); 8 | 9 | app.directive('menu', function () { 10 | return { 11 | templateUrl: 'static/elements/menu.html', 12 | controller: 'MenuController' 13 | } 14 | }); 15 | 16 | app.directive('monitorlevel', function () { 17 | return { 18 | templateUrl: 'static/elements/monitorLevel.html', 19 | controller: 'MonitorLevelController', 20 | scope: { 21 | name: '=', 22 | value: '=' 23 | } 24 | } 25 | }); 26 | 27 | app.directive('endpointdetails', function () { 28 | return { 29 | templateUrl: 'static/elements/endpointDetails.html', 30 | controller: 'EndpointController', 31 | } 32 | }); 33 | 34 | app.directive('modal', function () { 35 | return { 36 | transclude: true, 37 | templateUrl: 'static/elements/modal.html', 38 | controller: 'ModalController', 39 | scope: { 40 | name: '=', 41 | title: '=', 42 | yes: '=?', 43 | no: '=?' 44 | }, 45 | } 46 | }); 47 | } -------------------------------------------------------------------------------- /flask_monitoringdashboard/frontend/js/services/endpoint.js: -------------------------------------------------------------------------------- 1 | export default function ($http, $routeParams) { 2 | let that = this; 3 | this.info = { 4 | id: 0, 5 | endpoint: '' 6 | }; 7 | this.graphId = 0; 8 | 9 | this.customGraphs = []; 10 | this.getGraph = function () { 11 | return this.customGraphs.find(o => o.graph_id === that.graphId); 12 | }; 13 | 14 | this.getGraphTitle = function () { 15 | let graph = this.getGraph(); 16 | if (typeof graph !== 'undefined') { 17 | return graph.title; 18 | } 19 | return ''; 20 | }; 21 | 22 | $http.get('api/custom_graphs').then(function (response) { 23 | that.customGraphs = response.data; 24 | if (that.graphId !== 0) { 25 | that.onNameChanged(that.getGraphTitle()) 26 | } 27 | }); 28 | 29 | this.reset = function () { 30 | if (typeof $routeParams.endpointId !== 'undefined') { 31 | this.info.id = $routeParams.endpointId; 32 | this.getInfo(); 33 | } else if (typeof $routeParams.graphId !== 'undefined') { 34 | this.graphId = $routeParams.graphId; 35 | } else { 36 | this.info.id = 0; 37 | } 38 | }; 39 | 40 | this.onNameChanged = function (name) { 41 | }; 42 | 43 | this.getInfo = function () { 44 | $http.get('api/endpoint_info/' + this.info.id).then(function (response) { 45 | that.info = response.data; 46 | that.onNameChanged(that.info.endpoint); 47 | }); 48 | }; 49 | }; -------------------------------------------------------------------------------- /flask_monitoringdashboard/frontend/js/services/info.js: -------------------------------------------------------------------------------- 1 | export default function () { 2 | this.graphText = 'You can hover the graph with your mouse to see the actual values. You can also use the ' + 3 | 'buttons at the top of the graph to select a subset of graph, scale it accordingly, or save the graph ' + 4 | 'as a PNG image.'; 5 | this.axesText = ''; 6 | this.contentText = ''; 7 | } -------------------------------------------------------------------------------- /flask_monitoringdashboard/frontend/js/services/menu.js: -------------------------------------------------------------------------------- 1 | export default function ($http, endpointService) { 2 | this.page = ''; 3 | this.endpoint = endpointService; 4 | 5 | this.reset = function (page) { 6 | if (this.page !== page) { 7 | this.page = page; 8 | } 9 | if (page.substr(0, 'custom_graph'.length) === 'custom_graph') { 10 | $('#collapseCustomGraphs').collapse('show'); 11 | } else { 12 | $('#collapseCustomGraphs').collapse('hide'); 13 | } 14 | 15 | var dashboardPages = ['overview', 'exception_overview', 'hourly_load', 'multi_version', 'daily_load', 'api_performance', 'reporting']; 16 | 17 | if (dashboardPages.includes(page)) { 18 | $('#collapseDashboard').collapse('show'); 19 | } else { 20 | $('#collapseDashboard').collapse('hide'); 21 | } 22 | 23 | var endpointPages = [ 24 | 'endpoint_hourly', 'endpoint_user_version', 'endpoint_ip', 'endpoint_version', 'endpoint_user', 25 | 'endpoint_profiler', 'endpoint_grouped_profiler', 'endpoint_exception', 'endpoint_outlier', 'status_code_distribution' 26 | ]; 27 | 28 | if (endpointPages.includes(page)) { 29 | $('#collapseEndpoint').collapse('show'); 30 | } else { 31 | $('#collapseEndpoint').collapse('hide'); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /flask_monitoringdashboard/frontend/js/services/modal.js: -------------------------------------------------------------------------------- 1 | export default function () { 2 | this.action = {}; 3 | this.error = {}; 4 | 5 | this.setConfirm = function(name, action){ 6 | this.action[name] = action; 7 | } 8 | 9 | this.setErrorMessage = function(name, message){ 10 | this.error[name] = message; 11 | } 12 | 13 | this.confirm = function(name){ 14 | this.action[name](); 15 | } 16 | } -------------------------------------------------------------------------------- /flask_monitoringdashboard/frontend/js/services/pagination.js: -------------------------------------------------------------------------------- 1 | export default function () { 2 | 3 | this.init = function (name) { 4 | this.page = 1; 5 | this.perPage = 5; 6 | this.total = 0; 7 | this.name = name; 8 | }; 9 | 10 | this.maxPages = function () { 11 | return Math.ceil(this.total / this.perPage); 12 | }; 13 | 14 | this.onReload = function () { 15 | }; 16 | 17 | this.getLeft = function () { 18 | return (this.page - 1) * this.perPage; 19 | }; 20 | 21 | this.getRight = function () { 22 | return Math.min(this.total, this.getLeft() + this.perPage); 23 | }; 24 | 25 | this.getFirstPage = function () { 26 | let pages = this.getPages(); 27 | return pages.length > 0 ? pages[0] : this.page; 28 | }; 29 | 30 | this.getLastPage = function () { 31 | let pages = this.getPages(); 32 | return pages.length > 0 ? pages[pages.length - 1] : this.page; 33 | }; 34 | 35 | this.goto = function (p) { 36 | this.page = p; 37 | this.onReload(); 38 | }; 39 | 40 | this.setTotal = function (t) { 41 | this.total = t; 42 | this.onReload(); 43 | }; 44 | 45 | this.getPages = function () { 46 | let left = this.page - 1; 47 | let right = this.page + 1; 48 | let range = []; 49 | 50 | if (left <= 0) { 51 | right -= left - 1; 52 | left = 1; 53 | } 54 | if (right > this.maxPages()) { 55 | right = this.maxPages(); 56 | } 57 | 58 | if (left == 2) { 59 | range.push(1); 60 | } else if (left == 3) { 61 | range.push(1, 2); 62 | } else if (left > 3) { 63 | range.push(1, '...'); 64 | } 65 | 66 | for (let i = left; i <= right; i++) { 67 | range.push(i); 68 | } 69 | if (this.maxPages() - right > 2) { 70 | range.push('...', this.maxPages()); 71 | } else if (this.maxPages() - right == 2) { 72 | range.push(this.maxPages() - 1, this.maxPages()); 73 | } else if (this.maxPages() - right == 1) { 74 | range.push(this.maxPages()); 75 | } 76 | return range; 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/frontend/js/services/plotly.js: -------------------------------------------------------------------------------- 1 | export default function (formService) { 2 | let layout = { 3 | height: 600, 4 | margin: {l: 200} 5 | }; 6 | let options = { 7 | displaylogo: false, 8 | responsive: true 9 | }; 10 | 11 | this.heatmap = function (x, y, z, layout_ext, hover_text) { 12 | this.chart([{ 13 | x: x, 14 | y: y, 15 | z: z.map(l => l.map(i => i === 0 ? NaN : i)), 16 | colorscale: 'YIOrRd', 17 | reversescale: true, 18 | type: 'heatmap', 19 | text: hover_text, 20 | hoverinfo: (hover_text === undefined ? undefined : 'text') 21 | }], $.extend({}, layout, layout_ext)); 22 | }; 23 | 24 | this.chart = function (data, layout_ext) { 25 | formService.isLoading = false; 26 | Plotly.newPlot('chart', data, $.extend({}, layout, layout_ext), options); 27 | } 28 | }; -------------------------------------------------------------------------------- /flask_monitoringdashboard/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "dev": "webpack --watch", 9 | "build": "webpack --mode production" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@babel/plugin-transform-class-properties": "^7.22.5", 16 | "@fortawesome/fontawesome-free": "^5.14.0", 17 | "@npmcli/fs": "^3.1.0", 18 | "@popperjs/core": "^2.11.8", 19 | "angular": "^1.6.10", 20 | "angular-datatables": "^16.0.0", 21 | "angular-route": "^1.8.0", 22 | "bootstrap": "^5.3.2", 23 | "css-loader": "^6.8.1", 24 | "d3": "^7.8.5", 25 | "datatables": "^1.10.18", 26 | "datatables.net-bs4": "^1.10.21", 27 | "datatables.net-dt": "^1.10.21", 28 | "jquery": "^3.5.1", 29 | "moment": "^2.27.0", 30 | "plotly.js": "^2.27.0", 31 | "plotly.js-basic-dist": "^1.54.6", 32 | "plotly.js-basic-dist-min": "^1.54.6", 33 | "plotly.js-cartesian-dist": "^1.54.6", 34 | "plotly.js-dist": "^1.54.6", 35 | "postcss-loader": "^7.3.3", 36 | "sass-loader": "^13.3.2", 37 | "sqlite3": "^5.1.6", 38 | "style-loader": "^1.2.1", 39 | "sunburst-chart": "^1.11.1", 40 | "url-loader": "^4.1.0" 41 | }, 42 | "devDependencies": { 43 | "@babel/core": "^7.10.5", 44 | "@babel/preset-env": "^7.10.4", 45 | "@babel/preset-react": "^7.10.4", 46 | "babel-loader": "^8.1.0", 47 | "font-awesome": "^4.7.0", 48 | "mini-css-extract-plugin": "^2.7.6", 49 | "node-gyp": "^10.0.0", 50 | "sass": "^1.69.5", 51 | "webpack": "^5.89.0", 52 | "webpack-cli": "^5.1.4" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/frontend/sass/app.scss: -------------------------------------------------------------------------------- 1 | // Bootstrap 2 | @import "~bootstrap/scss/bootstrap"; 3 | 4 | 5 | // Font awesome 6 | $fa-font-path: "../fonts"; 7 | @import "~@fortawesome/fontawesome-free/scss/fontawesome.scss"; 8 | @import "~@fortawesome/fontawesome-free/scss/solid.scss"; 9 | 10 | // Bootstrap Data tables 11 | @import "~datatables.net-bs4/css/dataTables.bootstrap4.min.css"; 12 | 13 | @import "./custom.css"; -------------------------------------------------------------------------------- /flask_monitoringdashboard/frontend/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 3 | 4 | module.exports = { 5 | mode: process.env.NODE_ENV === 'production' ? 'production' : 'development', 6 | entry: ['./js/app.js', './sass/app.scss'], 7 | output: { 8 | path: path.resolve(__dirname, '../static'), 9 | filename: 'js/app.js', 10 | publicPath: '/static/' 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.scss$/, 16 | use: [ 17 | MiniCssExtractPlugin.loader, 18 | { 19 | loader: 'css-loader', 20 | options: { 21 | url: false 22 | } 23 | }, 24 | 'sass-loader' 25 | ] 26 | }, 27 | { 28 | test: /\.(woff(2)?|ttf|eot|svg|otf)(\?v=\d+\.\d+\.\d+)?$/, 29 | type: 'asset/resource', 30 | generator: { 31 | filename: 'fonts/[name][ext]' 32 | } 33 | } 34 | ] 35 | }, 36 | plugins: [ 37 | new MiniCssExtractPlugin({ 38 | filename: 'css/[name].css', 39 | }) 40 | ] 41 | }; 42 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/static/elements/endpointDetails.html: -------------------------------------------------------------------------------- 1 |
2 |

Endpoint details

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 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
Endpoint{{ endpoint.info.endpoint }}
Color
ID{{ endpoint.info.id }}
Rule(s){{ endpoint.info.rules.join(', ') }}
HTTP Methods{{ endpoint.info.methods.join(', ') }}
Monitoring-level 29 | 30 |
URL to endpoint{{ endpoint.info.url }}
Total number of hits{{ endpoint.info.total_hits | number }}
41 |
42 |
43 |
-------------------------------------------------------------------------------- /flask_monitoringdashboard/static/elements/modal.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/static/elements/monitorLevel.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/static/elements/pagination.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Displaying {{ pagination.getLeft() + 1 }} 4 | - {{ pagination.getRight() }} {{ pagination.name }}: {{ pagination.total | number }} in total 5 |
6 |
7 | 21 |
22 |
-------------------------------------------------------------------------------- /flask_monitoringdashboard/static/fonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flask-dashboard/Flask-MonitoringDashboard/3cebbd62a8fd248313eeb630375d5dd07a6ff572/flask_monitoringdashboard/static/fonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /flask_monitoringdashboard/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flask-dashboard/Flask-MonitoringDashboard/3cebbd62a8fd248313eeb630375d5dd07a6ff572/flask_monitoringdashboard/static/img/favicon.ico -------------------------------------------------------------------------------- /flask_monitoringdashboard/static/img/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flask-dashboard/Flask-MonitoringDashboard/3cebbd62a8fd248313eeb630375d5dd07a6ff572/flask_monitoringdashboard/static/img/header.png -------------------------------------------------------------------------------- /flask_monitoringdashboard/static/pages/exception_overview.html: -------------------------------------------------------------------------------- 1 |
2 |

Exceptions

3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
Last seenFirst occurenceEndpointTypeMessageCount
{{ row.latest_timestamp | dateDifference }}{{ row.first_timestamp | dateDifference }}{{ row.endpoint }}{{ row.type }}{{ row.message }}{{ row.count }}
26 | 27 |
28 |
29 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/static/pages/grouped_profiler.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

{{ title }}

6 |
7 | 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 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | 52 | 53 | 54 |
Code-lineHitsAverage timeStandard deviationTotal
Absolute%Absolute%
38 | {{ row.code }} 39 | 42 | {{ row.hits }} 45 | {{ row.hits / table[0].hits * 100 | number: 1 }}%{{ row.duration / row.hits | duration }}{{ row.std | duration }}{{ row.duration | duration }} 51 | {{ (row.duration / table[0].duration * 100) | number: 1 }}%
55 |
56 | 57 |
58 |

Sunburst

59 |
60 |
61 |
62 |
63 | 64 | 65 |
-------------------------------------------------------------------------------- /flask_monitoringdashboard/static/pages/plotly_graph.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ title }}

4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | 17 |
18 |
19 |
20 |
Graph options
21 |
22 |
23 |
24 | 25 |
26 | 27 |
28 |
29 |
30 | 32 | 35 |
36 | 37 |
38 |
39 |
40 |
41 |
42 |
43 |
Information
44 |
45 | Graph 46 |

{{ info.graphText }}

47 | Axes 48 |

{{ info.axesText }}

49 | Content 50 |

{{ info.contentText }}

51 |
52 |
53 |
54 |
55 | 56 | 57 |
-------------------------------------------------------------------------------- /flask_monitoringdashboard/static/pages/profiler.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ title }}

4 |
5 | 6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | 14 | 15 |
16 |
17 |
18 |
Request id: {{ request.id }}
19 |
Request date: {{ request.time_requested | dateLayout }}
20 |
21 | 23 |
24 |
25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 42 | 45 | 48 | 49 | 50 |
Code-lineDurationPercentage
37 | {{ row.code.code }} 38 | 41 | 43 | 44 | 46 | {{ (row.duration / request.stack_lines[0].duration * 100) | number: 1 }}% 47 |
51 |
52 | 53 | 54 |
55 |
56 | 57 | 58 |
-------------------------------------------------------------------------------- /flask_monitoringdashboard/static/pages/status_code_distribution.html: -------------------------------------------------------------------------------- 1 |
2 |

Status code distribution

3 |
4 |
5 |
6 |
7 | 8 |
9 |

Error requests

10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
TimeStatus Code
{{ row.time_requested | dateLayout }}{{ row.status_code }}
25 |
26 |
27 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/templates/fmd_login.html: -------------------------------------------------------------------------------- 1 | {% extends "fmd_base.html" %} {% block body %} 2 | 3 | 4 |
5 | {% if show_login_banner %} 6 |
7 |
8 |
9 |
10 | 14 |
15 | Automatically monitor the evolving performance of Flask/Python web 17 | services 19 |
20 |
21 |
22 | {% endif %} 23 | 24 |
25 |
26 | 27 |
28 |
29 |
Login
30 |
31 | 32 |
33 |
34 |
35 | 36 | 45 |
46 | 47 |
48 | 51 | 58 |
59 | 60 | 63 |
64 |
65 | {% if show_login_footer %} 66 |
67 | For advanced documentation, see 68 | this site 73 |
74 | {% endif %} 75 |
76 |
77 |
78 |
79 |
80 | 81 | {% endblock %} 82 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/views/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Main class for adding all route-functions to user_app. 3 | Setup requires only to import this file. All other imports are done in this file 4 | """ 5 | from flask import render_template 6 | from flask.helpers import send_from_directory 7 | 8 | from flask_monitoringdashboard import loc, blueprint, config 9 | from flask_monitoringdashboard.core.auth import secure 10 | 11 | 12 | @blueprint.route('/static/') 13 | def static(filename): 14 | """ 15 | Serve static files 16 | :param filename: filename in the /static file 17 | :return: content of the file 18 | """ 19 | return send_from_directory(loc() + 'static', filename) 20 | 21 | 22 | @blueprint.route('/', defaults={'path': ''}) 23 | @blueprint.route('/') # Catch-All URL: http://flask.pocoo.org/snippets/57/ 24 | @secure 25 | def index(path): 26 | return render_template('fmd_base.html', 27 | blueprint_name=config.blueprint_name, 28 | brand_name=config.brand_name, 29 | title_name=config.title_name, 30 | description=config.description, 31 | ) 32 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/views/custom.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from flask.json import jsonify 4 | 5 | from flask_monitoringdashboard import blueprint 6 | from flask_monitoringdashboard.core.auth import secure 7 | from flask_monitoringdashboard.core.custom_graph import get_custom_graphs 8 | from flask_monitoringdashboard.core.telemetry import post_to_back_if_telemetry_enabled 9 | from flask_monitoringdashboard.database import row2dict, session_scope 10 | from flask_monitoringdashboard.database.custom_graph import get_graph_data 11 | 12 | 13 | @blueprint.route('/api/custom_graphs') 14 | @secure 15 | def custom_graphs(): 16 | post_to_back_if_telemetry_enabled(**{'name': 'custom_graphs'}) 17 | graphs = get_custom_graphs() 18 | if not graphs: 19 | return jsonify([]) 20 | return jsonify([row2dict(row) for row in graphs if row is not None]) 21 | 22 | 23 | @blueprint.route('/api/custom_graph///') 24 | @secure 25 | def custom_graph(graph_id, start_date, end_date): 26 | post_to_back_if_telemetry_enabled(**{'name': f'custom_graph{graph_id}'}) 27 | start_date = datetime.datetime.strptime(start_date, '%Y-%m-%d') 28 | end_date = datetime.datetime.strptime(end_date, '%Y-%m-%d') 29 | with session_scope() as session: 30 | return jsonify(get_graph_data(session, graph_id, start_date, end_date)) 31 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/views/deployment.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from flask import jsonify 4 | 5 | from flask_monitoringdashboard import blueprint, config 6 | from flask_monitoringdashboard.core.auth import secure 7 | from flask_monitoringdashboard.core.timezone import to_local_datetime 8 | from flask_monitoringdashboard.core.telemetry import post_to_back_if_telemetry_enabled 9 | from flask_monitoringdashboard.core.utils import get_details 10 | from flask_monitoringdashboard.database import session_scope 11 | 12 | 13 | 14 | @blueprint.route('/api/deploy_details') 15 | @secure 16 | def deploy_details(): 17 | """ 18 | :return: A JSON-object with deployment details 19 | """ 20 | post_to_back_if_telemetry_enabled(**{'name': 'deploy_details'}) 21 | 22 | with session_scope() as session: 23 | details = get_details(session) 24 | details['first-request'] = to_local_datetime( 25 | datetime.datetime.fromtimestamp(details['first-request']) 26 | ) 27 | details['first-request-version'] = to_local_datetime( 28 | datetime.datetime.fromtimestamp(details['first-request-version']) 29 | ) 30 | return jsonify(details) 31 | 32 | 33 | @blueprint.route('/api/deploy_config') 34 | @secure 35 | def deploy_config(): 36 | """ 37 | :return: A JSON-object with configuration details 38 | """ 39 | post_to_back_if_telemetry_enabled(**{'name': 'deploy_config'}) 40 | return jsonify( 41 | { 42 | 'database_name': config.database_name, 43 | 'outlier_detection_constant': config.outlier_detection_constant, 44 | 'timezone': str(config.timezone), 45 | 'colors': config.colors, 46 | } 47 | ) 48 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/views/outlier.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | 3 | from flask_monitoringdashboard.controllers.outliers import get_outlier_graph, get_outlier_table 4 | from flask_monitoringdashboard.database.count import count_outliers 5 | 6 | from flask_monitoringdashboard.database import session_scope 7 | 8 | from flask_monitoringdashboard.core.auth import secure 9 | from flask_monitoringdashboard.core.telemetry import post_to_back_if_telemetry_enabled 10 | from flask_monitoringdashboard import blueprint 11 | 12 | 13 | @blueprint.route('/api/num_outliers/') 14 | @secure 15 | def num_outliers(endpoint_id): 16 | post_to_back_if_telemetry_enabled(**{'name': f'num_outliers/{endpoint_id}'}) 17 | with session_scope() as session: 18 | return jsonify(count_outliers(session, endpoint_id)) 19 | 20 | 21 | @blueprint.route('/api/outlier_graph/') 22 | @secure 23 | def outlier_graph(endpoint_id): 24 | post_to_back_if_telemetry_enabled(**{'name': f'outlier_graph/{endpoint_id}'}) 25 | with session_scope() as session: 26 | return jsonify(get_outlier_graph(session, endpoint_id)) 27 | 28 | 29 | @blueprint.route('/api/outlier_table///') 30 | @secure 31 | def outlier_table(endpoint_id, offset, per_page): 32 | post_to_back_if_telemetry_enabled(**{'name': f'outlier_table/{endpoint_id}'}) 33 | with session_scope() as session: 34 | return jsonify(get_outlier_table(session, endpoint_id, offset, per_page)) 35 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/views/profiler.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | 3 | from flask_monitoringdashboard.controllers.profiler import get_profiler_table, get_grouped_profiler 4 | from flask_monitoringdashboard.database import session_scope, Endpoint 5 | 6 | from flask_monitoringdashboard.core.auth import secure 7 | from flask_monitoringdashboard.core.telemetry import post_to_back_if_telemetry_enabled 8 | 9 | from flask_monitoringdashboard import blueprint 10 | from flask_monitoringdashboard.database.count import count_profiled_requests 11 | 12 | 13 | @blueprint.route('/api/num_profiled/') 14 | @secure 15 | def num_profiled(endpoint_id): 16 | post_to_back_if_telemetry_enabled(**{'name': f'num_profiled/{endpoint_id}'}) 17 | with session_scope() as session: 18 | return jsonify(count_profiled_requests(session, endpoint_id)) 19 | 20 | 21 | @blueprint.route('/api/profiler_table///') 22 | @secure 23 | def profiler_table(endpoint_id, offset, per_page): 24 | post_to_back_if_telemetry_enabled(**{'name': f'profiled_table/{endpoint_id}'}) 25 | with session_scope() as session: 26 | return jsonify(get_profiler_table(session, endpoint_id, offset, per_page)) 27 | 28 | 29 | @blueprint.route('/api/grouped_profiler/') 30 | @secure 31 | def grouped_profiler(endpoint_id): 32 | post_to_back_if_telemetry_enabled(**{'name': f'grouped_profiler/{endpoint_id}'}) 33 | with session_scope() as session: 34 | return jsonify(get_grouped_profiler(session, endpoint_id)) 35 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/views/pruning.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flask import request 4 | from flask.json import jsonify 5 | 6 | from flask_monitoringdashboard import blueprint 7 | from flask_monitoringdashboard.core.custom_graph import scheduler 8 | from flask_monitoringdashboard.core.telemetry import post_to_back_if_telemetry_enabled 9 | from flask_monitoringdashboard.core.database_pruning import prune_database_older_than_weeks 10 | from flask_monitoringdashboard.database import session_scope 11 | 12 | 13 | @blueprint.route('/database_pruning/prune_on_demand', methods=['POST']) 14 | def prune_database_on_demand(): 15 | """ 16 | Endpoint for pruning the database of Request and optionally CustomGraph data older than the specified number of weeks 17 | """ 18 | data = request.json 19 | weeks = data.get('age_threshold_weeks') 20 | delete_custom_graph_data = data.get('delete_custom_graph_data') 21 | 22 | # Validation 23 | if not isinstance(weeks, int) or weeks < 0: 24 | return jsonify({'error': 'age_threshold_weeks must be a natural number'}), 400 25 | if not isinstance(delete_custom_graph_data, bool): 26 | return jsonify({'error': 'delete_custom_graph_data must be a boolean'}), 400 27 | 28 | # Prune database 29 | prune_database_older_than_weeks(weeks, delete_custom_graph_data) 30 | 31 | # Post info to telemetry if enabled 32 | post_data = {'age_threshold_weeks': weeks, 'delete_custom_graphs': delete_custom_graph_data} 33 | post_to_back_if_telemetry_enabled('DatabasePruning', **post_data) 34 | 35 | return jsonify({'message': 'Database pruning complete'}), 200 36 | 37 | 38 | @blueprint.route('/database_pruning/get_pruning_schedule', methods=['GET']) 39 | def get_pruning_schedule(): 40 | job = scheduler.get_job('database_pruning_schedule') 41 | 42 | # Check if the job exists and return details 43 | if job: 44 | return jsonify({ 45 | 'year': str(job.trigger.fields[0]), 46 | 'month': str(job.trigger.fields[1]), 47 | 'day_of_the_month': str(job.trigger.fields[2]), 48 | 'week': str(job.trigger.fields[3]), 49 | 'day_of_the_week': str(job.trigger.fields[4]), 50 | 'hour': str(job.trigger.fields[5]), 51 | 'minute': str(job.trigger.fields[6]), 52 | 'second': str(job.trigger.fields[7]), 53 | 'next_run_time': job.next_run_time, 54 | 'weeks_to_keep': job.args[0], 55 | 'delete_custom_graph_data': job.args[1], 56 | }) 57 | else: 58 | return jsonify({'error': 'No pruning schedule found'}), 404 59 | 60 | 61 | @blueprint.route('/database_pruning/get_database_size', methods=['GET']) 62 | def get_database_size(): 63 | """ 64 | Endpoint for getting the size of the database in MB 65 | """ 66 | with session_scope() as session: 67 | engine = session.bind 68 | relative_path = engine.url.database 69 | 70 | if relative_path: 71 | absolute_path = os.path.abspath(relative_path) 72 | size_in_bytes = os.path.getsize(absolute_path) 73 | size_in_mb = size_in_bytes / 1024 / 1024 74 | return jsonify({'size': f'{size_in_mb:.2f} MB'}) 75 | 76 | else: 77 | return jsonify({'size': 'N/A'}), 404 78 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/views/request.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from flask import jsonify 4 | 5 | from flask_monitoringdashboard.controllers.requests import get_num_requests_data, get_hourly_load 6 | from flask_monitoringdashboard.database import session_scope 7 | 8 | from flask_monitoringdashboard.core.auth import secure 9 | from flask_monitoringdashboard.core.telemetry import post_to_back_if_telemetry_enabled 10 | 11 | from flask_monitoringdashboard import blueprint 12 | 13 | 14 | @blueprint.route('/api/requests//') 15 | @secure 16 | def num_requests(start_date, end_date): 17 | """ 18 | :param start_date: must be in the following form: yyyy-mm-dd 19 | :param end_date: must be in the following form: yyyy-mm-dd 20 | :return: A JSON-list with the following object: { 21 | 'name': 'endpoint', 22 | 'values': [list with an integer per day], 23 | } 24 | """ 25 | post_to_back_if_telemetry_enabled(**{'name': 'requests'}) 26 | start_date = datetime.datetime.strptime(start_date, '%Y-%m-%d') 27 | end_date = datetime.datetime.strptime(end_date, '%Y-%m-%d') 28 | 29 | with session_scope() as session: 30 | return jsonify(get_num_requests_data(session, start_date, end_date)) 31 | 32 | 33 | @blueprint.route('/api/hourly_load//') 34 | @blueprint.route('/api/hourly_load///') 35 | @secure 36 | # both days must be in the form: yyyy-mm-dd 37 | def hourly_load(start_date, end_date, endpoint_id=None): 38 | """ 39 | :param start_date: must be in the following form: yyyy-mm-dd 40 | :param end_date: must be in the following form: yyyy-mm-dd 41 | :param endpoint_id: if specified, filter on this endpoint 42 | :return: A JSON-object: { 43 | 'data': [ [hits for 0:00-1:00 per day], [hits for 1:00-2:00 per day], ...] 44 | 'days': ['start_date', 'start_date+1', ..., 'end_date'], 45 | } 46 | """ 47 | post_to_back_if_telemetry_enabled(**{'name': 'hourly_load'}) 48 | start_date = datetime.datetime.strptime(start_date, '%Y-%m-%d') 49 | end_date = datetime.datetime.strptime(end_date, '%Y-%m-%d') 50 | 51 | with session_scope() as session: 52 | return jsonify(get_hourly_load(session, endpoint_id, start_date, end_date)) 53 | -------------------------------------------------------------------------------- /flask_monitoringdashboard/views/telemetry.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify, request, json 2 | from sqlalchemy.exc import SQLAlchemyError 3 | from flask_monitoringdashboard.core.auth import secure 4 | from flask_monitoringdashboard import blueprint, telemetry_config 5 | from flask_monitoringdashboard.core.config import TelemetryConfig 6 | from flask_monitoringdashboard.core.telemetry import get_telemetry_user, post_to_back_if_telemetry_enabled 7 | from flask_monitoringdashboard.database import session_scope 8 | 9 | 10 | @blueprint.route('/telemetry/accept_telemetry_consent', methods=['POST']) 11 | def accept_telemetry_consent(): 12 | with session_scope() as session: 13 | try: 14 | telemetry_user = get_telemetry_user(session) 15 | data = request.get_json() 16 | if 'consent' in data and isinstance(data['consent'], bool) and data.get('consent'): # if True then agreed 17 | telemetry_user.monitoring_consent = TelemetryConfig.ACCEPTED # agree to monitoring 18 | telemetry_config.telemetry_consent = True 19 | else: 20 | telemetry_user.monitoring_consent = TelemetryConfig.REJECTED # reject monitoring 21 | telemetry_config.telemetry_consent = False 22 | session.commit() 23 | 24 | except SQLAlchemyError as e: 25 | print('error committing telemetry consent to database', e) 26 | session.rollback() 27 | 28 | # Return no content 29 | return '', 204 30 | 31 | 32 | @blueprint.route('/telemetry/get_is_telemetry_answered', methods=['GET']) 33 | def get_is_telemetry_answered(): 34 | with session_scope() as session: 35 | telemetry_user = get_telemetry_user(session) 36 | res = True if telemetry_user.monitoring_consent in (TelemetryConfig.REJECTED, TelemetryConfig.ACCEPTED) else False 37 | return {'is_telemetry_answered': res} 38 | 39 | @blueprint.route('/telemetry/get_is_telemetry_accepted', methods=['GET']) 40 | def get_is_telemetry_accepted(): 41 | with session_scope() as session: 42 | telemetry_user = get_telemetry_user(session) 43 | res = True if telemetry_user.monitoring_consent == TelemetryConfig.ACCEPTED else False 44 | return {'is_telemetry_accepted': res} 45 | 46 | @blueprint.route('/telemetry/submit_follow_up', methods=['POST']) 47 | def submit_follow_up(): 48 | data = request.json 49 | feedback = data.get('feedback') 50 | 51 | post_to_back_if_telemetry_enabled('FollowUp', feedback=feedback) 52 | 53 | return jsonify({'message': 'Feedback submitted successfully'}), 200 54 | 55 | 56 | -------------------------------------------------------------------------------- /migration/migrate_v2_to_v3.py: -------------------------------------------------------------------------------- 1 | """ 2 | Use this file for migrating the Database such that status codes can be tracked. 3 | Before running the script, make sure to specify DB_URL on line 15 4 | Refer to http://docs.sqlalchemy.org/en/latest/core/engines.html on how to configure this. 5 | """ 6 | from contextlib import contextmanager 7 | 8 | 9 | from sqlalchemy import create_engine 10 | from sqlalchemy.orm import sessionmaker 11 | 12 | from sqlalchemy.ext.declarative import declarative_base 13 | 14 | # DB_URL = 'dialect+driver://username:password@host:port/db' 15 | DB_URL = 'mysql+pymysql://user:password@localhost:3306/database' 16 | 17 | engine = create_engine(DB_URL) 18 | 19 | Base = declarative_base() 20 | Base.metadata.create_all(engine) 21 | Base.metadata.bind = engine 22 | 23 | DBSession = sessionmaker(bind=engine) 24 | 25 | connection = engine.connect() 26 | 27 | 28 | @contextmanager 29 | def session_scope(): 30 | """ 31 | When accessing the database, use the following syntax: 32 | with session_scope() as session: 33 | session.query(...) 34 | 35 | :return: the session for accessing the database 36 | """ 37 | session = DBSession() 38 | try: 39 | yield session 40 | session.commit() 41 | except Exception: 42 | session.rollback() 43 | raise 44 | finally: 45 | session.close() 46 | 47 | 48 | def main(): 49 | print("Starting migration") 50 | 51 | with session_scope(): 52 | try: 53 | connection.execute("ALTER TABLE Request ADD COLUMN status_code INT") 54 | except Exception as e: 55 | print("Column already exists: {}".format(e)) 56 | 57 | print("Finished.") 58 | 59 | 60 | if __name__ == "__main__": 61 | main() 62 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | # install all requirements from requirements.txt 2 | -r requirements.txt 3 | 4 | # ... and install more 5 | codecov 6 | pytest 7 | pytest-factoryboy 8 | pytest-cov 9 | flake8 10 | -------------------------------------------------------------------------------- /requirements-micro.txt: -------------------------------------------------------------------------------- 1 | click 2 | apscheduler 3 | flask>=1.0.0 # for monitoring the web-service 4 | sqlalchemy>=1.1.9 # for database support 5 | configparser # for parsing the config-file 6 | psutil # for logging extra CPU-info 7 | colorhash # for hashing a string into a color 8 | numpy # for computing median and other stats 9 | pytz # for timezone info 10 | requests==2.32.0 # for telemetry data posting 11 | tzlocal==2.0 # for figuring out the local timezone; frozen to 2.0 because 3.0 conflicts the latest version of appscheduler (as of jan.2020) 12 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -r requirements-micro.txt 2 | scipy # for statistical analysis of latency measurements 3 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flask-dashboard/Flask-MonitoringDashboard/3cebbd62a8fd248313eeb630375d5dd07a6ff572/tests/__init__.py -------------------------------------------------------------------------------- /tests/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flask-dashboard/Flask-MonitoringDashboard/3cebbd62a8fd248313eeb630375d5dd07a6ff572/tests/api/__init__.py -------------------------------------------------------------------------------- /tests/api/test_custom.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | 3 | from flask_monitoringdashboard.database import CustomGraph 4 | 5 | 6 | def test_custom_graphs(dashboard_user, custom_graph, session): 7 | response = dashboard_user.get('dashboard/api/custom_graphs') 8 | assert response.status_code == 200 9 | 10 | data = response.json 11 | assert len(data) == session.query(CustomGraph).count() 12 | 13 | [data_custom_graph] = [graph for graph in data if graph['graph_id'] == str(custom_graph.graph_id)] 14 | assert data_custom_graph['title'] == custom_graph.title 15 | assert data_custom_graph['time_added'] == str(custom_graph.time_added) 16 | assert data_custom_graph['version_added'] == custom_graph.version_added 17 | 18 | 19 | def test_custom_graph_data(dashboard_user, custom_graph, custom_graph_data): 20 | today = datetime.utcnow() 21 | yesterday = today - timedelta(days=1) 22 | response = dashboard_user.get('dashboard/api/custom_graph/{id}/{start}/{end}'.format( 23 | id=custom_graph.graph_id, 24 | start=yesterday.strftime('%Y-%m-%d'), 25 | end=today.strftime('%Y-%m-%d'), 26 | )) 27 | assert response.status_code == 200 28 | [data] = response.json 29 | assert data['graph_id'] == str(custom_graph.graph_id) 30 | assert data['id'] == str(custom_graph_data.id) 31 | assert data['time'] == str(custom_graph_data.time) 32 | assert data['value'] == str(custom_graph_data.value) 33 | -------------------------------------------------------------------------------- /tests/api/test_deployment.py: -------------------------------------------------------------------------------- 1 | from flask_monitoringdashboard.database import Request 2 | 3 | 4 | def test_deployment(dashboard_user, session, config): 5 | response = dashboard_user.get('dashboard/api/deploy_details') 6 | assert response.status_code == 200 7 | 8 | data = response.json 9 | assert data['config-version'] == config.version 10 | assert data['link'] == 'dashboard' 11 | assert data['total-requests'] == session.query(Request).count() 12 | 13 | 14 | def test_deployment_config(dashboard_user, config): 15 | response = dashboard_user.get('dashboard/api/deploy_config') 16 | assert response.status_code == 200 17 | 18 | data = response.json 19 | assert data['database_name'] == config.database_name 20 | assert data['outlier_detection_constant'] == config.outlier_detection_constant 21 | assert data['timezone'] == str(config.timezone) 22 | assert data['colors'] == config.colors 23 | -------------------------------------------------------------------------------- /tests/api/test_outlier.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.usefixtures('outlier_1', 'outlier_2') 5 | def test_num_outliers(dashboard_user, endpoint): 6 | response = dashboard_user.get('dashboard/api/num_outliers/{0}'.format(endpoint.id)) 7 | assert response.status_code == 200 8 | 9 | data = response.json 10 | assert data == 2 11 | 12 | 13 | @pytest.mark.parametrize('outlier_1__cpu_percent', ['[0, 1, 2, 3]']) 14 | @pytest.mark.usefixtures('outlier_1') 15 | def test_outlier_graph(dashboard_user, endpoint): 16 | response = dashboard_user.get('dashboard/api/outlier_graph/{0}'.format(endpoint.id)) 17 | assert response.status_code == 200 18 | 19 | data = response.json 20 | for i in range(4): 21 | assert data[i]['name'] == "CPU core {0}".format(i) 22 | assert data[i]['values'] == [i] 23 | 24 | 25 | @pytest.mark.parametrize('outlier_1__cpu_percent', ['[0, 1, 2, 3]']) 26 | @pytest.mark.parametrize('outlier_1__memory', ['memory']) 27 | @pytest.mark.parametrize('outlier_1__request_environment', ['request_environment']) 28 | @pytest.mark.parametrize('outlier_1__request_header', ['request_header']) 29 | @pytest.mark.parametrize('outlier_1__request_url', ['request_url']) 30 | @pytest.mark.parametrize('outlier_1__stacktrace', ['stacktrace']) 31 | @pytest.mark.parametrize('offset,per_page', [[0, 10]]) 32 | def test_outlier_table(dashboard_user, outlier_1, endpoint, offset, per_page): 33 | response = dashboard_user.get( 34 | 'dashboard/api/outlier_table/{0}/{1}/{2}'.format(endpoint.id, offset, per_page), 35 | ) 36 | assert response.status_code == 200 37 | 38 | [data] = response.json 39 | assert data['cpu_percent'] == outlier_1.cpu_percent 40 | assert data['id'] == str(outlier_1.id) 41 | assert data['memory'] == outlier_1.memory 42 | assert data['request_id'] == str(outlier_1.request.id) 43 | assert data['request_environment'] == outlier_1.request_environment 44 | assert data['request_header'] == outlier_1.request_header 45 | assert data['request_url'] == outlier_1.request_url 46 | assert data['stacktrace'] == outlier_1.stacktrace 47 | -------------------------------------------------------------------------------- /tests/api/test_profiler.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from flask_monitoringdashboard.core.timezone import to_local_datetime 4 | 5 | 6 | @pytest.mark.usefixtures('stack_line', 'stack_line_2') 7 | def test_num_profiled(dashboard_user, endpoint): 8 | response = dashboard_user.get('dashboard/api/num_profiled/{0}'.format(endpoint.id)) 9 | assert response.status_code == 200 10 | 11 | data = response.json 12 | assert data == 2 13 | 14 | 15 | @pytest.mark.parametrize('request_1__group_by', ['group_by']) 16 | @pytest.mark.parametrize('offset,per_page', [[0, 10]]) 17 | def test_profiler_table(dashboard_user, stack_line, request_1, endpoint, offset, per_page): 18 | response = dashboard_user.get( 19 | 'dashboard/api/profiler_table/{0}/{1}/{2}'.format(endpoint.id, offset, per_page), 20 | ) 21 | assert response.status_code == 200 22 | 23 | [data] = response.json 24 | assert data['duration'] == str(request_1.duration) 25 | assert data['endpoint_id'] == str(endpoint.id) 26 | assert data['group_by'] == request_1.group_by 27 | assert data['id'] == str(request_1.id) 28 | assert data['ip'] == request_1.ip 29 | assert data['status_code'] == str(request_1.status_code) 30 | assert data['time_requested'] == str(to_local_datetime(request_1.time_requested)) 31 | assert data['version_requested'] == request_1.version_requested 32 | 33 | assert len(data['stack_lines']) == 1 34 | assert data['stack_lines'][0]['code']['code'] == stack_line.code.code 35 | assert data['stack_lines'][0]['code']['filename'] == stack_line.code.filename 36 | assert data['stack_lines'][0]['code']['function_name'] == stack_line.code.function_name 37 | assert data['stack_lines'][0]['code']['line_number'] == str(stack_line.code.line_number) 38 | 39 | 40 | def test_grouped_profiler(dashboard_user, stack_line, endpoint): 41 | response = dashboard_user.get('dashboard/api/grouped_profiler/{0}'.format(endpoint.id)) 42 | assert response.status_code == 200 43 | 44 | [data] = response.json 45 | assert data['code'] == stack_line.code.code 46 | assert data['duration'] == stack_line.duration 47 | assert data['hits'] == 1 48 | assert data['indent'] == stack_line.indent 49 | assert data['std'] == 0 50 | assert data['total_hits'] == 1 51 | -------------------------------------------------------------------------------- /tests/api/test_reporting.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from datetime import datetime, timedelta 3 | import pytest 4 | 5 | from flask_monitoringdashboard.database import Endpoint 6 | 7 | 8 | def test_make_report_get(dashboard_user): 9 | """GET is not allowed. It should return the overview page.""" 10 | response = dashboard_user.get('dashboard/api/reporting/make_report/intervals') 11 | assert not response.is_json 12 | 13 | 14 | @pytest.mark.parametrize('request_1__time_requested', [datetime.utcnow() - timedelta(hours=6)]) 15 | @pytest.mark.parametrize('request_1__duration', [5000]) 16 | @pytest.mark.parametrize('request_1__status_code', [500]) 17 | @pytest.mark.parametrize('request_2__time_requested', [datetime.utcnow() - timedelta(days=1, hours=6)]) 18 | @pytest.mark.parametrize('request_2__duration', [100]) 19 | @pytest.mark.skipif(sys.version_info < (3, ), reason="For some reason, this doesn't work in python 2.7.") 20 | def test_make_report_post_not_significant(dashboard_user, endpoint, request_1, request_2, session): 21 | epoch = datetime(1970, 1, 1) 22 | response = dashboard_user.post( 23 | 'dashboard/api/reporting/make_report/intervals', 24 | json={ 25 | 'interval': { 26 | 'from': (datetime.utcnow() - timedelta(days=1) - epoch).total_seconds(), 27 | 'to': (datetime.utcnow() - epoch).total_seconds(), 28 | }, 29 | 'baseline_interval': { 30 | 'from': (datetime.utcnow() - timedelta(days=2) - epoch).total_seconds(), 31 | 'to': (datetime.utcnow() - timedelta(days=1) - epoch).total_seconds(), 32 | }, 33 | }, 34 | ) 35 | assert response.status_code == 200 36 | 37 | assert len(response.json['summaries']) == session.query(Endpoint).count() 38 | [data] = [row for row in response.json['summaries'] if row['endpoint_id'] == endpoint.id] 39 | 40 | assert data['endpoint_id'] == endpoint.id 41 | assert data['endpoint_name'] == endpoint.name 42 | assert not data['has_anything_significant'] 43 | 44 | question1, question2 = data['answers'] 45 | assert question1['type'] == 'MEDIAN_LATENCY' 46 | assert question1['percentual_diff'] == 4900 47 | assert question1['median'] == request_1.duration 48 | assert question1['latencies_samples'] == {'baseline': [request_2.duration], 'comparison': [request_1.duration]} 49 | assert not question1['is_significant'] 50 | assert question1['baseline_median'] == request_2.duration 51 | 52 | assert question2['type'] == 'STATUS_CODE_DISTRIBUTION' 53 | assert not question2['is_significant'] 54 | assert question2['percentages'] is None 55 | 56 | 57 | def test_make_report_post_is_significant(dashboard_user, endpoint, request_1, request_2, session): 58 | """TODO: implement this test.""" 59 | -------------------------------------------------------------------------------- /tests/api/test_request.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize('request_1__time_requested', [datetime(2020, 1, 1)]) 7 | @pytest.mark.parametrize('request_2__time_requested', [datetime(2020, 1, 2)]) 8 | @pytest.mark.usefixtures('request_1', 'request_2') 9 | def test_num_requests(dashboard_user, endpoint): 10 | response = dashboard_user.get('dashboard/api/requests/2020-01-01/2020-01-02') 11 | 12 | assert response.status_code == 200 13 | 14 | assert response.json['days'] == ['2020-01-01', '2020-01-02'] 15 | [data] = [row for row in response.json['data'] if row['name'] == endpoint.name] 16 | assert data['values'] == [1, 1] 17 | 18 | 19 | @pytest.mark.parametrize('request_1__time_requested', [datetime(2020, 1, 1, hour=2)]) 20 | @pytest.mark.parametrize('request_2__time_requested', [datetime(2020, 1, 1, hour=3)]) 21 | @pytest.mark.usefixtures('request_1', 'request_2') 22 | def test_hourly_load(dashboard_user, endpoint): 23 | response = dashboard_user.get('dashboard/api/hourly_load/2020-01-01/2020-01-01/{0}'.format(endpoint.id)) 24 | 25 | assert response.status_code == 200 26 | 27 | data = response.json 28 | assert data['days'] == ['2020-01-01'] 29 | for index, row in enumerate(data['data']): 30 | if index in [2, 3]: 31 | assert row == [1] 32 | else: 33 | assert row == [0] 34 | 35 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Import all fixtures here.""" 2 | 3 | from tests.fixtures.dashboard import * # noqa 4 | from tests.fixtures.database import * # noqa 5 | from tests.fixtures.models import * # noqa 6 | -------------------------------------------------------------------------------- /tests/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flask-dashboard/Flask-MonitoringDashboard/3cebbd62a8fd248313eeb630375d5dd07a6ff572/tests/fixtures/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/dashboard.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import pytz 3 | from flask import Flask 4 | 5 | import flask_monitoringdashboard 6 | 7 | 8 | @pytest.fixture 9 | def config(colors=None, group_by=None): 10 | flask_monitoringdashboard.config.colors = colors or {'endpoint': '[0, 1, 2]'} 11 | flask_monitoringdashboard.config.group_by = group_by 12 | flask_monitoringdashboard.config.timezone = pytz.timezone('UTC') 13 | 14 | return flask_monitoringdashboard.config 15 | 16 | 17 | @pytest.fixture 18 | def view_func(): 19 | return 'test' 20 | 21 | 22 | @pytest.fixture 23 | def dashboard(config, endpoint, view_func, rule='/'): 24 | print("inside dashboard...") 25 | app = Flask(__name__) 26 | app.config['DEBUG'] = True 27 | app.config['TESTING'] = True 28 | 29 | app.add_url_rule(rule, endpoint=endpoint.name, view_func=lambda: view_func) 30 | flask_monitoringdashboard.bind(app, schedule=False) 31 | 32 | 33 | with app.test_client() as client: 34 | yield client 35 | 36 | 37 | @pytest.fixture 38 | def dashboard_user(dashboard, user, config): 39 | """ 40 | Returns a testing application that can be used for testing the endpoints. 41 | """ 42 | dashboard.post('dashboard/login', data={'name': user.username, 'password': user.password}) 43 | yield dashboard 44 | 45 | dashboard.post('dashboard/logout') 46 | 47 | 48 | @pytest.fixture 49 | def request_context(dashboard): 50 | with dashboard.application.test_request_context(): 51 | yield 52 | -------------------------------------------------------------------------------- /tests/fixtures/database.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from factory.alchemy import SQLAlchemyModelFactory 3 | from sqlalchemy.orm import scoped_session 4 | 5 | from flask_monitoringdashboard.database import DBSession 6 | 7 | 8 | @pytest.fixture 9 | def session(): 10 | """Db session.""" 11 | session = scoped_session(DBSession) 12 | yield session 13 | session.close() 14 | 15 | 16 | class ModelFactory(SQLAlchemyModelFactory): 17 | """Base model factory.""" 18 | 19 | class Meta: 20 | abstract = True 21 | sqlalchemy_session = scoped_session(DBSession) 22 | sqlalchemy_session_persistence = 'commit' 23 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flask-dashboard/Flask-MonitoringDashboard/3cebbd62a8fd248313eeb630375d5dd07a6ff572/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flask-dashboard/Flask-MonitoringDashboard/3cebbd62a8fd248313eeb630375d5dd07a6ff572/tests/unit/core/__init__.py -------------------------------------------------------------------------------- /tests/unit/core/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flask-dashboard/Flask-MonitoringDashboard/3cebbd62a8fd248313eeb630375d5dd07a6ff572/tests/unit/core/config/__init__.py -------------------------------------------------------------------------------- /tests/unit/core/config/test_config.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | import os 3 | 4 | from flask_monitoringdashboard.core.config.parser import ( 5 | get_environment_var, 6 | parse_literal, 7 | parse_bool, 8 | parse_string, 9 | parse_version, 10 | ) 11 | 12 | 13 | def test_init_from(config): 14 | """Test whether the group_by returns the right result.""" 15 | 16 | config.init_from() 17 | config.init_from(file='../../config.cfg') 18 | 19 | 20 | def test_parser(): 21 | """Test whether the parser reads the right values.""" 22 | 23 | parser = configparser.RawConfigParser() 24 | version = '1.2.3' 25 | string = 'string-value' 26 | bool = 'False' 27 | literal = "['a', 'b', 'c']" 28 | literal2 = '1.23' 29 | section = 'dashboard' 30 | 31 | parser.add_section(section) 32 | parser.set(section, 'APP_VERSION', version) 33 | parser.set(section, 'string', string) 34 | parser.set(section, 'bool', bool) 35 | parser.set(section, 'literal', literal) 36 | parser.set(section, 'literal2', literal2) 37 | 38 | assert parse_version(parser, section, 'default') == version 39 | assert parse_string(parser, section, 'string', 'default') == string 40 | assert not parse_bool(parser, section, 'bool', 'True') 41 | assert parse_literal(parser, section, 'literal', 'default') == ['a', 'b', 'c'] 42 | assert parse_literal(parser, section, 'literal2', 'default') == 1.23 43 | 44 | -------------------------------------------------------------------------------- /tests/unit/core/profiler/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flask-dashboard/Flask-MonitoringDashboard/3cebbd62a8fd248313eeb630375d5dd07a6ff572/tests/unit/core/profiler/__init__.py -------------------------------------------------------------------------------- /tests/unit/core/profiler/test_profiler.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | 4 | from flask import request 5 | import pytest 6 | from werkzeug.routing import Rule 7 | 8 | from flask_monitoringdashboard.core.cache import memory_cache, init_cache 9 | from flask_monitoringdashboard.core.profiler import ( 10 | start_thread_last_requested, 11 | start_performance_thread, 12 | start_profiler_and_outlier_thread, 13 | start_outlier_thread, 14 | ) 15 | 16 | 17 | def wait_until_threads_finished(num_threads): 18 | while threading.active_count() > num_threads: 19 | time.sleep(0.01) 20 | 21 | 22 | @pytest.mark.usefixtures("request_context") 23 | def test_start_thread_last_requested(endpoint, config): 24 | config.app.url_map.add(Rule("/", endpoint=endpoint.name)) 25 | init_cache() 26 | num_threads = threading.active_count() 27 | start_thread_last_requested(endpoint) 28 | wait_until_threads_finished(num_threads) 29 | 30 | assert memory_cache.get(endpoint.name).last_requested 31 | 32 | 33 | @pytest.mark.usefixtures("request_context") 34 | def test_start_performance_thread(endpoint, config): 35 | config.app.url_map.add(Rule("/", endpoint=endpoint.name)) 36 | init_cache() 37 | request.environ["REMOTE_ADDR"] = "127.0.0.1" 38 | num_threads = threading.active_count() 39 | start_performance_thread(endpoint, 1234, 200, None) 40 | assert threading.active_count() == num_threads + 1 41 | wait_until_threads_finished(num_threads) 42 | 43 | assert memory_cache.get(endpoint.name).average_duration > 0 44 | 45 | 46 | @pytest.mark.usefixtures("request_context") 47 | def test_start_outlier_thread(endpoint, config): 48 | config.app.url_map.add(Rule("/", endpoint=endpoint.name)) 49 | init_cache() 50 | request.environ["REMOTE_ADDR"] = "127.0.0.1" 51 | num_threads = threading.active_count() 52 | outlier = start_outlier_thread(endpoint) 53 | assert threading.active_count() == num_threads + 1 54 | outlier.stop(duration=1, status_code=200, e_collector=None) 55 | wait_until_threads_finished(num_threads) 56 | 57 | 58 | @pytest.mark.usefixtures("request_context") 59 | def test_start_profiler_and_outlier_thread(endpoint, config): 60 | config.app.url_map.add(Rule("/", endpoint=endpoint.name)) 61 | init_cache() 62 | request.environ["REMOTE_ADDR"] = "127.0.0.1" 63 | num_threads = threading.active_count() 64 | thread = start_profiler_and_outlier_thread(endpoint) 65 | assert threading.active_count() == num_threads + 2 66 | thread.stop(duration=1, status_code=200, e_collector=None) 67 | wait_until_threads_finished(num_threads) 68 | -------------------------------------------------------------------------------- /tests/unit/core/profiler/test_stacktrace_profiler.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import pytest 3 | 4 | from flask import request 5 | from werkzeug.routing import Rule 6 | 7 | from flask_monitoringdashboard.core.cache import init_cache 8 | from flask_monitoringdashboard.core.profiler import StacktraceProfiler 9 | 10 | 11 | @pytest.mark.usefixtures('request_context') 12 | def test_after_run(endpoint, config): 13 | config.app.url_map.add(Rule('/', endpoint=endpoint.name)) 14 | init_cache() 15 | request.environ['REMOTE_ADDR'] = '127.0.0.1' 16 | current_thread = threading.current_thread().ident 17 | ip = request.environ['REMOTE_ADDR'] 18 | thread = StacktraceProfiler(current_thread, endpoint, ip, group_by=None) 19 | thread._keeprunning = False 20 | thread.run() 21 | -------------------------------------------------------------------------------- /tests/unit/core/profiler/util/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flask-dashboard/Flask-MonitoringDashboard/3cebbd62a8fd248313eeb630375d5dd07a6ff572/tests/unit/core/profiler/util/__init__.py -------------------------------------------------------------------------------- /tests/unit/core/profiler/util/test_grouped_stack_line.py: -------------------------------------------------------------------------------- 1 | from math import sqrt 2 | 3 | 4 | def test_grouped_stack_line(grouped_stack_line): 5 | assert grouped_stack_line.hits == 3 6 | assert grouped_stack_line.sum == 60 7 | assert grouped_stack_line.standard_deviation == sqrt(200) 8 | assert grouped_stack_line.hits_percentage == 0.5 9 | assert grouped_stack_line.percentage == 0.6 10 | assert grouped_stack_line.average == 20 11 | -------------------------------------------------------------------------------- /tests/unit/core/profiler/util/test_path_hash.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture 5 | def filename(): 6 | return 'filename0' 7 | 8 | 9 | @pytest.fixture 10 | def line_number(): 11 | return 42 12 | 13 | 14 | def test_get_path(path_hash, filename, line_number): 15 | assert path_hash.get_path(filename, line_number) == '0:42' 16 | 17 | 18 | def test_append(path_hash, filename, line_number): 19 | path_hash.get_path(filename, line_number) 20 | assert path_hash.append(filename, line_number) == '0:42->0:42' 21 | 22 | 23 | def test_encode(path_hash, filename, line_number): 24 | assert path_hash._encode(filename, line_number) == '0:42' 25 | 26 | 27 | def test_decode(path_hash, filename, line_number): 28 | assert path_hash._decode(path_hash._encode(filename, line_number)) == (filename, line_number) 29 | 30 | 31 | def test_get_indent(path_hash, filename, line_number): 32 | assert path_hash.get_indent('') == 0 33 | assert path_hash.get_indent('0:42') == 1 34 | assert path_hash.get_indent('0:42->0:42') == 2 35 | 36 | 37 | def test_get_last_fn_ln(path_hash, filename, line_number): 38 | assert path_hash.get_last_fn_ln(path_hash._encode(filename, line_number)) == (filename, line_number) 39 | 40 | 41 | def test_get_code(path_hash, stack_line, filename, line_number): 42 | path = path_hash.get_stacklines_path([stack_line], 0) 43 | assert path_hash.get_code(path) == stack_line.code.code 44 | 45 | 46 | def test_get_stacklines_path(path_hash, stack_line, stack_line_2, filename, line_number): 47 | assert path_hash.get_stacklines_path([stack_line, stack_line_2], 0) == '1:0' 48 | assert path_hash.get_stacklines_path([stack_line, stack_line_2], 1) == '1:0->1:0' 49 | -------------------------------------------------------------------------------- /tests/unit/core/profiler/util/test_string_hash.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_stringhash(string_hash): 5 | assert string_hash.hash('abc') == 0 6 | assert string_hash.hash('def') == 1 7 | assert string_hash.hash('abc') == 0 8 | 9 | 10 | def test_unhash(string_hash): 11 | assert string_hash.unhash(string_hash.hash('abc')) == 'abc' 12 | 13 | with pytest.raises(ValueError): 14 | string_hash.unhash('unkown') 15 | -------------------------------------------------------------------------------- /tests/unit/core/profiler/util/test_util.py: -------------------------------------------------------------------------------- 1 | from flask_monitoringdashboard.core.profiler.util import order_histogram 2 | 3 | 4 | def test_order_histogram(): 5 | histogram = {('0:42->1:12', 'c'): 610, ('0:42', 'a'): 1234, ('0:42->1:13', 'b'): 614} 6 | assert order_histogram(histogram.items()) == ( 7 | [(('0:42', 'a'), 1234), (('0:42->1:13', 'b'), 614), (('0:42->1:12', 'c'), 610)] 8 | ) 9 | -------------------------------------------------------------------------------- /tests/unit/core/test_blueprints.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from flask_monitoringdashboard.core.blueprints import get_blueprint 4 | 5 | 6 | @pytest.mark.parametrize('name', ['Fiddler']) 7 | def test_get_blueprint(name): 8 | actual = get_blueprint(name) 9 | assert actual == 'Fiddler' 10 | 11 | 12 | @pytest.mark.parametrize('name', ['Anomander.Purake']) 13 | def test_get_blueprint_double_gives_first(name): 14 | actual = get_blueprint(name) 15 | assert actual == 'Anomander' 16 | 17 | 18 | @pytest.mark.parametrize('name', ['Karsa.Orlong.Toblakai']) 19 | def test_get_blueprint_triple_gives_first(name): 20 | actual = get_blueprint(name) 21 | assert actual == 'Karsa' 22 | 23 | 24 | @pytest.mark.usefixtures('request_context') 25 | @pytest.mark.parametrize('name', ['']) 26 | def test_get_blueprint_blank_gives_blank(name): 27 | actual = get_blueprint(name) 28 | assert actual == '' 29 | -------------------------------------------------------------------------------- /tests/unit/core/test_colors.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from flask_monitoringdashboard.core.colors import get_color 4 | 5 | 6 | @pytest.mark.usefixtures('request_context') 7 | def test_get_color(): 8 | assert get_color('endpoint') == 'rgb(0, 1, 2)' 9 | assert get_color('main') in ['rgb(140, 191, 64)', 'rgb(140.0, 191.0, 64.0)'] 10 | -------------------------------------------------------------------------------- /tests/unit/core/test_group_by.py: -------------------------------------------------------------------------------- 1 | from flask_monitoringdashboard.core.group_by import get_group_by, recursive_group_by 2 | 3 | 4 | def test_get_group_by_function(config): 5 | """Test whether the group_by returns the right result.""" 6 | config.group_by = lambda: 3 7 | assert get_group_by() == '3' 8 | 9 | 10 | def test_get_group_by_tuple(config): 11 | config.group_by = (lambda: 'User', lambda: 3.0) 12 | assert get_group_by() == '(User,3.0)' 13 | 14 | 15 | def test_get_group_by_function_in_function(config): 16 | config.group_by = lambda: lambda: '1234' 17 | assert get_group_by() == '1234' 18 | 19 | 20 | def test_recursive_group_by(): 21 | class Object(object): 22 | def __str__(self): 23 | return 'object' 24 | 25 | assert recursive_group_by(Object()) == 'object' 26 | -------------------------------------------------------------------------------- /tests/unit/core/test_measurement.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from flask_monitoringdashboard.core.measurement import init_measurement, add_decorator 4 | 5 | 6 | @pytest.mark.usefixtures('request_context') 7 | def test_init_measurement(): 8 | init_measurement() 9 | 10 | 11 | @pytest.mark.usefixtures('request_context') 12 | @pytest.mark.parametrize('endpoint__monitor_level', [0, 1, 2, 3]) 13 | def test_add_decorator(endpoint, config): 14 | def f(): 15 | pass 16 | 17 | config.app.view_functions[endpoint.name] = f 18 | add_decorator(endpoint) 19 | assert config.app.view_functions[endpoint.name].original == f 20 | 21 | 22 | @pytest.mark.usefixtures('request_context') 23 | @pytest.mark.parametrize('endpoint__monitor_level', [-1]) 24 | def test_add_decorator_fails(endpoint, config): 25 | config.app.view_functions[endpoint.name] = lambda: 42 26 | with pytest.raises(ValueError): 27 | endpoint.monitor_level = -1 28 | add_decorator(endpoint) 29 | -------------------------------------------------------------------------------- /tests/unit/core/test_rules.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from flask_monitoringdashboard.core.rules import get_rules 3 | 4 | 5 | @pytest.mark.usefixtures('request_context') 6 | def test_rules(endpoint): 7 | assert len(get_rules()) == 1 8 | assert len(get_rules(endpoint.name)) == 1 9 | assert get_rules('unknown') == [] 10 | -------------------------------------------------------------------------------- /tests/unit/core/test_timezone.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from flask_monitoringdashboard.core.timezone import to_local_datetime, to_utc_datetime 4 | 5 | 6 | def test_timezone(): 7 | dt = datetime.datetime.now() 8 | assert to_local_datetime(to_utc_datetime(dt)) == dt 9 | assert to_utc_datetime(to_local_datetime(dt)) == dt 10 | 11 | 12 | def test_timezone_none(): 13 | assert to_local_datetime(None) is None 14 | assert to_utc_datetime(None) is None 15 | -------------------------------------------------------------------------------- /tests/unit/database/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flask-dashboard/Flask-MonitoringDashboard/3cebbd62a8fd248313eeb630375d5dd07a6ff572/tests/unit/database/__init__.py -------------------------------------------------------------------------------- /tests/unit/database/test_auth.py: -------------------------------------------------------------------------------- 1 | from flask_monitoringdashboard.database import User 2 | from flask_monitoringdashboard.database.auth import get_user 3 | 4 | 5 | def test_get_user_adds_default(session, config): 6 | session.query(User).delete() # delete all existing users 7 | session.commit() 8 | new_user = get_user(config.username, config.password) 9 | 10 | assert session.query(User).count() == 1 11 | 12 | assert new_user.username == config.username 13 | assert new_user.check_password(config.password) 14 | 15 | 16 | def test_get_user_returns_none(user): 17 | """Test that get_user returns None if the user cannot be found.""" 18 | assert get_user(username=user.username, password='1234') is None 19 | -------------------------------------------------------------------------------- /tests/unit/database/test_codeline.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains all unit tests that count a number of results in the database. 3 | (Corresponding to the file: 'flask_monitoringdashboard/database/count.py') 4 | """ 5 | import pytest 6 | 7 | from flask_monitoringdashboard.database.code_line import get_code_line 8 | 9 | 10 | @pytest.mark.parametrize('filename', ['filename']) 11 | @pytest.mark.parametrize('line_number', [42]) 12 | @pytest.mark.parametrize('function', ['f']) 13 | @pytest.mark.parametrize('code', ['x = 5']) 14 | def test_get_code_line(session, filename, line_number, function, code): 15 | code_line1 = get_code_line(session, filename, line_number, function, code) 16 | code_line2 = get_code_line(session, filename, line_number, function, code) 17 | assert code_line1.id == code_line2.id 18 | assert code_line1.function_name == code_line2.function_name 19 | assert code_line1.filename == code_line2.filename 20 | assert code_line1.line_number == code_line2.line_number 21 | assert code_line1.code == code_line2.code 22 | -------------------------------------------------------------------------------- /tests/unit/database/test_count.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains all pytests that count a number of results in the database. 3 | (Corresponding to the file: 'flask_monitoringdashboard/database/count.py') 4 | """ 5 | import pytest 6 | 7 | from flask_monitoringdashboard.database import Endpoint, Request 8 | from flask_monitoringdashboard.database.count import ( 9 | count_requests, 10 | count_total_requests, 11 | count_outliers, 12 | count_profiled_requests, 13 | ) 14 | 15 | 16 | @pytest.fixture 17 | def non_existing_endpoint_id(session): 18 | return session.query(Endpoint).count() + 1 19 | 20 | 21 | @pytest.mark.usefixtures('request_1') 22 | def test_count_requests(session, endpoint, non_existing_endpoint_id): 23 | assert count_requests(session, endpoint.id) == 1 24 | assert count_requests(session, non_existing_endpoint_id) == 0 25 | 26 | 27 | def test_count_total_requests(session): 28 | assert count_total_requests(session) == session.query(Request).count() 29 | 30 | 31 | @pytest.mark.usefixtures('outlier_1') 32 | def test_count_outliers(session, endpoint, non_existing_endpoint_id): 33 | assert count_outliers(session, endpoint.id) == 1 34 | assert count_outliers(session, non_existing_endpoint_id) == 0 35 | 36 | 37 | def test_count_profiled_requests(session, endpoint): 38 | assert count_profiled_requests(session, endpoint.id) == 0 39 | -------------------------------------------------------------------------------- /tests/unit/database/test_count_group.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from random import randint 3 | 4 | import pytest 5 | 6 | from flask_monitoringdashboard.database import Request 7 | from flask_monitoringdashboard.database.count_group import ( 8 | count_requests_group, 9 | count_requests_per_day, 10 | ) 11 | 12 | 13 | def test_count_requests_group(session, request_1, endpoint): 14 | assert count_requests_group(session, Request.version_requested == request_1.version_requested) == [(endpoint.id, 1)] 15 | 16 | 17 | @pytest.mark.parametrize('request_1__time_requested', [datetime(1970 + randint(0, 1000), 1, 2)]) 18 | def test_count_requests_per_day(session, request_1, endpoint): 19 | assert count_requests_per_day(session, []) == [] 20 | 21 | assert count_requests_per_day(session, [request_1.time_requested.date()]) == [[(endpoint.id, 1)]] 22 | -------------------------------------------------------------------------------- /tests/unit/database/test_data_grouped.py: -------------------------------------------------------------------------------- 1 | from flask_monitoringdashboard.database.data_grouped import ( 2 | get_endpoint_data_grouped, 3 | get_version_data_grouped, 4 | ) 5 | 6 | 7 | def test_get_endpoint_data_grouped(session, request_1): 8 | data = get_endpoint_data_grouped(session, lambda x: x) 9 | for key, value in data: 10 | if key == request_1.endpoint_id: 11 | assert value == [request_1.duration] 12 | return 13 | assert False, "Shouldn't reach here." 14 | 15 | 16 | def test_get_version_data_grouped(session, request_1): 17 | data = get_version_data_grouped(session, lambda x: x) 18 | for key, value in data: 19 | if key == request_1.version_requested: 20 | assert value == [request_1.duration] 21 | return 22 | assert False, "Shouldn't reach here." 23 | -------------------------------------------------------------------------------- /tests/unit/database/test_endpoint.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains all unit tests for the endpoint-table in the database. (Corresponding to the 3 | file: 'flask_monitoringdashboard/database/endpoint.py') 4 | """ 5 | from datetime import datetime 6 | 7 | import pytest 8 | 9 | from flask_monitoringdashboard.database import Endpoint 10 | from flask_monitoringdashboard.database.count_group import get_value 11 | from flask_monitoringdashboard.database.endpoint import get_endpoint_by_name, update_endpoint, update_last_requested, \ 12 | get_last_requested, get_endpoints 13 | 14 | 15 | def test_get_endpoint(session, endpoint): 16 | endpoint2 = get_endpoint_by_name(session, endpoint.name) 17 | assert endpoint.name == endpoint2.name 18 | assert endpoint.id == endpoint2.id 19 | 20 | 21 | @pytest.mark.parametrize('endpoint__monitor_level', [1]) 22 | def test_update_endpoint(session, endpoint): 23 | update_endpoint(session, endpoint.name, 2) 24 | assert get_endpoint_by_name(session, endpoint.name).monitor_level == 2 25 | 26 | 27 | @pytest.mark.parametrize('timestamp', [datetime(2020, 2, 2), datetime(2020, 3, 3)]) 28 | def test_update_last_accessed(session, endpoint, timestamp): 29 | update_last_requested(session, endpoint.name, timestamp=timestamp) 30 | result = get_value(get_last_requested(session), endpoint.name) 31 | assert result == timestamp 32 | 33 | 34 | def test_endpoints(session, endpoint): 35 | endpoints = get_endpoints(session) 36 | assert endpoints.count() == session.query(Endpoint).count() 37 | assert [endpoint.id == e.id for e in endpoints] # check that the endpoint is included. 38 | -------------------------------------------------------------------------------- /tests/unit/database/test_exception_message.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains all unit tests of exception message in the database. 3 | (Corresponding to the file: 'flask_monitoringdashboard/database/exception_message.py') 4 | """ 5 | 6 | import pytest 7 | from flask_monitoringdashboard.database import ExceptionMessage 8 | from flask_monitoringdashboard.database.exception_message import add_exception_message 9 | 10 | 11 | @pytest.mark.parametrize("message", ["some message"]) 12 | def test_add_exception_message(session, message): 13 | exception_message1_id = add_exception_message(session, message) 14 | exception_message_count = session.query(ExceptionMessage).count() 15 | exception_message2_id = add_exception_message(session, message) 16 | assert exception_message1_id == exception_message2_id 17 | assert exception_message_count == session.query(ExceptionMessage).count() 18 | -------------------------------------------------------------------------------- /tests/unit/database/test_exception_occurrence.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains all unit tests of exception info in the database. 3 | (Corresponding to the file: 'flask_monitoringdashboard/database/exception_occurrence.py') 4 | """ 5 | 6 | from flask_monitoringdashboard.database import ExceptionOccurrence 7 | from flask_monitoringdashboard.database.exception_occurrence import ( 8 | add_exception_occurrence, 9 | count_grouped_exceptions, 10 | count_endpoint_grouped_exceptions, 11 | get_exceptions_with_timestamps, 12 | delete_exception_group, 13 | get_exceptions_with_timestamps_and_stack_trace_id, 14 | ) 15 | 16 | 17 | def test_count_grouped_exceptions(session, request_1): 18 | """ 19 | Test the count_grouped_exceptions function to ensure it correctly counts grouped exceptions. 20 | They should be grouped by stack_trace_snapshot_id 21 | """ 22 | stack_trace_snapshot_id = 100 23 | delete_exception_group(session, stack_trace_snapshot_id) 24 | 25 | count_grouped_before = count_grouped_exceptions(session) 26 | count_occurences_before = session.query(ExceptionOccurrence).count() 27 | 28 | exception_occurrence_1 = ExceptionOccurrence( 29 | request_id=request_1.id, 30 | exception_type_id=2, 31 | exception_msg_id=3, 32 | stack_trace_snapshot_id=stack_trace_snapshot_id, 33 | is_user_captured=True, 34 | ) 35 | exception_occurrence_2 = ExceptionOccurrence( 36 | request_id=request_1.id, 37 | exception_type_id=2, 38 | exception_msg_id=3, 39 | stack_trace_snapshot_id=stack_trace_snapshot_id, 40 | is_user_captured=True, 41 | ) 42 | session.add(exception_occurrence_1) 43 | session.add(exception_occurrence_2) 44 | session.commit() 45 | 46 | count_grouped_after = count_grouped_exceptions(session) 47 | count_occurences_after = session.query(ExceptionOccurrence).count() 48 | 49 | assert count_grouped_before + 1 == count_grouped_after 50 | assert count_occurences_before + 2 == count_occurences_after 51 | -------------------------------------------------------------------------------- /tests/unit/database/test_exception_stack_line.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains all unit tests of exception stack line in the database. 3 | (Corresponding to the file: 'flask_monitoringdashboard/database/exception_stack_line.py') 4 | """ 5 | 6 | from flask_monitoringdashboard.database import ExceptionStackLine 7 | from flask_monitoringdashboard.database.exception_stack_line import ( 8 | add_exception_stack_line, 9 | ) 10 | 11 | 12 | def test_add_exception_stack_line( 13 | session, stack_trace_snapshot, exception_frame 14 | ): 15 | assert ( 16 | session.query(ExceptionStackLine) 17 | .filter(ExceptionStackLine.stack_trace_snapshot_id == stack_trace_snapshot.id) 18 | .one_or_none() 19 | is None 20 | ) 21 | add_exception_stack_line( 22 | session, 23 | stack_trace_snapshot_id=stack_trace_snapshot.id, 24 | exception_frame_id=exception_frame.id, 25 | position=0, 26 | ) 27 | session.commit() 28 | assert ( 29 | session.query(ExceptionStackLine) 30 | .filter(ExceptionStackLine.stack_trace_snapshot_id == stack_trace_snapshot.id) 31 | .one() 32 | ) 33 | -------------------------------------------------------------------------------- /tests/unit/database/test_exception_type.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains all unit tests of exception type in the database. 3 | (Corresponding to the file: 'flask_monitoringdashboard/database/exception_type.py') 4 | """ 5 | 6 | import pytest 7 | from flask_monitoringdashboard.database import ExceptionType 8 | from flask_monitoringdashboard.database.exception_type import add_exception_type 9 | 10 | 11 | @pytest.mark.parametrize("type", ["type"]) 12 | def test_add_exception_message(session, type): 13 | exception_type1_id = add_exception_type(session, type) 14 | exception_type_count = session.query(ExceptionType).count() 15 | exception_type2_id = add_exception_type(session, type) 16 | assert exception_type1_id == exception_type2_id 17 | assert exception_type_count == session.query(ExceptionType).count() 18 | -------------------------------------------------------------------------------- /tests/unit/database/test_function_definition.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains all unit tests of function definition in the database. 3 | (Corresponding to the file: 'flask_monitoringdashboard/database/function_definition.py') 4 | """ 5 | 6 | from flask_monitoringdashboard.database import FunctionDefinition 7 | from flask_monitoringdashboard.database.function_definition import ( 8 | add_function_definition, 9 | get_function_definition_from_id, 10 | ) 11 | 12 | 13 | def test_add_function_definition(session, function_definition): 14 | function_definition_id = add_function_definition(session, function_definition) 15 | f_def = ( 16 | session.query(FunctionDefinition) 17 | .filter(FunctionDefinition.id == function_definition_id) 18 | .one() 19 | ) 20 | assert f_def.code == function_definition.code 21 | assert f_def.code_hash == function_definition.code_hash 22 | 23 | 24 | def test_add_existing_function_definition(session, function_definition): 25 | function_definition_id = add_function_definition(session, function_definition) 26 | function_definition_count = session.query(FunctionDefinition).count() 27 | function_definition_id_2 = add_function_definition(session, function_definition) 28 | assert function_definition_count == session.query(FunctionDefinition).count() 29 | assert function_definition_id == function_definition_id_2 30 | 31 | 32 | def test_get_function_definition_from_id(session, function_definition): 33 | f_def = get_function_definition_from_id(session, function_definition.id) 34 | assert f_def.id == function_definition.id 35 | assert f_def.code == function_definition.code 36 | assert f_def.code_hash == function_definition.code_hash 37 | 38 | 39 | def test_get_function_definition_from_invalid_id(session): 40 | f_def = get_function_definition_from_id(session, -1) 41 | assert f_def is None 42 | -------------------------------------------------------------------------------- /tests/unit/database/test_outlier.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains all unit tests for the monitor-rules-table in the database. (Corresponding 3 | to the file: 'flask_monitoringdashboard/database/outlier.py') 4 | """ 5 | 6 | import pytest 7 | 8 | from flask_monitoringdashboard.database import Outlier 9 | from flask_monitoringdashboard.database.count import count_outliers 10 | from flask_monitoringdashboard.database.outlier import add_outlier, get_outliers_sorted, get_outliers_cpus 11 | 12 | 13 | def test_add_outlier(session, request_1): 14 | assert not request_1.outlier 15 | 16 | add_outlier( 17 | session, 18 | request_id=request_1.id, 19 | cpu_percent="cpu_percent", 20 | memory="memory", 21 | stacktrace="stacktrace", 22 | request=("headers", "environ", "url"), 23 | ) 24 | session.commit() 25 | assert session.query(Outlier).filter(Outlier.request_id == request_1.id).one() 26 | 27 | 28 | def test_get_outliers(session, outlier_1, endpoint): 29 | outliers = get_outliers_sorted(session, endpoint_id=endpoint.id, offset=0, per_page=10) 30 | assert len(outliers) == 1 31 | assert outliers[0].id == outlier_1.id 32 | 33 | 34 | @pytest.mark.usefixtures('outlier_1', 'outlier_2') 35 | def test_count_outliers(session, endpoint): 36 | assert count_outliers(session, endpoint.id) == 2 37 | 38 | 39 | @pytest.mark.usefixtures('outlier_1', 'outlier_2') 40 | @pytest.mark.parametrize('outlier_1__cpu_percent', ['[0, 1, 2, 3]']) 41 | @pytest.mark.parametrize('outlier_2__cpu_percent', ['[1, 2, 3, 4]']) 42 | def test_get_outliers_cpus(session, endpoint): 43 | expected_cpus = ['[{0}, {1}, {2}, {3}]'.format(i, i + 1, i + 2, i + 3) for i in range(2)] 44 | assert get_outliers_cpus(session, endpoint.id) == expected_cpus 45 | -------------------------------------------------------------------------------- /tests/unit/database/test_request.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains all unit tests for the endpoint-table in the database. (Corresponding to the 3 | file: 'flask_monitoringdashboard/database/request.py') 4 | """ 5 | from __future__ import division # can be removed once we leave python 2.7 6 | 7 | import time 8 | from datetime import datetime, timedelta 9 | 10 | import pytest 11 | 12 | from flask_monitoringdashboard.core.date_interval import DateInterval 13 | from flask_monitoringdashboard.database.count import count_requests 14 | from flask_monitoringdashboard.database.endpoint import get_avg_duration, get_endpoints 15 | from flask_monitoringdashboard.database.request import add_request, \ 16 | get_date_of_first_request, get_latencies_sample, create_time_based_sample_criterion 17 | from flask_monitoringdashboard.database.versions import get_versions 18 | 19 | 20 | def test_get_latencies_sample(session, request_1, endpoint): 21 | interval = DateInterval(datetime.utcnow() - timedelta(days=1), datetime.utcnow()) 22 | requests_criterion = create_time_based_sample_criterion(interval.start_date(), 23 | interval.end_date()) 24 | data = get_latencies_sample(session, endpoint.id, requests_criterion, sample_size=500) 25 | assert data == [request_1.duration] 26 | 27 | 28 | def test_add_request(endpoint, session): 29 | num_requests = len(endpoint.requests) 30 | add_request( 31 | session, 32 | duration=200, 33 | endpoint_id=endpoint.id, 34 | ip='127.0.0.1', 35 | group_by=None, 36 | status_code=200, 37 | ) 38 | assert count_requests(session, endpoint.id) == num_requests + 1 39 | 40 | 41 | @pytest.mark.parametrize('request_1__time_requested', [datetime(2020, 2, 3)]) 42 | def test_get_versions(session, request_1): 43 | for version, first_request in get_versions(session): 44 | if version == request_1.version_requested: 45 | assert first_request == request_1.time_requested 46 | return 47 | assert False, "Shouldn't reach here" 48 | 49 | 50 | def test_get_endpoints(session, endpoint): 51 | endpoints = get_endpoints(session) 52 | assert endpoint.name in [endpoint.name for endpoint in endpoints] 53 | 54 | 55 | @pytest.mark.parametrize('request_1__time_requested', [datetime(1970, 1, 1)]) 56 | def test_get_date_of_first_request(session, request_1): 57 | total_seconds = int(time.mktime(request_1.time_requested.timetuple())) 58 | assert get_date_of_first_request(session) == total_seconds 59 | 60 | 61 | def test_get_avg_duration(session, request_1, request_2, endpoint): 62 | assert get_avg_duration(session, endpoint.id) == (request_1.duration + request_2.duration) / 2 63 | -------------------------------------------------------------------------------- /tests/unit/database/test_stack_trace_snapshot.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file contains all unit tests of full stack trace in the database. 3 | (Corresponding to the file: 'flask_monitoringdashboard/database/stack_trace_snapshot.py') 4 | """ 5 | 6 | import pytest 7 | from flask_monitoringdashboard.database import StackTraceSnapshot 8 | from flask_monitoringdashboard.database.stack_trace_snapshot import ( 9 | get_stack_trace_by_hash, 10 | add_stack_trace_snapshot, 11 | get_stacklines_from_stack_trace_snapshot_id, 12 | ) 13 | 14 | 15 | def test_add_stack_trace_snapshot(session): 16 | stack_trace_snapshot_count = session.query(StackTraceSnapshot).count() 17 | hash = "test_hash" 18 | stack_trace_snapshot_id = add_stack_trace_snapshot( 19 | session, hash 20 | ) 21 | f_stack_trace = ( 22 | session.query(StackTraceSnapshot) 23 | .filter(StackTraceSnapshot.hash == hash) 24 | .one() 25 | ) 26 | assert stack_trace_snapshot_id == f_stack_trace.id 27 | assert stack_trace_snapshot_count + 1 == session.query(StackTraceSnapshot).count() 28 | 29 | 30 | def test_add_existing_stack_trace_snapshot(session, stack_trace_snapshot): 31 | stack_trace_snapshot_id = add_stack_trace_snapshot( 32 | session, stack_trace_snapshot.hash 33 | ) 34 | stack_trace_snapshot_count = session.query(StackTraceSnapshot).count() 35 | stack_trace_snapshot_id_2 = add_stack_trace_snapshot( 36 | session, stack_trace_snapshot.hash 37 | ) 38 | assert stack_trace_snapshot_count == session.query(StackTraceSnapshot).count() 39 | assert stack_trace_snapshot_id == stack_trace_snapshot_id_2 40 | 41 | 42 | def test_get_stack_trace_by_hash(session, stack_trace_snapshot): 43 | f_stack_trace = get_stack_trace_by_hash( 44 | session, stack_trace_snapshot.hash 45 | ) 46 | assert f_stack_trace.id == stack_trace_snapshot.id 47 | 48 | 49 | def test_get_stack_trace_by_invalid_hash(session): 50 | f_stack_trace = get_stack_trace_by_hash(session, "invalid") 51 | assert f_stack_trace is None 52 | 53 | 54 | def test_get_stacklines_from_stack_trace_snapshot_id(session, exception_stack_line): 55 | stacklines = get_stacklines_from_stack_trace_snapshot_id( 56 | session, exception_stack_line.stack_trace_snapshot_id 57 | ) 58 | assert len(stacklines) == 1 59 | assert stacklines[0].position == exception_stack_line.position 60 | assert stacklines[0].path == exception_stack_line.exception_frame.function_location.file_path.path 61 | assert stacklines[0].line_number == exception_stack_line.exception_frame.line_number 62 | assert stacklines[0].name == exception_stack_line.exception_frame.function_location.function_definition.name 63 | assert stacklines[0].function_definition_id == exception_stack_line.exception_frame.function_location.function_definition_id 64 | -------------------------------------------------------------------------------- /tests/unit/database/test_stackline.py: -------------------------------------------------------------------------------- 1 | from flask_monitoringdashboard.database.stack_line import ( 2 | add_stack_line, 3 | get_profiled_requests, 4 | get_grouped_profiled_requests, 5 | ) 6 | 7 | from flask_monitoringdashboard.database import StackLine, Request 8 | 9 | 10 | def test_add_stackline(session, request_1): 11 | assert session.query(StackLine).filter(StackLine.request_id == request_1.id).one_or_none() is None 12 | add_stack_line(session, request_id=request_1.id, position=0, indent=1, duration=1, code_line="code") 13 | session.commit() 14 | assert session.query(StackLine).filter(StackLine.request_id == request_1.id).one() 15 | 16 | 17 | def test_get_profiled_requests(session, endpoint, request_1): 18 | assert not get_profiled_requests(session, endpoint_id=endpoint.id, offset=0, per_page=10) 19 | add_stack_line(session, request_id=request_1.id, position=0, indent=1, duration=1, code_line="code") 20 | session.commit() 21 | assert get_profiled_requests(session, endpoint_id=endpoint.id, offset=0, per_page=10) 22 | 23 | 24 | def test_get_grouped_profiled_requests(session, request_1, endpoint): 25 | assert not get_grouped_profiled_requests(session, endpoint_id=endpoint.id) 26 | add_stack_line(session, request_id=request_1.id, position=0, indent=1, duration=1, code_line="code") 27 | session.commit() 28 | assert get_grouped_profiled_requests(session, endpoint_id=endpoint.id) --------------------------------------------------------------------------------