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:
287
src/apps/core/logging.py
Normal file
287
src/apps/core/logging.py
Normal 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)
|
||||
Reference in New Issue
Block a user