11from typing import cast
2+ import time
23
34from flask import Flask , g , request
5+ from sqlalchemy import event
46from sqlalchemy .engine import Connection
57from werkzeug .local import LocalProxy
68
9+ from .utils .logger import get_structured_logger
710from ._config import SECRET
811from ._db import engine
9- from ._exceptions import DatabaseErrorException
12+ from ._exceptions import DatabaseErrorException , EpiDataException
1013
1114app = Flask ("EpiData" , static_url_path = "" )
1215app .config ["SECRET" ] = SECRET
@@ -24,19 +27,53 @@ def _get_db() -> Connection:
2427"""
2528db : Connection = cast (Connection , LocalProxy (_get_db ))
2629
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+
2747
2848@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+
3056 if request .path .startswith ('/lib' ):
3157 return
3258 # try to get the db
3359 try :
3460 _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 )
3763 raise DatabaseErrorException ()
3864
3965
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+
4077@app .teardown_appcontext
4178def teardown_db (exception = None ):
4279 # close the db connection
@@ -46,6 +83,16 @@ def teardown_db(exception=None):
4683 db .close ()
4784
4885
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+
4996def is_compatibility_mode () -> bool :
5097 """
5198 checks whether this request is in compatibility mode
0 commit comments