"""
Кастомный логгер с поддержкой декораторов и прямого вызова
"""
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] = (
'{time:YYYY-MM-DD HH:mm:ss.SSS} | '
'{extra[system]}-{extra[log_type]} | '
'{extra[user]} | {message}'
)
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' или '@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