1
1
from typing import cast
2
+ import time
2
3
3
4
from flask import Flask , g , request
5
+ from sqlalchemy import event
4
6
from sqlalchemy .engine import Connection
5
7
from werkzeug .local import LocalProxy
6
8
9
+ from .utils .logger import get_structured_logger
7
10
from ._config import SECRET
8
11
from ._db import engine
9
- from ._exceptions import DatabaseErrorException
12
+ from ._exceptions import DatabaseErrorException , EpiDataException
10
13
11
14
app = Flask ("EpiData" , static_url_path = "" )
12
15
app .config ["SECRET" ] = SECRET
@@ -24,19 +27,53 @@ def _get_db() -> Connection:
24
27
"""
25
28
db : Connection = cast (Connection , LocalProxy (_get_db ))
26
29
30
+ @event .listens_for (engine , "before_cursor_execute" )
31
+ def before_cursor_execute (conn , cursor , statement , parameters , context , executemany ):
32
+ context ._query_start_time = time .time ()
33
+
34
+
35
+ @event .listens_for (engine , "after_cursor_execute" )
36
+ def after_cursor_execute (conn , cursor , statement , parameters , context , executemany ):
37
+ # this timing info may be suspect, at least in terms of dbms cpu time...
38
+ # it is likely that it includes that time as well as any overhead that
39
+ # comes from throttling or flow control on the streamed data, as well as
40
+ # any row transform/processing time
41
+ total_time = time .time () - context ._query_start_time
42
+
43
+ # Convert to milliseconds
44
+ total_time *= 1000
45
+ get_structured_logger ('server_api' ).info ("Executed SQL" , statement = statement , params = parameters , elapsed_time_ms = total_time )
46
+
27
47
28
48
@app .before_request
29
- def connect_db ():
49
+ def before_request_execute ():
50
+ # Set timer for statement
51
+ g ._request_start_time = time .time ()
52
+
53
+ # Log statement
54
+ get_structured_logger ('server_api' ).info ("Received API request" , method = request .method , url = request .url , form_args = request .form , req_length = request .content_length , remote_addr = request .remote_addr , user_agent = request .user_agent .string )
55
+
30
56
if request .path .startswith ('/lib' ):
31
57
return
32
58
# try to get the db
33
59
try :
34
60
_get_db ()
35
- except :
36
- app . logger . error ('database connection error' , exc_info = True )
61
+ except Exception as e :
62
+ get_structured_logger ( 'server_error' ). error ('database connection error' , exception = e )
37
63
raise DatabaseErrorException ()
38
64
39
65
66
+ @app .after_request
67
+ def after_request_execute (response ):
68
+ total_time = time .time () - g ._request_start_time
69
+ # Convert to milliseconds
70
+ total_time *= 1000
71
+ get_structured_logger ('server_api' ).info ('Served API request' , method = request .method , url = request .url , form_args = request .form , req_length = request .content_length , remote_addr = request .remote_addr , user_agent = request .user_agent .string ,
72
+ values = request .values .to_dict (flat = False ), blueprint = request .blueprint , endpoint = request .endpoint ,
73
+ response_status = response .status , content_length = response .calculate_content_length (), elapsed_time_ms = total_time )
74
+ return response
75
+
76
+
40
77
@app .teardown_appcontext
41
78
def teardown_db (exception = None ):
42
79
# close the db connection
@@ -46,6 +83,16 @@ def teardown_db(exception=None):
46
83
db .close ()
47
84
48
85
86
+ @app .errorhandler (EpiDataException )
87
+ def handle_exception (e ):
88
+ # Log error and pass through; EpiDataExceptions are HTTPExceptions which are valid WSGI responses (see https://werkzeug.palletsprojects.com/en/2.2.x/exceptions/ )
89
+ if isinstance (e , DatabaseErrorException ):
90
+ get_structured_logger ('server_error' ).error ('Received DatabaseErrorException' , exception = str (e ), exc_info = True )
91
+ else :
92
+ get_structured_logger ('server_error' ).warn ('Encountered user-side error' , exception = str (e ))
93
+ return e
94
+
95
+
49
96
def is_compatibility_mode () -> bool :
50
97
"""
51
98
checks whether this request is in compatibility mode
0 commit comments