128 lines
4.0 KiB
Python
128 lines
4.0 KiB
Python
import time
|
|
import json
|
|
import threading
|
|
from django.utils.deprecation import MiddlewareMixin
|
|
from django.conf import settings
|
|
|
|
|
|
SENSITIVE_FIELDS = {
|
|
'password', 'password1', 'password2', 'old_password', 'new_password',
|
|
'token', 'access_token', 'refresh_token', 'secret', 'api_key',
|
|
'authorization', 'credit_card', 'cvv', 'ssn',
|
|
}
|
|
|
|
EXCLUDE_PATHS = [
|
|
'/static/', '/media/', '/favicon.ico', '/admin/jsi18n/',
|
|
'/swagger/', '/redoc/',
|
|
]
|
|
|
|
|
|
def _get_client_ip(request):
|
|
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
|
if x_forwarded_for:
|
|
return x_forwarded_for.split(',')[0].strip()
|
|
return request.META.get('REMOTE_ADDR')
|
|
|
|
|
|
def _mask_sensitive_data(data):
|
|
if not data:
|
|
return data
|
|
try:
|
|
if isinstance(data, str):
|
|
try:
|
|
data = json.loads(data)
|
|
except (json.JSONDecodeError, TypeError):
|
|
return data[:2000]
|
|
if isinstance(data, dict):
|
|
masked = {}
|
|
for key, value in data.items():
|
|
if any(s in key.lower() for s in SENSITIVE_FIELDS):
|
|
masked[key] = '***'
|
|
elif isinstance(value, dict):
|
|
masked[key] = _mask_sensitive_data(value)
|
|
elif isinstance(value, list):
|
|
masked[key] = [_mask_sensitive_data(item) if isinstance(item, dict) else item for item in value[:10]]
|
|
else:
|
|
masked[key] = value
|
|
return json.dumps(masked, ensure_ascii=False)[:2000]
|
|
return str(data)[:2000]
|
|
except Exception:
|
|
return ''
|
|
|
|
|
|
def _extract_app_name(path):
|
|
parts = path.strip('/').split('/')
|
|
if parts and parts[0]:
|
|
return parts[0]
|
|
return 'root'
|
|
|
|
|
|
def _save_log_async(log_data):
|
|
def _save():
|
|
try:
|
|
from .models import ApiRequestLog
|
|
ApiRequestLog.objects.create(**log_data)
|
|
except Exception:
|
|
pass
|
|
thread = threading.Thread(target=_save)
|
|
thread.daemon = True
|
|
thread.start()
|
|
|
|
|
|
class RequestLoggingMiddleware(MiddlewareMixin):
|
|
def __init__(self, get_response):
|
|
super().__init__(get_response)
|
|
self.get_response = get_response
|
|
|
|
def __call__(self, request):
|
|
if any(request.path.startswith(path) for path in EXCLUDE_PATHS):
|
|
return self.get_response(request)
|
|
|
|
start_time = time.time()
|
|
|
|
try:
|
|
user = request.user if hasattr(request, 'user') and request.user.is_authenticated else None
|
|
except Exception:
|
|
user = None
|
|
|
|
response = self.get_response(request)
|
|
|
|
try:
|
|
duration_ms = (time.time() - start_time) * 1000
|
|
|
|
request_body = ''
|
|
if request.method in ('POST', 'PUT', 'PATCH'):
|
|
try:
|
|
if hasattr(request, 'body'):
|
|
body = request.body.decode('utf-8', errors='ignore')
|
|
request_body = _mask_sensitive_data(body)
|
|
except Exception:
|
|
request_body = ''
|
|
|
|
response_size = 0
|
|
if hasattr(response, 'content'):
|
|
response_size = len(response.content)
|
|
|
|
log_data = {
|
|
'user': user,
|
|
'username': user.username if user else (request.user.username if hasattr(request, 'user') and hasattr(request.user, 'username') else ''),
|
|
'ip_address': _get_client_ip(request),
|
|
'user_agent': request.META.get('HTTP_USER_AGENT', '')[:500],
|
|
'method': request.method,
|
|
'path': request.path[:255],
|
|
'query_string': request.META.get('QUERY_STRING', '')[:1000],
|
|
'request_body': request_body,
|
|
'status_code': response.status_code,
|
|
'response_size': response_size,
|
|
'duration_ms': round(duration_ms, 2),
|
|
'app_name': _extract_app_name(request.path),
|
|
'is_error': response.status_code >= 400,
|
|
}
|
|
|
|
_save_log_async(log_data)
|
|
|
|
except Exception:
|
|
pass
|
|
|
|
return response
|