Skip to content

Commit 4551f19

Browse files
authored
Merge pull request #25 from mircealungu/development
Development
2 parents 31c6ba8 + 49ef450 commit 4551f19

File tree

108 files changed

+1372
-9375
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

108 files changed

+1372
-9375
lines changed

README.md

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
1-
# Flask Dashboard
1+
# Automatic monitoring dashboard
22
Dashboard for automatic monitoring of python web services
33

44
This is a flask extension that can be added to your existing flask application.
55

66
It measures which python functions are heavily used and which are not.
7+
78
You can see the execution time and last access time per endpoint.
8-
Also, unit tests can be run and monitored.
9+
10+
Also, unit tests can be run by TravisCI and monitored.
911

1012
Installation
1113
============
1214
To install from source, download the source code, then run this:
1315

1416
python setup.py install
17+
18+
Or install with pip:
19+
20+
pip install flask_monitoring_dashboard
1521

1622
Setup
1723
=====
@@ -40,12 +46,39 @@ The following things can be configured:
4046
CUSTOM_LINK=dashboard
4147
USERNAME=admin
4248
PASSWORD=admin
49+
GUEST_USERNAME=guest
50+
GUEST_PASSWORD=dashboardguest!
4351
DATABASE=sqlite:////<path to your project>/dashboard.db
4452
GIT=/<path to your project>/dashboard/.git/
4553
TEST_DIR=/<path to your project>/dashboard/tests/
54+
N=5
55+
SUBMIT_RESULTS_URL=http://0.0.0.0:5000/dashboard/submit-test-results
56+
OUTLIER_DETECTION_CONSTANT=2.5
57+
COLORS={'main':[0,97,255], 'static':[255,153,0]}
4658

4759
For more information: [see this file](dashboard/config.py)
4860

4961
When running your app, the dashboard van be viewed by default in the route:
5062

5163
/dashboard
64+
65+
TravisCI unit testing
66+
=====================
67+
To enable Travis to run your unit tests and send the results to the dashboard, four steps have to be taken.
68+
69+
First off, the file 'collect_performance.py' (which comes with the dashboard) should be copied to the directory where your '.travis.yml' file resides.
70+
71+
Secondly, your config file for the dashboard ('config.cfg') should be updated to include three additional values, TEST_DIR, SUBMIT_RESULTS_URL and N.
72+
The first specifies where your unit tests reside, the second where Travis should upload the test results to, and the third specifies the number of times Travis should run each unit test.
73+
See the sample config file in the section above for more details.
74+
75+
Then, a dependency link to the dashboard has to be added to the 'setup.py' file of your app:
76+
77+
dependency_links=["git+https://github.com/mircealungu/automatic-monitoring-dashboard.git#egg=flask_monitoring_dashboard"],
78+
install_requires=('flask_monitoring_dashboard')
79+
80+
Lastly, in your '.travis.yml' file, two script commands should be added:
81+
82+
script:
83+
- export DASHBOARD_CONFIG=config.cfg
84+
- python ./collect_performance.py

