Files
Juan Denis d67e5d07d0 fix: complete audit follow-ups and add regression coverage
Centralize backup schedule calculations and shared request/form flows, encode query values, and move repository finalization into its service. Add browser CI coverage, repair stale test fixtures, and provide opt-in request and build measurement tools.
2026-09-05 14:41:15 -04:00

67 lines
2.4 KiB
Python

"""Opt-in request/SQL costs, without SQL text, parameters or persistent storage."""
from time import perf_counter_ns
from flask import g, has_request_context, request
from sqlalchemy import event
def register_request_profiling(app, database):
"""Attach only when enabled; normal deployments install no query listeners.
Counts SQLAlchemy statements on the request thread, including failed SQL.
Streaming body iteration and background jobs occur outside this interval.
"""
if not app.config.get('PROFILE_REQUESTS'):
return
if app.extensions.get('serverkit_request_profiling'):
return
app.extensions['serverkit_request_profiling'] = True
@app.before_request
def begin_request():
if request.path.startswith('/api/'):
g.serverkit_profile = {
'start': perf_counter_ns(), 'queries': 0, 'sql_ns': 0,
}
def before_sql(_conn, _cursor, _statement, _parameters, context, _many):
if not has_request_context():
return
state = getattr(g, 'serverkit_profile', None)
if state is not None:
state['queries'] += 1
context._serverkit_profile = (state, perf_counter_ns())
def finish_sql(context):
timing = getattr(context, '_serverkit_profile', None)
if timing is not None:
state, start = timing
state['sql_ns'] += perf_counter_ns() - start
context._serverkit_profile = None
def after_sql(_conn, _cursor, _statement, _parameters, context, _many):
finish_sql(context)
def failed_sql(error_context):
finish_sql(error_context.execution_context)
with app.app_context():
for engine in set(database.engines.values()):
event.listen(engine, 'before_cursor_execute', before_sql)
event.listen(engine, 'after_cursor_execute', after_sql)
event.listen(engine, 'handle_error', failed_sql)
@app.after_request
def expose_profile(response):
state = getattr(g, 'serverkit_profile', None)
if state is not None:
app_ms = (perf_counter_ns() - state['start']) / 1_000_000
sql_ms = state['sql_ns'] / 1_000_000
response.headers.add(
'Server-Timing',
f'app;dur={app_ms:.3f}, db;dur={sql_ms:.3f};'
f'desc="{state["queries"]} queries"',
)
return response