Skip to content

Commit fce10d8

Browse files
Ziulgutorc92
andauthored
Ensure Flask-Monitor is singleton (#4)
* Teste Signed-off-by: Gustavo Coelho <[email protected]> * make metrics registry singleton and stops Timer thread when close Signed-off-by: Luiz Oliveira <[email protected]> * update docs to watch_dependencies Signed-off-by: Luiz Oliveira <[email protected]> * improve tests Signed-off-by: Luiz Oliveira <[email protected]> * install nose to run testes Signed-off-by: Luiz Oliveira <[email protected]> * use default prom registry if none passed Signed-off-by: Luiz Oliveira <[email protected]> * include tests for watch_dependencies Signed-off-by: Luiz Oliveira <[email protected]> * isError receives conditional result Signed-off-by: Luiz Oliveira <[email protected]> * using extensions to hold prometheus registry Signed-off-by: Luiz Oliveira <[email protected]> * default status code value to exception Signed-off-by: Luiz Oliveira <[email protected]> * splited watch_dependencies in watch_dependencies and collect_dependency_time Signed-off-by: Luiz Oliveira <[email protected]> * improved simple_example Signed-off-by: Luiz Oliveira <[email protected]> * using status code as result in example Signed-off-by: Luiz Oliveira <[email protected]> * removed global statement Signed-off-by: Luiz Oliveira <[email protected]> * removed use of kwargs Signed-off-by: Luiz Oliveira <[email protected]> * removed start time from cdt Signed-off-by: Luiz Oliveira <[email protected]> Co-authored-by: Gustavo Coelho <[email protected]>
1 parent a08c857 commit fce10d8

File tree

13 files changed

+290
-208
lines changed

13 files changed

+290
-208
lines changed

.github/workflows/continuous-integration-workflow.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,7 @@ jobs:
2121
python-version: ${{ matrix.python-version }}
2222
- name: Install dependencies
2323
run: pip3 install -r requirements.txt
24+
- name: Install tests dependencies
25+
run: pip3 install nose coverage
2426
- name: Run tests
25-
run: pytest
27+
run: nosetests --with-coverage --cover-package=flask_monitor -v tests

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def check_db():
106106
traceback.print_stack()
107107
return 0
108108

109-
watch_dependencies("Bd", check_db)
109+
watch_dependencies("Bd", check_db, app=app)
110110
```
111111

112112
Other optional parameters are also:

example/simple_example.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@
33
from werkzeug.middleware.dispatcher import DispatcherMiddleware
44
from werkzeug.serving import run_simple
55
import traceback
6-
from flask_monitor import register_metrics, watch_dependencies
6+
from flask_monitor import register_metrics, watch_dependencies, collect_dependency_time
77
from flask import Flask
88
import requests as req
9+
from prometheus_client import CollectorRegistry
10+
from time import time, sleep
11+
from random import random
12+
13+
registry = CollectorRegistry()
914

1015
## create a flask app
1116
app = Flask(__name__)
@@ -23,27 +28,27 @@ def is_error200(code):
2328
# buckets is the internavals for histogram parameter. buckets is a optional parameter
2429
# error_fn is a function to define what http status code is a error. By default errors are
2530
# 400 and 500 status code. error_fn is a option parameter
26-
register_metrics(app, buckets=[0.3, 0.6], error_fn=is_error200)
31+
register_metrics(app, buckets=[0.3, 0.6], error_fn=is_error200, registry=registry)
2732

2833
# Plug metrics WSGI app to your main app with dispatcher
29-
dispatcher = DispatcherMiddleware(app.wsgi_app, {"/metrics": make_wsgi_app()})
34+
dispatcher = DispatcherMiddleware(app.wsgi_app, {"/metrics": make_wsgi_app(registry=registry)})
3035

3136
# a dependency healthcheck
3237
def check_db():
3338
try:
3439
response = req.get("http://localhost:5000/database")
35-
if response.status_code == 200:
36-
return 1
40+
app.logger.info(response)
41+
return response.status_code < 400
3742
except:
38-
traceback.print_stack()
39-
return 0
43+
return 0
44+
4045

4146
# watch dependency
4247
# first parameter is the dependency's name. It's a mandatory parameter.
4348
# second parameter is the health check function. It's a mandatory parameter.
4449
# time_execution is used to set the interval of running the healthchec function.
4550
# time_execution is a optional parameter
46-
watch_dependencies("database", check_db, time_execution=1)
51+
scheduler = watch_dependencies('database', check_db, app=app, time_execution=500)
4752

4853
# endpoint
4954
@app.route('/teste')
@@ -58,6 +63,22 @@ def hello_world():
5863
# endpoint
5964
@app.route('/database')
6065
def bd_running():
66+
start = time()
67+
# checks the database
68+
sleep(random()/10)
69+
# compute the elapsed time
70+
elapsed = time() - start
71+
# register the dependency time
72+
collect_dependency_time(
73+
app=app,
74+
name='database',
75+
rtype='http',
76+
status=200,
77+
is_error= 'False',
78+
method='GET',
79+
addr='external/database',
80+
elapsed=elapsed
81+
)
6182
return 'I am a database working.'
6283

6384
if __name__ == "__main__":

flask_monitor/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
from .metrics import register_metrics, watch_dependencies
1+
""" Functions for define and register metrics """
2+
from .metrics import register_metrics, watch_dependencies, collect_dependency_time

flask_monitor/metrics.py

Lines changed: 112 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,14 @@
11
""" Functions for define and register metrics """
22
import time
3-
import threading
4-
from flask import request
5-
from prometheus_client import Counter, Histogram, Gauge
3+
import atexit
4+
from flask import request, current_app
5+
from prometheus_client import Counter, Histogram, Gauge, CollectorRegistry
6+
from apscheduler.schedulers.background import BackgroundScheduler
67

78
#
89
# Request callbacks
910
#
10-
11-
METRICS_INFO = Gauge(
12-
"application_info",
13-
"records static application info such as it's semantic version number",
14-
["version"]
15-
)
16-
17-
DEPENDENCY_UP = Gauge(
18-
'dependency_up',
19-
'records if a dependency is up or down. 1 for up, 0 for down',
20-
["name"]
21-
)
22-
23-
def is_error(code):
11+
def _is_error_(code):
2412
"""
2513
Default status error checking
2614
"""
@@ -30,7 +18,7 @@ def is_error(code):
3018
#
3119
# Metrics registration
3220
#
33-
def register_metrics(app, buckets=None, error_fn=None):
21+
def register_metrics(app=current_app, buckets=None, error_fn=None, registry=None):
3422
"""
3523
Register metrics middlewares
3624
@@ -42,24 +30,46 @@ def register_metrics(app, buckets=None, error_fn=None):
4230
Before CPython 3.6 dictionaries didn't guarantee keys order, so callbacks
4331
could be executed in arbitrary order.
4432
"""
33+
34+
if app.config.get("METRICS_ENABLED", False):
35+
return app, app.extensions.get("registry", registry)
36+
app.config["METRICS_ENABLED"] = True
37+
if not registry:
38+
registry = app.extensions.get("registry", CollectorRegistry())
39+
app.extensions["registry"] = registry
40+
app.logger.info('Metrics enabled')
41+
4542
buckets = [0.1, 0.3, 1.5, 10.5] if buckets is None else buckets
43+
44+
4645
# pylint: disable=invalid-name
47-
METRICS_REQUEST_LATENCY = Histogram(
46+
metrics_info = Gauge(
47+
"application_info",
48+
"records static application info such as it's semantic version number",
49+
["version", "name"],
50+
registry=registry
51+
)
52+
53+
# pylint: disable=invalid-name
54+
metrics_request_latency = Histogram(
4855
"request_seconds",
4956
"records in a histogram the number of http requests and their duration in seconds",
50-
["type", "status", "isError", "method", "addr"],
51-
buckets=buckets
57+
["type", "status", "isError", "errorMessage", "method", "addr"],
58+
buckets=buckets,
59+
registry=registry
5260
)
5361

54-
METRICS_REQUEST_SIZE = Counter(
62+
# pylint: disable=invalid-name
63+
metrics_request_size = Counter(
5564
"response_size_bytes",
5665
"counts the size of each http response",
57-
["type", "status", "isError", "method", "addr"],
66+
["type", "status", "isError", "errorMessage", "method", "addr"],
67+
registry=registry
5868
)
5969
# pylint: enable=invalid-name
6070

6171
app_version = app.config.get("APP_VERSION", "0.0.0")
62-
METRICS_INFO.labels(app_version).set(1)
72+
metrics_info.labels(app_version, app.name).set(1)
6373

6474
def before_request():
6575
"""
@@ -77,29 +87,91 @@ def after_request(response):
7787
# pylint: disable=protected-access
7888
request_latency = time.time() - request._prometheus_metrics_request_start_time
7989
# pylint: enable=protected-access
80-
error_status = is_error(response.status_code)
81-
METRICS_REQUEST_LATENCY \
82-
.labels("http", response.status_code, error_status, request.method, request.path) \
90+
error_status = _is_error_(response.status_code)
91+
metrics_request_latency \
92+
.labels("http", response.status_code, error_status, "", request.method, request.path) \
8393
.observe(request_latency)
84-
METRICS_REQUEST_SIZE.labels(
85-
"http", response.status_code, error_status, request.method, request.path
94+
metrics_request_size.labels(
95+
"http", response.status_code, error_status, "", request.method, request.path
8696
).inc(size_request)
8797
return response
98+
8899
if error_fn is not None:
89-
is_error.__code__ = error_fn.__code__
100+
_is_error_.__code__ = error_fn.__code__
90101
app.before_request(before_request)
91102
app.after_request(after_request)
103+
return app, registry
104+
92105

93-
def watch_dependencies(dependency, func, time_execution=1500):
106+
def watch_dependencies(dependency, func, time_execution=15000, registry=None, app=current_app):
107+
"""
108+
Register dependencies metrics up
109+
"""
110+
111+
if not registry:
112+
registry = app.extensions.get("registry", CollectorRegistry())
113+
app.extensions["registry"] = registry
114+
115+
# pylint: disable=invalid-name
116+
DEPENDENCY_UP = Gauge(
117+
'dependency_up',
118+
'records if a dependency is up or down. 1 for up, 0 for down',
119+
["name"],
120+
registry=registry
121+
)
122+
def register_dependecy():
123+
DEPENDENCY_UP.labels(dependency).set(func())
124+
125+
scheduler = BackgroundScheduler()
126+
scheduler.add_job(
127+
func=register_dependecy,
128+
trigger="interval",
129+
seconds=time_execution/1000,
130+
max_instances=1,
131+
name='dependency',
132+
misfire_grace_time=2,
133+
replace_existing=True
134+
)
135+
scheduler.start()
136+
137+
# Shut down the scheduler when exiting the app
138+
atexit.register(scheduler.shutdown)
139+
return scheduler
140+
141+
# pylint: disable=too-many-arguments
142+
def collect_dependency_time(
143+
app, name, rtype='http', status=200,
144+
is_error=False, error_message='',
145+
method='GET', addr='/',
146+
elapsed=0,
147+
registry=None
148+
):
94149
"""
95150
Register dependencies metrics
96151
"""
97-
def thread_function():
98-
thread = threading.Timer(time_execution, lambda x: x + 1, args=(1,))
99-
thread.start()
100-
thread.join()
101-
response = func()
102-
DEPENDENCY_UP.labels(dependency).set(response)
103-
thread_function()
104-
thread = threading.Timer(time_execution, thread_function)
105-
thread.start()
152+
153+
if not registry:
154+
registry = app.extensions.get("registry", CollectorRegistry())
155+
app.extensions["registry"] = registry
156+
157+
dependency_up_latency = app.extensions.get(
158+
"dependency_latency"
159+
)
160+
if not dependency_up_latency:
161+
app.extensions['dependency_latency'] = dependency_up_latency = Histogram(
162+
"dependency_request_seconds",
163+
"records in a histogram the number of requests to dependency",
164+
["name", "type", "status", "isError", "errorMessage", "method", "addr"],
165+
registry=registry
166+
)
167+
168+
dependency_up_latency \
169+
.labels(
170+
name,
171+
rtype.lower(),
172+
status,
173+
"False" if is_error else "True",
174+
error_message,
175+
method.upper(),
176+
addr) \
177+
.observe(elapsed)

flask_monitor/tests/__init__.py

Whitespace-only changes.

flask_monitor/tests/app.py

Lines changed: 0 additions & 66 deletions
This file was deleted.

0 commit comments

Comments
 (0)