dashboard/collect_performance.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import requests
2+
import configparser
3+
import time
4+
import datetime
5+
import os
6+
import sys
7+
from unittest import TestLoader
8+
9+
# Abort if config file is not specified.
10+
config = os.getenv('DASHBOARD_CONFIG')
11+
if config is None:
12+
print('You must specify a config file for the dashboard to be able to use the unit test monitoring functionality.')
13+
print('Please set an environment variable \'DASHBOARD_CONFIG\' specifying the absolute path to your config file.')
14+
sys.exit(0)
15+
16+
n = 1
17+
parser = configparser.RawConfigParser()
18+
try:
19+
parser.read(config)
20+
if parser.has_option('dashboard', 'N'):
21+
n = int(parser.get('dashboard', 'N'))
22+
if parser.has_option('dashboard', 'TEST_DIR'):
23+
test_dir = parser.get('dashboard', 'TEST_DIR')
24+
else:
25+
print('No test directory specified in your config file. Please do so.')
26+
sys.exit(0)
27+
if parser.has_option('dashboard', 'SUBMIT_RESULTS_URL'):
28+
url = parser.get('dashboard', 'SUBMIT_RESULTS_URL')
29+
else:
30+
print('No url specified in your config file for submitting test results. Please do so.')
31+
sys.exit(0)
32+
except configparser.Error:
33+
raise
34+
35+
data = {'test_runs': []}
36+
37+
if test_dir:
38+
suites = TestLoader().discover(test_dir, pattern="*test*.py")
39+
for i in range(n):
40+
for suite in suites:
41+
for case in suite:
42+
for test in case:
43+
result = None
44+
time1 = time.time()
45+
result = test.run(result)
46+
time2 = time.time()
47+
t = (time2 - time1) * 1000
48+
data['test_runs'].append({'name': str(test), 'exec_time': t, 'time': str(datetime.datetime.now()),
49+
'successful': result.wasSuccessful(), 'iter': i + 1})
50+
51+
# Try to send test results to the dashboard
52+
try:
53+
requests.post(url, json=data)
54+
print('Sent unit test results to the dashboard.')
55+
except:
56+
print('Sending unit test results to the dashboard failed.')
57+
raise

dashboard/colors.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from colorhash import ColorHash
2+
from dashboard import config
3+
4+
5+
def get_color(hash):
6+
"""
7+
Returns an rgb-color (as string, which can be using in plotly) from a given hash,
8+
if no color for that string was specified in the config file.
9+
:param hash: the string that is translated into a color
10+
:return: a color (as string)
11+
"""
12+
if hash in config.colors:
13+
rgb = config.colors[hash]
14+
else:
15+
rgb = ColorHash(hash).rgb
16+
return 'rgb({0}, {1}, {2})'.format(rgb[0], rgb[1], rgb[2])
17+

dashboard/config.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import configparser
22
import os
3+
import ast
34

45

56
class Config(object):
@@ -18,6 +19,10 @@ def __init__(self):
1819
self.test_dir = None
1920
self.username = 'admin'
2021
self.password = 'admin'
22+
self.guest_username = 'guest'
23+
self.guest_password = 'guest_password'
24+
self.outlier_detection_constant = 2.5
25+
self.colors = {}
2126

