Первый коммит
This commit is contained in:
1
middleware/loggers/__init__.py
Normal file
1
middleware/loggers/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .logs import *
|
||||
364
middleware/loggers/logs.py
Normal file
364
middleware/loggers/logs.py
Normal file
@@ -0,0 +1,364 @@
|
||||
"""
|
||||
Кастомный логгер с поддержкой декораторов и прямого вызова
|
||||
"""
|
||||
from sys import stderr
|
||||
from pathlib import Path
|
||||
from functools import wraps
|
||||
from inspect import iscoroutinefunction
|
||||
from typing import Any, Callable, Optional, TypeVar, Union, cast, Final
|
||||
from contextlib import contextmanager
|
||||
|
||||
from loguru import logger as nlogger
|
||||
from aiogram.types import Message, User, CallbackQuery
|
||||
|
||||
from configs import settings
|
||||
|
||||
# Экспортируемые объекты
|
||||
__all__ = ('Logger', 'setup_logging', 'logger', 'log')
|
||||
|
||||
# Универсальный тип для функций
|
||||
F = TypeVar('F', bound=Callable[..., Any])
|
||||
|
||||
# Типы для aiogram событий
|
||||
EventType = Union[Message, CallbackQuery]
|
||||
|
||||
|
||||
class Logger:
|
||||
"""
|
||||
Кастомный логгер с поддержкой декораторов, прямого вызова и контекстных менеджеров.
|
||||
|
||||
Features:
|
||||
- Декораторы для sync/async функций
|
||||
- Прямой вызов методов (debug, info, warning, error, critical)
|
||||
- Автоматическое извлечение юзера из Message/CallbackQuery
|
||||
- Раздельные файлы логов по уровням
|
||||
- Ротация и retention логов
|
||||
- Контекстные менеджеры для блоков кода
|
||||
"""
|
||||
|
||||
# Формат логов
|
||||
_log_format: Final[str] = (
|
||||
'<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> <red>|</red> '
|
||||
'<blue>{extra[system]}-{extra[log_type]}</blue> <red>|</red> '
|
||||
'<cyan>{extra[user]}</cyan> <red>|</red> <level>{message}</level>'
|
||||
)
|
||||
|
||||
|
||||
def __init__(self, system_name: str = 'PRIMO') -> None:
|
||||
"""
|
||||
Инициализация логгера.
|
||||
|
||||
Args:
|
||||
system_name: Имя системы для логирования (по умолчанию из settings)
|
||||
"""
|
||||
self.system_name = system_name or settings.PROJECT_NAME
|
||||
self._setup_done = False
|
||||
|
||||
def setup(self, start: bool = True) -> None:
|
||||
"""
|
||||
Настройка обработчиков Loguru: консоль и файлы.
|
||||
|
||||
Args:
|
||||
start: Если True, сразу логирует запуск проекта
|
||||
"""
|
||||
if self._setup_done:
|
||||
return
|
||||
|
||||
# Полная очистка настроек
|
||||
nlogger.remove()
|
||||
|
||||
# Создание директории для файловых логов
|
||||
log_dir: Path = settings.LOG_DIR
|
||||
if not log_dir.exists():
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Консольный лог
|
||||
if settings.LOG_CONSOLE:
|
||||
nlogger.add(
|
||||
sink=stderr,
|
||||
format=self._log_format,
|
||||
colorize=True,
|
||||
level='INFO',
|
||||
filter=lambda rec: rec['extra'].get('log_type') != 'TRACE'
|
||||
)
|
||||
|
||||
# Файловые логи
|
||||
if settings.LOG_FILE:
|
||||
# Общий лог со всеми уровнями
|
||||
nlogger.add(
|
||||
sink=log_dir / 'bot.log',
|
||||
rotation=settings.LOG_ROTATION,
|
||||
retention=settings.LOG_RETENTION,
|
||||
format=self._log_format,
|
||||
level='DEBUG',
|
||||
enqueue=True,
|
||||
backtrace=True,
|
||||
diagnose=True,
|
||||
encoding='utf-8'
|
||||
)
|
||||
|
||||
# Раздельные логи по уровням
|
||||
log_levels = {
|
||||
'INFO': 'info.log',
|
||||
'WARNING': 'warning.log',
|
||||
'ERROR': 'error.log',
|
||||
'CRITICAL': 'critical.log'
|
||||
}
|
||||
|
||||
|
||||
for level_name, filename in log_levels.items():
|
||||
nlogger.add(
|
||||
sink=log_dir / filename,
|
||||
rotation='10 MB',
|
||||
retention=settings.LOG_RETENTION,
|
||||
format=self._log_format,
|
||||
level=level_name,
|
||||
filter=lambda rec, lvl=level_name: rec['level'].name == lvl,
|
||||
enqueue=True,
|
||||
encoding='utf-8'
|
||||
)
|
||||
|
||||
self._setup_done = True
|
||||
|
||||
# Логируем старт
|
||||
if start:
|
||||
self.log_entry(
|
||||
level='INFO',
|
||||
text=f'Запуск проекта {self.system_name}...',
|
||||
log_type='START'
|
||||
)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def format_user(event: Optional[EventType] = None) -> str:
|
||||
"""
|
||||
Форматирует имя пользователя из объекта Message или CallbackQuery.
|
||||
|
||||
Args:
|
||||
event: Объект Message или CallbackQuery
|
||||
|
||||
Returns:
|
||||
str: Строка '@username' или 'id<user_id>' или '@System'
|
||||
"""
|
||||
if event is None:
|
||||
return '@System'
|
||||
|
||||
# Извлекаем пользователя из Message или CallbackQuery
|
||||
user: Optional[User] = None
|
||||
if isinstance(event, Message):
|
||||
user = event.from_user
|
||||
elif isinstance(event, CallbackQuery):
|
||||
user = event.from_user
|
||||
|
||||
if user is None:
|
||||
return '@System'
|
||||
|
||||
return f"@{user.username}" if user.username else f"id{user.id}"
|
||||
|
||||
def log_entry(
|
||||
self,
|
||||
level: str,
|
||||
text: str,
|
||||
log_type: str = 'SYSTEM',
|
||||
user: Optional[str] = None,
|
||||
message: Optional[EventType] = None
|
||||
) -> None:
|
||||
"""
|
||||
Основной метод для записи логов.
|
||||
|
||||
Args:
|
||||
level: Уровень логирования ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL')
|
||||
text: Сообщение для логирования
|
||||
log_type: Кастомный тип лога (например, 'HANDLER', 'COMMAND', 'SPAM')
|
||||
user: Явно указанный пользователь (если None, извлекается из message)
|
||||
message: Объект Message/CallbackQuery для извлечения юзера
|
||||
"""
|
||||
actual_user: str = user or self.format_user(message)
|
||||
nlogger.bind(
|
||||
system=self.system_name,
|
||||
user=actual_user,
|
||||
log_type=log_type
|
||||
).log(level, text)
|
||||
|
||||
def log(
|
||||
self,
|
||||
level: str = 'INFO',
|
||||
log_type: str = 'HANDLER',
|
||||
text: Optional[str] = None
|
||||
) -> Callable[[F], F]:
|
||||
"""
|
||||
Декоратор для логирования функций (sync и async).
|
||||
|
||||
Args:
|
||||
level: Уровень логирования
|
||||
log_type: Категория лога
|
||||
text: Кастомный текст сообщения (если None, используется имя функции)
|
||||
|
||||
Returns:
|
||||
Декорированную функцию
|
||||
|
||||
Example:
|
||||
```python
|
||||
@logger.log(level='INFO', log_type='COMMAND', text='Команда /start')
|
||||
async def start_handler(message: Message):
|
||||
await message.answer("Привет!")
|
||||
```
|
||||
"""
|
||||
|
||||
def decorator(func: F) -> F:
|
||||
is_coroutine = iscoroutinefunction(func)
|
||||
action_text = text or f'Вызов {func.__name__}'
|
||||
|
||||
@wraps(func)
|
||||
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||
message = self._find_event(args)
|
||||
self.log_entry(level, f"[▶] {action_text}", log_type, message=message)
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
self.log_entry(level, f"[✓] {action_text}", log_type, message=message)
|
||||
return result
|
||||
except Exception as e:
|
||||
self.log_entry(
|
||||
'ERROR',
|
||||
f"[✗] {action_text} | Exception: {e!r}",
|
||||
log_type,
|
||||
message=message
|
||||
)
|
||||
raise
|
||||
|
||||
@wraps(func)
|
||||
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||
message = self._find_event(args)
|
||||
self.log_entry(level, f"[▶] {action_text}", log_type, message=message)
|
||||
try:
|
||||
result = await func(*args, **kwargs)
|
||||
self.log_entry(level, f"[✓] {action_text}", log_type, message=message)
|
||||
return result
|
||||
except Exception as e:
|
||||
self.log_entry(
|
||||
'ERROR',
|
||||
f"[✗] {action_text} | Exception: {e!r}",
|
||||
log_type,
|
||||
message=message
|
||||
)
|
||||
raise
|
||||
|
||||
return cast(F, async_wrapper if is_coroutine else sync_wrapper)
|
||||
|
||||
return decorator
|
||||
|
||||
@staticmethod
|
||||
def _find_event(args: tuple[Any, ...]) -> Optional[EventType]:
|
||||
"""
|
||||
Ищет объект Message или CallbackQuery в аргументах функции.
|
||||
|
||||
Args:
|
||||
args: Аргументы функции
|
||||
|
||||
Returns:
|
||||
Найденный Message/CallbackQuery или None
|
||||
"""
|
||||
for arg in args:
|
||||
if isinstance(arg, (Message, CallbackQuery)):
|
||||
return arg
|
||||
return None
|
||||
|
||||
# ================= МЕТОДЫ ДЛЯ ПРЯМОГО ВЫЗОВА =================
|
||||
|
||||
def debug(
|
||||
self,
|
||||
text: str,
|
||||
log_type: str = 'DEBUG',
|
||||
user: Optional[str] = None,
|
||||
message: Optional[EventType] = None
|
||||
) -> None:
|
||||
"""Логирование уровня DEBUG"""
|
||||
self.log_entry('DEBUG', text, log_type, user, message)
|
||||
|
||||
def info(
|
||||
self,
|
||||
text: str,
|
||||
log_type: str = 'INFO',
|
||||
user: Optional[str] = None,
|
||||
message: Optional[EventType] = None
|
||||
) -> None:
|
||||
"""Логирование уровня INFO"""
|
||||
self.log_entry('INFO', text, log_type, user, message)
|
||||
|
||||
def warning(
|
||||
self,
|
||||
text: str,
|
||||
log_type: str = 'WARNING',
|
||||
user: Optional[str] = None,
|
||||
message: Optional[EventType] = None
|
||||
) -> None:
|
||||
"""Логирование уровня WARNING"""
|
||||
self.log_entry('WARNING', text, log_type, user, message)
|
||||
|
||||
def error(
|
||||
self,
|
||||
text: str,
|
||||
log_type: str = 'ERROR',
|
||||
user: Optional[str] = None,
|
||||
message: Optional[EventType] = None
|
||||
) -> None:
|
||||
"""Логирование уровня ERROR"""
|
||||
self.log_entry('ERROR', text, log_type, user, message)
|
||||
|
||||
def critical(
|
||||
self,
|
||||
text: str,
|
||||
log_type: str = 'CRITICAL',
|
||||
user: Optional[str] = None,
|
||||
message: Optional[EventType] = None
|
||||
) -> None:
|
||||
"""Логирование уровня CRITICAL"""
|
||||
self.log_entry('CRITICAL', text, log_type, user, message)
|
||||
|
||||
def success(
|
||||
self,
|
||||
text: str,
|
||||
log_type: str = 'SUCCESS',
|
||||
user: Optional[str] = None,
|
||||
message: Optional[EventType] = None
|
||||
) -> None:
|
||||
"""Логирование успешного выполнения (уровень INFO)"""
|
||||
self.log_entry('INFO', f"✓ {text}", log_type, user, message)
|
||||
|
||||
# ================= КОНТЕКСТНЫЕ МЕНЕДЖЕРЫ =================
|
||||
|
||||
@contextmanager
|
||||
def log_context(
|
||||
self,
|
||||
action: str,
|
||||
log_type: str = 'CONTEXT',
|
||||
level: str = 'INFO',
|
||||
message: Optional[EventType] = None
|
||||
):
|
||||
"""
|
||||
Контекстный менеджер для логирования блока кода.
|
||||
|
||||
Example:
|
||||
```python
|
||||
with logger.log_context("Обработка данных", log_type='DATA'):
|
||||
# ... ваш код ...
|
||||
pass
|
||||
```
|
||||
"""
|
||||
self.log_entry(level, f"[▶] {action}", log_type, message=message)
|
||||
try:
|
||||
yield
|
||||
self.log_entry(level, f"[✓] {action}", log_type, message=message)
|
||||
except Exception as e:
|
||||
self.log_entry('ERROR', f"[✗] {action} | Exception: {e!r}", log_type, message=message)
|
||||
raise
|
||||
|
||||
|
||||
# ================= ГЛОБАЛЬНЫЙ ЭКЗЕМПЛЯР =================
|
||||
|
||||
# Создаем глобальный экземпляр логгера
|
||||
logger: Logger = Logger(system_name="PRIMO")
|
||||
|
||||
# Алиасы для обратной совместимости
|
||||
setup_logging = logger.setup
|
||||
log = logger.log
|
||||
Reference in New Issue
Block a user