Добавлен средний слой с логированием данных
This commit is contained in:
207
net/xeaf/rack/utils/drf/custom_logging_middleware.py
Normal file
207
net/xeaf/rack/utils/drf/custom_logging_middleware.py
Normal file
@@ -0,0 +1,207 @@
|
||||
# DRF Rack
|
||||
# Библиотека классов расширений для Django REST Framework
|
||||
#
|
||||
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
|
||||
# Все права защищены. Лицензия: MIT
|
||||
|
||||
"""
|
||||
Описание класса CustomLoggingMiddleware
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from logging import Logger
|
||||
from typing import Any
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpRequest
|
||||
from django.http import HttpResponse
|
||||
from rest_framework import status
|
||||
|
||||
|
||||
class CustomLoggingMiddleware:
|
||||
"""
|
||||
Класс для логирования запросов и ответов DRF
|
||||
"""
|
||||
|
||||
DEFAULT_SENSITIVE_KEYS = {'password', 'old_password', 'new_password', 'token', 'access_token'}
|
||||
""" Словарь ключей, которые нужно замаскировать в логах """
|
||||
|
||||
MAX_LOG_LENGTH = 2048
|
||||
""" Максимальная длина объекта данных лога """
|
||||
|
||||
sensitive_keys: set = {}
|
||||
""" Актуальный словарь ключей, которые нужно замаскировать в логах """
|
||||
|
||||
logger: Logger
|
||||
""" Объект доступа к логгеру """
|
||||
|
||||
def __init__(self, get_response):
|
||||
"""
|
||||
Инициализация
|
||||
"""
|
||||
|
||||
self.get_response = get_response
|
||||
self.logger = logging.getLogger(settings.LOGGER_NAME)
|
||||
|
||||
self.sensitive_keys = set(self.DEFAULT_SENSITIVE_KEYS)
|
||||
if settings.LOGGER_SENSITIVE_KEYS:
|
||||
self.sensitive_keys.update(settings.LOGGER_SENSITIVE_KEYS)
|
||||
|
||||
def __call__(self, request: HttpRequest):
|
||||
"""
|
||||
Собираем данные запроса и отправляем в лог
|
||||
"""
|
||||
|
||||
# Обрабатываем данные запроса
|
||||
path = request.path
|
||||
method = request.method
|
||||
query_params = self._process_query_params(request)
|
||||
|
||||
# Передаем запрос дальше по конвейеру Django / DRF
|
||||
response = self.get_response(request)
|
||||
|
||||
# Обрабатываем данные запроса, ответа и перехваченного исключения
|
||||
body = self._process_body(request)
|
||||
exc = getattr(request, "_raised_exception", None)
|
||||
status_code = response.status_code
|
||||
request_id = request.META.get('HTTP_X_REQUEST_ID', '')
|
||||
response_content = self._process_response(response)
|
||||
|
||||
# Обрабатываем данные пользователя
|
||||
user_id = ""
|
||||
if hasattr(request, 'user'):
|
||||
if request.user.is_authenticated:
|
||||
user_id = str(request.user.id)
|
||||
|
||||
# Формируем единое сообщение для лога
|
||||
log_parts = [
|
||||
f"{status_code} [{method}] {path}"
|
||||
]
|
||||
|
||||
if user_id:
|
||||
log_parts.append(f"User ID: {user_id}")
|
||||
if request_id:
|
||||
log_parts.append(f"Request ID: {request_id}")
|
||||
if query_params:
|
||||
log_parts.append(f"Query Params: {query_params}")
|
||||
if body:
|
||||
log_parts.append(f"Request Body: {body}")
|
||||
if response_content:
|
||||
log_parts.append(f"Response Content: {response_content}")
|
||||
|
||||
log_message = "\n".join(log_parts) + "\n"
|
||||
|
||||
# Создаем и записываем лог
|
||||
self._write_log(log_message, status_code, exc)
|
||||
|
||||
return response
|
||||
|
||||
def _process_query_params(self, request: HttpRequest) -> dict[str, Any]:
|
||||
"""
|
||||
Обрабатывает параметры запроса и маскирует конфиденциальные данные
|
||||
|
||||
:param request: Объект запроса
|
||||
|
||||
:return: Обработанные параметры запроса
|
||||
"""
|
||||
|
||||
raw_query_params = dict(request.GET.lists())
|
||||
return self._mask_sensitive_data(raw_query_params)
|
||||
|
||||
def _process_body(self, request: HttpRequest) -> str:
|
||||
"""
|
||||
Обрабатывает тело запроса и маскирует конфиденциальные данные
|
||||
|
||||
:param request: Объект запроса
|
||||
|
||||
:return: Обработанное тело запроса
|
||||
"""
|
||||
|
||||
if request.body:
|
||||
return self._process_data(request.body)
|
||||
|
||||
return ""
|
||||
|
||||
def _process_response(self, response: HttpResponse) -> str:
|
||||
"""
|
||||
Обрабатываем данные ответа
|
||||
|
||||
:param response: HttpResponse
|
||||
|
||||
:return: Строковое представление данных ответа
|
||||
"""
|
||||
|
||||
if hasattr(response, 'content'):
|
||||
return self._process_data(response.content)
|
||||
|
||||
return ""
|
||||
|
||||
def _process_data(self, data: Any) -> str:
|
||||
"""
|
||||
Обрабатывает данные запроса или ответа и возвращает их в виде строки
|
||||
|
||||
:param data: Данные запроса или ответа
|
||||
|
||||
:return: Обработанные данные в виде строки
|
||||
"""
|
||||
|
||||
data_len = len(data)
|
||||
if data_len > self.MAX_LOG_LENGTH:
|
||||
return f"<Data too large to log (len={data_len})>"
|
||||
|
||||
try:
|
||||
raw_response = data.decode('utf-8')
|
||||
try:
|
||||
data_json = json.loads(raw_response)
|
||||
masked_response = self._mask_sensitive_data(data_json)
|
||||
result = json.dumps(masked_response, ensure_ascii=False, indent=2)
|
||||
except json.JSONDecodeError:
|
||||
result = raw_response
|
||||
except UnicodeDecodeError:
|
||||
result = f"<Binary/Streaming Data (len={data_len})>"
|
||||
|
||||
return result
|
||||
|
||||
def _write_log(self, log_message: str, status_code: int, exc: Exception | None):
|
||||
"""
|
||||
Записывает лог с учетом исключения
|
||||
|
||||
:param log_message: Сообщение лога
|
||||
:param status_code: Код статуса ответа
|
||||
:param exc: Исключение
|
||||
"""
|
||||
|
||||
if exc:
|
||||
|
||||
log_message += f"\nException: {str(exc)}"
|
||||
|
||||
if status_code == status.HTTP_400_BAD_REQUEST:
|
||||
self.logger.debug(log_message, exc_info=exc)
|
||||
elif status_code in [status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN]: # UNAUTHORIZED, FORBIDDEN
|
||||
self.logger.warning(log_message, exc_info=exc)
|
||||
elif status_code == status.HTTP_404_NOT_FOUND:
|
||||
self.logger.error(log_message, exc_info=exc)
|
||||
else:
|
||||
self.logger.critical(log_message, exc_info=exc)
|
||||
else:
|
||||
self.logger.debug(log_message)
|
||||
|
||||
def _mask_sensitive_data(self, data: Any) -> Any:
|
||||
"""
|
||||
Рекурсивно маскирует конфиденциальные данные в словарях и списках
|
||||
|
||||
:param data: Данные для маскировки
|
||||
|
||||
:return: Маскированные данные
|
||||
"""
|
||||
|
||||
if isinstance(data, dict):
|
||||
return {
|
||||
key: '***' if key.lower() in self.sensitive_keys else self._mask_sensitive_data(value)
|
||||
for key, value in data.items()
|
||||
}
|
||||
elif isinstance(data, list):
|
||||
return [self._mask_sensitive_data(item) for item in data]
|
||||
|
||||
return data
|
||||
Reference in New Issue
Block a user