mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-09 05:24:49 +09:00
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.
116 lines
3.3 KiB
Python
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}')
|