Files
ServerKit/backend/app/middleware/api_analytics.py
T
Juan Denis c5710e88b9 Add API keys, analytics, webhooks, and middleware
Introduce programmatic API features and observability: add API Key management, API usage analytics, webhook event subscriptions, and OpenAPI docs. New models (ApiKey, ApiUsageLog, ApiUsageSummary, EventSubscription, EventDelivery), services (api_analytics, api_key, event, openapi), and blueprints (/api-keys, /api-analytics, /event-subscriptions, /docs) were added and registered in create_app. Middleware for API key authentication, buffered analytics logging (with a flush thread), and enhanced rate limiting (custom key function and response headers) were implemented; RBAC was updated to accept API key auth via an auth_required decorator. Background threads for hourly aggregation and event retry were added; security CSP updated to allow resources for Swagger UI. Frontend settings UI and styles were added/updated for API keys and webhooks, and requirements were updated.
2026-03-04 01:53:46 -05:00

116 lines
3.3 KiB
Python

"""API analytics middleware for logging request metrics."""
import time
import threading
import logging
from flask import g, request
logger = logging.getLogger(__name__)
# Buffer for batch inserts
_log_buffer = []
_buffer_lock = threading.Lock()
def register_api_analytics(app):
"""Register before/after request handlers for API analytics."""
@app.before_request
def record_request_start():
"""Record request start time."""
if request.path.startswith('/api/'):
g.request_start_time = time.time()
@app.after_request
def record_request_metrics(response):
"""Log API request metrics."""
if not request.path.startswith('/api/'):
return response
start_time = getattr(g, 'request_start_time', None)
if start_time is None:
return response
elapsed_ms = (time.time() - start_time) * 1000
api_key = getattr(g, 'api_key', None)
api_key_user = getattr(g, 'api_key_user', None)
# Get user_id from API key or JWT
user_id = None
if api_key_user:
user_id = api_key_user.id
else:
try:
from flask_jwt_extended import get_jwt_identity
user_id = get_jwt_identity()
except Exception:
pass
ip_address = request.remote_addr
if request.headers.get('X-Forwarded-For'):
ip_address = request.headers.get('X-Forwarded-For').split(',')[0].strip()
elif request.headers.get('X-Real-IP'):
ip_address = request.headers.get('X-Real-IP')
log_entry = {
'api_key_id': api_key.id if api_key else None,
'user_id': user_id,
'method': request.method,
'endpoint': request.path,
'blueprint': request.blueprints[0] if request.blueprints else None,
'status_code': response.status_code,
'response_time_ms': round(elapsed_ms, 2),
'ip_address': ip_address,
'user_agent': (request.headers.get('User-Agent') or '')[:500],
'request_size': request.content_length or 0,
'response_size': response.content_length or 0,
}
with _buffer_lock:
_log_buffer.append(log_entry)
return response
def start_analytics_flush_thread(app):
"""Start background thread to flush analytics buffer to DB."""
def flush_loop():
while True:
time.sleep(5)
try:
_flush_buffer(app)
except Exception as e:
logger.error(f'Analytics flush error: {e}')
thread = threading.Thread(
target=flush_loop,
daemon=True,
name='api-analytics-flush'
)
thread.start()
return thread
def _flush_buffer(app):
"""Flush buffered log entries to the database."""
with _buffer_lock:
if not _log_buffer:
return
entries = _log_buffer.copy()
_log_buffer.clear()
with app.app_context():
from app import db
from app.models.api_usage import ApiUsageLog
for entry in entries:
log = ApiUsageLog(**entry)
db.session.add(log)
try:
db.session.commit()
except Exception as e:
db.session.rollback()
logger.error(f'Failed to flush analytics: {e}')