Добавлен средний слой с логированием данных
This commit is contained in:
@@ -30,12 +30,12 @@ RACK_TEST_DATABASE='/tmp/rack/rack.sqlite'
|
|||||||
#
|
#
|
||||||
# KeyDb Database
|
# KeyDb Database
|
||||||
#
|
#
|
||||||
USE_KEYDB=1
|
RACK_USE_KEYDB=1
|
||||||
KEYDB_HOST='127.0.0.1'
|
RACK_KEYDB_HOST='127.0.0.1'
|
||||||
KEYDB_PORT=6379
|
RACK_KEYDB_PORT=6379
|
||||||
KEYDB_DBINDEX=0
|
RACK_KEYDB_DBINDEX=0
|
||||||
KEYDB_PASSWORD='<change-me>'
|
RACK_KEYDB_PASSWORD='<change-me>'
|
||||||
KEYDB_POOL_SIZE=4
|
RACK_KEYDB_POOL_SIZE=4
|
||||||
|
|
||||||
#
|
#
|
||||||
# S3 Object Storage
|
# S3 Object Storage
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ services:
|
|||||||
env_file :
|
env_file :
|
||||||
- .env
|
- .env
|
||||||
environment :
|
environment :
|
||||||
POSTGRES_DB : ${rack_DATABASE_NAME}
|
POSTGRES_DB : ${RACK_DATABASE_NAME}
|
||||||
POSTGRES_USER : ${rack_DATABASE_USERNAME}
|
POSTGRES_USER : ${RACK_DATABASE_USERNAME}
|
||||||
POSTGRES_PASSWORD: ${rack_DATABASE_PASSWORD}
|
POSTGRES_PASSWORD: ${RACK_DATABASE_PASSWORD}
|
||||||
ports :
|
ports :
|
||||||
- "${rack_DATABASE_PORT:-5432}:5432"
|
- "${rack_DATABASE_PORT:-5432}:5432"
|
||||||
volumes :
|
volumes :
|
||||||
@@ -29,7 +29,7 @@ services:
|
|||||||
env_file :
|
env_file :
|
||||||
- .env
|
- .env
|
||||||
ports :
|
ports :
|
||||||
- "${rack_KEYDB_PORT:-6379}:6379"
|
- "${RACK_KEYDB_PORT:-6379}:6379"
|
||||||
command : keydb-server --protected-mode no --save ""
|
command : keydb-server --protected-mode no --save ""
|
||||||
volumes :
|
volumes :
|
||||||
- keydb_data:/data
|
- keydb_data:/data
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"""
|
"""
|
||||||
Описание класса AccountManager
|
Описание класса AccountManager
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from net.xeaf.rack.enums import AuthRejectReason
|
from net.xeaf.rack.enums import AuthRejectReason
|
||||||
from net.xeaf.rack.models.account_model import AccountModel
|
from net.xeaf.rack.models.account_model import AccountModel
|
||||||
|
|
||||||
|
|||||||
@@ -9,4 +9,5 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from .custom_exception_handler import custom_exception_handler
|
from .custom_exception_handler import custom_exception_handler
|
||||||
|
from .custom_logging_middleware import CustomLoggingMiddleware
|
||||||
from .expiring_token_authentication import ExpiringTokenAuthentication
|
from .expiring_token_authentication import ExpiringTokenAuthentication
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
Описание функции custom_exception_handler
|
Описание функции custom_exception_handler
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.exceptions import MethodNotAllowed
|
from rest_framework.exceptions import MethodNotAllowed
|
||||||
from rest_framework.exceptions import NotAuthenticated
|
from rest_framework.exceptions import NotAuthenticated
|
||||||
@@ -21,6 +23,7 @@ from net.xeaf.rack.models.responses import ErrorResponse
|
|||||||
from net.xeaf.rack.utils.exceptions import ForbiddenException
|
from net.xeaf.rack.utils.exceptions import ForbiddenException
|
||||||
from net.xeaf.rack.utils.exceptions import NotFoundException
|
from net.xeaf.rack.utils.exceptions import NotFoundException
|
||||||
from net.xeaf.rack.utils.exceptions import UnauthorizedException
|
from net.xeaf.rack.utils.exceptions import UnauthorizedException
|
||||||
|
from wsgiapp import settings
|
||||||
|
|
||||||
|
|
||||||
def custom_exception_handler(exc: CoreException | Exception, context: dict) -> Response:
|
def custom_exception_handler(exc: CoreException | Exception, context: dict) -> Response:
|
||||||
@@ -39,7 +42,12 @@ def custom_exception_handler(exc: CoreException | Exception, context: dict) -> R
|
|||||||
|
|
||||||
# Это наше исключение
|
# Это наше исключение
|
||||||
if isinstance(exc, CoreException):
|
if isinstance(exc, CoreException):
|
||||||
return ErrorResponse(status_code=exc.status_code, detail=exc.detail, meta=exc.meta, pure_status=exc.is_pure_status())
|
return _make_error_response(
|
||||||
|
status_code=exc.status_code,
|
||||||
|
detail=exc.detail,
|
||||||
|
meta=exc.meta,
|
||||||
|
pure_status=exc.is_pure_status(),
|
||||||
|
exc_info=exc)
|
||||||
|
|
||||||
# Проверка на стандартное исключение валидации
|
# Проверка на стандартное исключение валидации
|
||||||
if isinstance(exc, ValidationError):
|
if isinstance(exc, ValidationError):
|
||||||
@@ -47,18 +55,38 @@ def custom_exception_handler(exc: CoreException | Exception, context: dict) -> R
|
|||||||
|
|
||||||
# Проверка на стандартное исключение авторизации
|
# Проверка на стандартное исключение авторизации
|
||||||
if isinstance(exc, NotAuthenticated):
|
if isinstance(exc, NotAuthenticated):
|
||||||
return ErrorResponse(status_code=status.HTTP_401_UNAUTHORIZED, detail=UnauthorizedException.Msg.DEFAULT_MESSAGE)
|
return _make_error_response(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=UnauthorizedException.Msg.DEFAULT_MESSAGE,
|
||||||
|
meta=None,
|
||||||
|
pure_status=False,
|
||||||
|
exc_info=exc)
|
||||||
|
|
||||||
# Проверка на стандартное исключение прав доступа
|
# Проверка на стандартное исключение прав доступа
|
||||||
if isinstance(exc, PermissionDenied):
|
if isinstance(exc, PermissionDenied):
|
||||||
return ErrorResponse(status_code=status.HTTP_403_FORBIDDEN, detail=ForbiddenException.Msg.DEFAULT_MESSAGE)
|
return _make_error_response(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail=ForbiddenException.Msg.DEFAULT_MESSAGE,
|
||||||
|
meta=None,
|
||||||
|
pure_status=False,
|
||||||
|
exc_info=exc)
|
||||||
|
|
||||||
# Проверка на стандартное исключение отсутствия реализации
|
# Проверка на стандартное исключение отсутствия реализации
|
||||||
if isinstance(exc, MethodNotAllowed):
|
if isinstance(exc, MethodNotAllowed):
|
||||||
return ErrorResponse(status_code=status.HTTP_404_NOT_FOUND, detail=NotFoundException.Msg.DEFAULT_MESSAGE)
|
return _make_error_response(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=NotFoundException.Msg.DEFAULT_MESSAGE,
|
||||||
|
meta=None,
|
||||||
|
pure_status=True,
|
||||||
|
exc_info=exc)
|
||||||
|
|
||||||
# Если ничего другое не прошло
|
# Если ничего другое не прошло
|
||||||
return ErrorResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc))
|
return _make_error_response(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail=str(exc),
|
||||||
|
meta=None,
|
||||||
|
pure_status=True,
|
||||||
|
exc_info=exc)
|
||||||
|
|
||||||
|
|
||||||
def _process_validation_exception(exc: ValidationError) -> ErrorResponse:
|
def _process_validation_exception(exc: ValidationError) -> ErrorResponse:
|
||||||
@@ -70,18 +98,53 @@ def _process_validation_exception(exc: ValidationError) -> ErrorResponse:
|
|||||||
:return: Ответ
|
:return: Ответ
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Обработка списка
|
||||||
if isinstance(exc.detail, list):
|
if isinstance(exc.detail, list):
|
||||||
if len(exc.detail) == 1:
|
if len(exc.detail) == 1:
|
||||||
detail = str(exc.detail[0])
|
detail = str(exc.detail[0])
|
||||||
else:
|
else:
|
||||||
detail = exc.detail
|
detail = exc.detail
|
||||||
|
|
||||||
|
# Обработка словаря
|
||||||
elif isinstance(exc.detail, dict):
|
elif isinstance(exc.detail, dict):
|
||||||
detail = {}
|
detail = {}
|
||||||
for field, error_info in exc.detail.items():
|
for field, error_info in exc.detail.items():
|
||||||
if isinstance(error_info, list):
|
if isinstance(error_info, list):
|
||||||
error = error_info[0]
|
error = error_info[0]
|
||||||
detail[field] = str(error)
|
detail[field] = str(error)
|
||||||
|
|
||||||
|
# Обработка остальных типов
|
||||||
else:
|
else:
|
||||||
detail = str(exc.detail)
|
detail = str(exc.detail)
|
||||||
|
|
||||||
return ErrorResponse(status_code=status.HTTP_400_BAD_REQUEST, detail=detail)
|
return _make_error_response(status_code=status.HTTP_400_BAD_REQUEST, detail=detail, meta=None, pure_status=False, exc_info=exc)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_error_response(status_code: int, detail: list | dict | str | bool | None, meta: dict | None, pure_status: bool,
|
||||||
|
exc_info: Exception) -> ErrorResponse:
|
||||||
|
"""
|
||||||
|
Создает ответ с ошибкой
|
||||||
|
|
||||||
|
:param status_code: Код статуса
|
||||||
|
:param detail: Детали ошибки
|
||||||
|
:param meta: Дополнительная информация
|
||||||
|
:param pure_status: Признак отправки чистого кода состояния
|
||||||
|
:param exc_info: Исключение
|
||||||
|
|
||||||
|
:return: Ответ
|
||||||
|
"""
|
||||||
|
|
||||||
|
logger = logging.getLogger(settings.LOGGER_NAME)
|
||||||
|
|
||||||
|
if status_code == status.HTTP_400_BAD_REQUEST:
|
||||||
|
logger.debug(str(exc_info), exc_info=exc_info)
|
||||||
|
elif status_code == status.HTTP_401_UNAUTHORIZED:
|
||||||
|
logger.warning(str(exc_info), exc_info=exc_info)
|
||||||
|
elif status_code == status.HTTP_403_FORBIDDEN:
|
||||||
|
logger.warning(str(exc_info), exc_info=exc_info)
|
||||||
|
elif status_code == status.HTTP_404_NOT_FOUND:
|
||||||
|
logger.error(str(exc_info), exc_info=exc_info)
|
||||||
|
else:
|
||||||
|
logger.critical(str(exc_info), exc_info=exc_info)
|
||||||
|
|
||||||
|
return ErrorResponse(status_code=status_code, detail=detail, meta=meta, pure_status=pure_status)
|
||||||
|
|||||||
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
|
||||||
@@ -17,6 +17,11 @@ from net.xeaf.rack.utils.settings.logger import LogDebugFilter
|
|||||||
#
|
#
|
||||||
LOGGER_NAME = Environment.get_str("LOGGER_NAME", "rack")
|
LOGGER_NAME = Environment.get_str("LOGGER_NAME", "rack")
|
||||||
|
|
||||||
|
#
|
||||||
|
# Ключи полей, которые необходимо скрывать в журнале
|
||||||
|
#
|
||||||
|
LOGGER_SENSITIVE_KEYS = Environment.get_str_as_list("LOGGER_SENSITIVE_KEYS", "")
|
||||||
|
|
||||||
#
|
#
|
||||||
# Уровень журналирования
|
# Уровень журналирования
|
||||||
#
|
#
|
||||||
@@ -61,7 +66,7 @@ LOGGING = {
|
|||||||
#
|
#
|
||||||
"main_file": {
|
"main_file": {
|
||||||
"level": "INFO" if LOG_LEVEL == "DEBUG" else LOG_LEVEL,
|
"level": "INFO" if LOG_LEVEL == "DEBUG" else LOG_LEVEL,
|
||||||
"class": "logging.handlers.RotatingFileHandler",
|
"class": "concurrent_log_handler.ConcurrentRotatingFileHandler",
|
||||||
"filename": LOG_FILE,
|
"filename": LOG_FILE,
|
||||||
"maxBytes": 1024 * 1024 * 10,
|
"maxBytes": 1024 * 1024 * 10,
|
||||||
"backupCount": 5,
|
"backupCount": 5,
|
||||||
@@ -70,11 +75,11 @@ LOGGING = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
#
|
#
|
||||||
# Отладка
|
# Лог отладки
|
||||||
#
|
#
|
||||||
"debug_file": {
|
"debug_file": {
|
||||||
"level": "DEBUG",
|
"level": "DEBUG",
|
||||||
"class": "logging.handlers.RotatingFileHandler",
|
"class": "concurrent_log_handler.ConcurrentRotatingFileHandler",
|
||||||
"filename": DEBUG_LOG_FILE,
|
"filename": DEBUG_LOG_FILE,
|
||||||
"maxBytes": 1024 * 1024 * 10,
|
"maxBytes": 1024 * 1024 * 10,
|
||||||
"backupCount": 5,
|
"backupCount": 5,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
'net.xeaf.rack.utils.drf.CustomLoggingMiddleware',
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
|||||||
Reference in New Issue
Block a user