init medical training project
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
"""API 请求/响应日志中间件。
|
||||
|
||||
记录每次 API 调用的完整信息:方法、路径、请求头、查询参数、请求体、
|
||||
响应状态码、响应头、响应体。
|
||||
日志输出到 `api_access` logger → `logs/api-access-YYYY-MM-DD.log`。
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
|
||||
api_logger = logging.getLogger('api_access')
|
||||
|
||||
# 跳过日志的路径前缀(静态文件、admin 等)
|
||||
_SKIP_PREFIXES = ('/static/', '/admin/', '/api/schema/', '/api/docs/')
|
||||
|
||||
# 请求体/响应体最大记录长度(字符)
|
||||
_MAX_BODY_LEN = 2000
|
||||
|
||||
# 需要记录的请求头(Django 中请求头加 HTTP_ 前缀并大写)
|
||||
_LOG_REQUEST_HEADERS = (
|
||||
'HTTP_AUTHORIZATION',
|
||||
'HTTP_ACCEPT',
|
||||
'HTTP_USER_AGENT',
|
||||
'HTTP_X_FORWARDED_FOR',
|
||||
'HTTP_X_REAL_IP',
|
||||
)
|
||||
|
||||
|
||||
def _truncate(text, max_len=_MAX_BODY_LEN):
|
||||
if len(text) > max_len:
|
||||
return text[:max_len] + f'... (truncated, total {len(text)} chars)'
|
||||
return text
|
||||
|
||||
|
||||
def _safe_json_body(body_bytes, content_type=''):
|
||||
"""尝试将请求/响应体解析为 JSON,失败则返回原始文本摘要。"""
|
||||
if not body_bytes:
|
||||
return ''
|
||||
# multipart (文件上传) 不记录原始内容
|
||||
if 'multipart' in content_type:
|
||||
return f'<multipart form data, {len(body_bytes)} bytes>'
|
||||
try:
|
||||
text = body_bytes.decode('utf-8')
|
||||
data = json.loads(text)
|
||||
return _truncate(json.dumps(data, ensure_ascii=False))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError):
|
||||
return _truncate(body_bytes.decode('utf-8', errors='replace'))
|
||||
|
||||
|
||||
class APIAccessLogMiddleware:
|
||||
"""记录 /api/ 请求与响应的中间件。"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
@staticmethod
|
||||
def _collect_request_headers(request):
|
||||
"""提取关键请求头。"""
|
||||
headers = {}
|
||||
ct = request.content_type or request.META.get('CONTENT_TYPE', '')
|
||||
if ct:
|
||||
headers['Content-Type'] = ct
|
||||
for meta_key in _LOG_REQUEST_HEADERS:
|
||||
val = request.META.get(meta_key)
|
||||
if val:
|
||||
name = meta_key.replace('HTTP_', '').replace('_', '-').title()
|
||||
headers[name] = val
|
||||
return headers
|
||||
|
||||
@staticmethod
|
||||
def _collect_response_headers(response):
|
||||
"""提取关键响应头。"""
|
||||
keys = ('Content-Type', 'Content-Length', 'Allow',
|
||||
'X-Request-Id', 'Retry-After', 'WWW-Authenticate')
|
||||
headers = {}
|
||||
for k in keys:
|
||||
v = response.get(k)
|
||||
if v:
|
||||
headers[k] = v
|
||||
return headers
|
||||
|
||||
def __call__(self, request):
|
||||
path = request.path
|
||||
|
||||
if not path.startswith('/api/') or any(path.startswith(p) for p in _SKIP_PREFIXES):
|
||||
return self.get_response(request)
|
||||
|
||||
start = time.time()
|
||||
|
||||
req_headers = self._collect_request_headers(request)
|
||||
|
||||
req_content_type = request.content_type or ''
|
||||
if 'multipart' in req_content_type:
|
||||
form_fields = dict(request.POST)
|
||||
file_info = {
|
||||
name: f'<file: {f.name}, {f.size} bytes>'
|
||||
for name, f in request.FILES.items()
|
||||
}
|
||||
all_fields = {**form_fields, **file_info}
|
||||
req_body = _truncate(json.dumps(all_fields, ensure_ascii=False)) if all_fields else ''
|
||||
else:
|
||||
req_body = _safe_json_body(request.body, req_content_type)
|
||||
|
||||
query = dict(request.GET) if request.GET else ''
|
||||
|
||||
user_id = None
|
||||
|
||||
response = self.get_response(request)
|
||||
|
||||
if hasattr(request, 'user') and request.user and request.user.is_authenticated:
|
||||
user_id = request.user.id
|
||||
|
||||
duration_ms = int((time.time() - start) * 1000)
|
||||
|
||||
resp_headers = self._collect_response_headers(response)
|
||||
|
||||
resp_content_type = response.get('Content-Type', '')
|
||||
resp_body = ''
|
||||
if hasattr(response, 'content'):
|
||||
resp_body = _safe_json_body(response.content, resp_content_type)
|
||||
|
||||
req_h_str = json.dumps(req_headers, ensure_ascii=False) if req_headers else ''
|
||||
resp_h_str = json.dumps(resp_headers, ensure_ascii=False) if resp_headers else ''
|
||||
|
||||
api_logger.info(
|
||||
'%s %s | user=%s | query=%s | status=%s | %dms\n'
|
||||
' >>> headers: %s\n'
|
||||
' >>> body: %s\n'
|
||||
' <<< headers: %s\n'
|
||||
' <<< body: %s',
|
||||
request.method, path,
|
||||
user_id, query, response.status_code, duration_ms,
|
||||
req_h_str, req_body,
|
||||
resp_h_str, resp_body,
|
||||
)
|
||||
|
||||
return response
|
||||
Reference in New Issue
Block a user