feat(core): add core module with mixins, services, and background jobs

- Add Model Mixins: TimestampMixin, SoftDeleteMixin, AuditMixin, etc.
- Add Base Services: BaseService, BulkOperationsMixin, QueryOptimizerMixin
- Add Base ViewSets with bulk operations
- Add BackgroundJob model for Celery task tracking
- Add BaseAppCommand for management commands
- Add permissions, pagination, filters, cache, logging
- Migrate tests to factory_boy + faker
- Add CHANGELOG.md
- 297 tests passing
This commit is contained in:
2026-01-21 11:47:26 +01:00
parent 06b30fca02
commit f121445313
72 changed files with 9258 additions and 594 deletions

287
src/apps/core/logging.py Normal file
View File

@@ -0,0 +1,287 @@
"""
Настройка структурированного логирования.
Предоставляет JSON-форматтер и утилиты для production логов.
"""
import json
import logging
import traceback
from datetime import UTC, datetime
from typing import Any
from apps.core.middleware import get_request_id
class JSONFormatter(logging.Formatter):
"""
Форматтер логов в JSON формате.
Формирует структурированные логи для удобного парсинга
в системах мониторинга (ELK, Grafana Loki, etc.).
Пример вывода:
{
"timestamp": "2024-01-15T10:30:45.123456Z",
"level": "INFO",
"logger": "apps.user.services",
"message": "User created",
"request_id": "abc-123",
"user_id": 42,
"extra": {"email": "user@example.com"}
}
"""
def format(self, record: logging.LogRecord) -> str:
"""Форматирует запись лога в JSON."""
log_data: dict[str, Any] = {
"timestamp": datetime.now(UTC).isoformat(),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
}
# Добавляем request_id если доступен
request_id = get_request_id()
if request_id:
log_data["request_id"] = request_id
# Добавляем информацию о месте вызова
log_data["location"] = {
"file": record.filename,
"line": record.lineno,
"function": record.funcName,
}
# Добавляем extra данные
extra_fields = {}
for key, value in record.__dict__.items():
if key not in {
"name",
"msg",
"args",
"created",
"filename",
"funcName",
"levelname",
"levelno",
"lineno",
"module",
"msecs",
"pathname",
"process",
"processName",
"relativeCreated",
"stack_info",
"exc_info",
"exc_text",
"thread",
"threadName",
"message",
"taskName",
}:
extra_fields[key] = value
if extra_fields:
log_data["extra"] = extra_fields
# Добавляем информацию об исключении
if record.exc_info:
log_data["exception"] = {
"type": record.exc_info[0].__name__ if record.exc_info[0] else None,
"message": str(record.exc_info[1]) if record.exc_info[1] else None,
"traceback": traceback.format_exception(*record.exc_info),
}
return json.dumps(log_data, ensure_ascii=False, default=str)
class ContextLogger:
"""
Логгер с автоматическим добавлением контекста.
Пример использования:
logger = ContextLogger(__name__)
logger.set_context(user_id=42, action="login")
logger.info("User logged in") # Автоматически добавит user_id и action
"""
def __init__(self, name: str):
self._logger = logging.getLogger(name)
self._context: dict[str, Any] = {}
def set_context(self, **kwargs: Any) -> None:
"""Устанавливает контекст для всех последующих логов."""
self._context.update(kwargs)
def clear_context(self) -> None:
"""Очищает контекст."""
self._context.clear()
def _log(
self,
level: int,
message: str,
*args: Any,
exc_info: bool = False,
**kwargs: Any,
) -> None:
"""Логирует сообщение с контекстом."""
extra = {**self._context, **kwargs.pop("extra", {})}
self._logger.log(
level, message, *args, extra=extra, exc_info=exc_info, **kwargs
)
def debug(self, message: str, *args: Any, **kwargs: Any) -> None:
"""Логирует DEBUG сообщение."""
self._log(logging.DEBUG, message, *args, **kwargs)
def info(self, message: str, *args: Any, **kwargs: Any) -> None:
"""Логирует INFO сообщение."""
self._log(logging.INFO, message, *args, **kwargs)
def warning(self, message: str, *args: Any, **kwargs: Any) -> None:
"""Логирует WARNING сообщение."""
self._log(logging.WARNING, message, *args, **kwargs)
def error(
self, message: str, *args: Any, exc_info: bool = True, **kwargs: Any
) -> None:
"""Логирует ERROR сообщение."""
self._log(logging.ERROR, message, *args, exc_info=exc_info, **kwargs)
def exception(self, message: str, *args: Any, **kwargs: Any) -> None:
"""Логирует исключение."""
self._log(logging.ERROR, message, *args, exc_info=True, **kwargs)
def get_json_logging_config(
log_level: str = "INFO",
log_file: str | None = None,
) -> dict[str, Any]:
"""
Возвращает конфигурацию логирования для production.
Args:
log_level: Уровень логирования
log_file: Путь к файлу логов (опционально)
Пример использования в settings.py:
from apps.core.logging import get_json_logging_config
LOGGING = get_json_logging_config(
log_level="INFO",
log_file="/var/log/app/app.log",
)
"""
handlers = {
"console": {
"class": "logging.StreamHandler",
"formatter": "json",
},
}
root_handlers = ["console"]
if log_file:
handlers["file"] = {
"class": "logging.handlers.RotatingFileHandler",
"filename": log_file,
"maxBytes": 10 * 1024 * 1024, # 10 MB
"backupCount": 5,
"formatter": "json",
}
root_handlers.append("file")
return {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"json": {
"()": "apps.core.logging.JSONFormatter",
},
"standard": {
"format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
},
},
"handlers": handlers,
"root": {
"handlers": root_handlers,
"level": log_level,
},
"loggers": {
"django": {
"handlers": root_handlers,
"level": "WARNING",
"propagate": False,
},
"django.request": {
"handlers": root_handlers,
"level": "WARNING",
"propagate": False,
},
"celery": {
"handlers": root_handlers,
"level": "INFO",
"propagate": False,
},
"apps": {
"handlers": root_handlers,
"level": log_level,
"propagate": False,
},
},
}
def log_request(
logger: logging.Logger,
request: Any,
response: Any | None = None,
duration_ms: float | None = None,
) -> None:
"""
Логирует HTTP запрос/ответ.
Пример использования:
from apps.core.logging import log_request
def my_middleware(get_response):
def middleware(request):
start = time.time()
response = get_response(request)
duration = (time.time() - start) * 1000
log_request(logger, request, response, duration)
return response
return middleware
"""
extra: dict[str, Any] = {
"method": request.method,
"path": request.path,
"user_id": getattr(request.user, "id", None)
if hasattr(request, "user")
else None,
}
if response:
extra["status_code"] = response.status_code
if duration_ms:
extra["duration_ms"] = round(duration_ms, 2)
request_id = get_request_id()
if request_id:
extra["request_id"] = request_id
message = f"{request.method} {request.path}"
if response:
message += f" -> {response.status_code}"
if duration_ms:
message += f" ({duration_ms:.0f}ms)"
if response and response.status_code >= 500:
logger.error(message, extra=extra)
elif response and response.status_code >= 400:
logger.warning(message, extra=extra)
else:
logger.info(message, extra=extra)