2227
# define a custom function to retrieve the session_id or username
2328
self.get_group_by = None
@@ -41,7 +46,13 @@ def from_file(self, config_file):
4146
USERNAME: for logging into the dashboard, a username and password is required. The
4247
username can be set using this variable.
4348
PASSWORD: same as for the username, but this is the password variable.
44-
49+
GUEST_USERNAME: A guest can only see the results, but cannot configure/download data.
50+
GUEST_PASSWORD: A guest can only see the results, but cannot configure/download data.
51+
52+
OUTLIER_DETECTION_CONSTANT: When the execution time is more than this constant *
53+
average, extra information is logged into the database. A default value for this
54+
variable is 2.5, but can be changed in the config-file.
55+
4556
:param config_file: a string pointing to the location of the config-file
4657
"""
4758

@@ -57,6 +68,10 @@ def from_file(self, config_file):
5768
if parser.has_option('dashboard', 'TEST_DIR'):
5869
self.test_dir = parser.get('dashboard', 'TEST_DIR')
5970

71+
# For manually defining colors of specific endpoints
72+
if parser.has_option('dashboard', 'COLORS'):
73+
self.colors = ast.literal_eval(parser.get('dashboard', 'COLORS'))
74+
6075
# When the option git is selected, it overrides the given version
6176
if parser.has_option('dashboard', 'GIT'):
6277
git = parser.get('dashboard', 'GIT')
@@ -72,10 +87,20 @@ def from_file(self, config_file):
7287
print("Error reading one of the files to retrieve the current git-version.")
7388
raise
7489

75-
# provide username and/or password
90+
# provide username and/or password ..
91+
# .. for admin
7692
if parser.has_option('dashboard', 'USERNAME'):
7793
self.username = parser.get('dashboard', 'USERNAME')
7894
if parser.has_option('dashboard', 'PASSWORD'):
7995
self.password = parser.get('dashboard', 'PASSWORD')
96+
# .. for guest (a guest can only see the results, but cannot configure or download any data)
97+
if parser.has_option('dashboard', 'GUEST_USERNAME'):
98+
self.guest_username = parser.get('dashboard', 'GUEST_USERNAME')
99+
if parser.has_option('dashboard', 'GUEST_PASSWORD'):
100+
self.guest_password = parser.get('dashboard', 'GUEST_PASSWORD')
101+
102+
# when an outlier detection constant has been set up:
103+
if parser.has_option('dashboard', 'OUTLIER_DETECTION_CONSTANT'):
104+
self.outlier_detection_constant = parser.get('dashboard', 'OUTLIER_DETECTION_CONSTANT')
80105
except configparser.Error:
81106
raise

dashboard/database/__init__.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ class TestRun(Base):
5050
time = Column(DateTime, primary_key=True)
5151
# version of the website at the moment of adding the result to the database
5252
version = Column(String(100), nullable=False)
53+
# number of the test suite execution
54+
suite = Column(Integer)
55+
# number describing the i-th run of the test within the suite
56+
run = Column(Integer)
5357

5458

5559
class FunctionCall(Base):
@@ -69,6 +73,35 @@ class FunctionCall(Base):
6973
ip = Column(String(25), nullable=False)
7074

7175

76+
class Outlier(Base):
77+
""" Table for storing information about outliers. """
78+
__tablename__ = 'outliers'
79+
id = Column(Integer, primary_key=True, autoincrement=True)
80+
endpoint = Column(String(250), nullable=False)
81+
82+
# request-values, GET, POST, PUT
83+
request_values = Column(String(10000))
84+
# request headers
85+
request_headers = Column(String(10000))
86+
# request environment
87+
request_environment = Column(String(10000))
88+
# request url
89+
request_url = Column(String(1000))
90+
91+
# cpu_percent in use
92+
cpu_percent = Column(String(100))
93+
# memory
94+
memory = Column(String(10000))
95+
96+
# stacktrace
97+
stacktrace = Column(String(100000))
98+
99+
# execution_time in ms
100+
execution_time = Column(Float, nullable=False)
101+
# time of adding the result to the database
102+
time = Column(DateTime)
103+
104+
72105
# define the database
73106
engine = create_engine(config.database_name)
74107

dashboard/database/function_calls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from dashboard import config
88
import datetime
99
from dashboard.database import session_scope, FunctionCall
10+
from dashboard.colors import get_color
1011

1112

1213
def get_reqs_endpoint_day():

dashboard/database/outlier.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import datetime
2+
from flask import json, request
3+
from dashboard.database import session_scope, Outlier
4+
5+
6+
def add_outlier(endpoint, execution_time, stack_info):
7+
""" Collects information (request-parameters, memory, stacktrace) about the request and adds it in the database. """
8+
with session_scope() as db_session:
9+
outlier = Outlier(endpoint=endpoint, request_values=json.dumps(request.values),
10+
request_headers=str(request.headers), request_environment=str(request.environ),
11+
request_url=str(request.url), cpu_percent=stack_info.cpu_percent,
12+
memory=stack_info.memory, stacktrace=stack_info.stacktrace,
13+
execution_time=execution_time, time=datetime.datetime.now())
14+
db_session.add(outlier)
15+
16+
17+
def get_outliers(endpoint):
18+
""" Returns a list of all outliers of a specific endpoint. """
19+
with session_scope() as db_session:
20+
result = db_session.query(Outlier).filter(Outlier.endpoint == endpoint).all()
21+
db_session.expunge_all()
22+
return result

dashboard/database/tests.py

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Contains all functions that returns results of all tests
33
"""
44
from dashboard.database import session_scope, Tests, TestRun
5-
from sqlalchemy import func, desc, text
5+
from sqlalchemy import func, desc
66

77

88
def get_tests():
@@ -13,37 +13,38 @@ def get_tests():
1313
return result
1414

1515

16-
def get_test(name):
17-
""" Return all existing tests. """
18-
with session_scope() as db_session:
19-
result = db_session.query(Tests).filter(Tests.name == name).one()
20-
db_session.expunge_all()
21-
return result
22-
23-
2416
def add_test(name):
25-
""" Add a test to the database. """
17+
""" Add a newly found test to the database. """
2618
with session_scope() as db_session:
2719
db_session.add(Tests(name=name))
2820

2921

30-
def update_test(name, run, last_run, succeeded):
22+
def add_or_update_test(name, last_run, succeeded):
3123
""" Updates values of a test. """
3224
with session_scope() as db_session:
33-
db_session.query(Tests).filter(Tests.name == name). \
34-
update({Tests.run: run, Tests.lastRun: last_run, Tests.succeeded: succeeded})
25+
if db_session.query(Tests).filter(Tests.name == name).count():
26+
db_session.query(Tests).filter(Tests.name == name). \
27+
update({Tests.lastRun: last_run, Tests.succeeded: succeeded})
28+
else:
29+
db_session.add(Tests(name=name, lastRun=last_run, succeeded=succeeded))
3530

3631

37-
def reset_run():
38-
""" Sets all run values to False. """
32+
def add_test_result(name, exec_time, time, version, suite, iter):
33+
""" Add a test result to the database. """
3934
with session_scope() as db_session:
40-
db_session.query(Tests).update({Tests.run: False})
35+
db_session.add(
36+
TestRun(name=name, execution_time=exec_time, time=time, version=version, suite=suite, run=iter))
4137

4238

43-
def add_test_result(name, exec_time, time, version):
44-
""" Add a test result to the database. """
39+
def get_suite_nr():
40+
""" Retrieves the number of the next suite to run. """
4541
with session_scope() as db_session:
46-
db_session.add(TestRun(name=name, execution_time=exec_time, time=time, version=version))
42+
result = db_session.query(func.max(TestRun.suite).label('nr')).one()
43+
if result.nr is None:
44+
next_nr = 1
45+
else:
46+
next_nr = result.nr + 1
47+
return next_nr
4748

4849

4950
def get_results():
@@ -62,9 +63,9 @@ def get_res_current(version):
6263
with session_scope() as db_session:
6364
result = db_session.query(TestRun.name,
6465
func.count(TestRun.execution_time).label('count'),
65-
func.avg(TestRun.execution_time).label('average'))\
66-
.filter(TestRun.version == version)\
67-
.group_by(TestRun.name).order_by(desc('count')).all()
66+
func.avg(TestRun.execution_time).label('average')) \
67+
.filter(TestRun.version == version) \
68+
.group_by(TestRun.name).order_by(desc('count')).all()
6869
db_session.expunge_all()
6970
return result
7071

@@ -79,3 +80,26 @@ def get_line_results():
7980
).group_by(TestRun.version).order_by(desc('count')).all()
8081
db_session.expunge_all()
8182
return result
83+
84+
85+
def get_suites():
86+
with session_scope() as db_session:
87+
result = db_session.query(TestRun.suite).group_by(TestRun.suite).all()
88+
db_session.expunge_all()
89+
return result
90+
91+
92+
def get_measurements(suite):
93+
"""Return all measurements for some Travis build. Used for creating a box plot. """
94+
with session_scope() as db_session:
95+
result = db_session.query(TestRun).filter(TestRun.suite == suite).all()
96+
db_session.expunge_all()
97+
return result
98+
99+
100+
def get_test_measurements(name, suite):
101+
"""Return all measurements for some test of some Travis build. Used for creating a box plot. """
102+
with session_scope() as db_session:
103+
result = db_session.query(TestRun).filter(TestRun.name == name, TestRun.suite == suite).all()
104+
db_session.expunge_all()
105+
return result

dashboard/forms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from flask_wtf import FlaskForm
2-
from wtforms import validators, SubmitField, PasswordField, StringField, SelectMultipleField
2+
from wtforms import validators, SubmitField, PasswordField, StringField
33

44

55
class MonitorDashboard(FlaskForm):

0 commit comments

Comments
 (0)