from pathlib import Path from functools import wraps from sys import stderr as console from inspect import iscoroutinefunction from typing import Any, Callable, Optional, TypeVar, cast, Final from loguru import logger as logs from configs.config import settings # экземпляр настроек __all__ = ("logger", "log") F = TypeVar("F", bound=Callable[..., Any]) class _Logger: """ Обёртка над loguru с: - единым форматом сообщений; - выводом в консоль; - файлами в ./logs: - logs/bot.log — все уровни (DEBUG+) - logs/debug.log — только DEBUG - logs/info.log — только INFO - logs/warning.log — только WARNING - logs/error.log — только ERROR - logs/critical.log — только CRITICAL """ _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 = "DISCORD_BOT") -> None: self.system_name: str = system_name self._setup_done: bool = False def setup(self, start: bool = True) -> None: """ Настроить loguru: консоль + файлы в каталоге logs/. Вызывать один раз при старте приложения. """ if self._setup_done: return logs.remove() # Директория для логов log_dir: Path = Path("logs") log_dir.mkdir(parents=True, exist_ok=True) # Консольный вывод logs.add( sink=console, format=self._log_format, colorize=True, level=settings.LOG_LEVEL.upper(), ) # Общий лог (все уровни) logs.add( sink=log_dir / "bot.log", rotation="100 MB", retention="7 days", format=self._log_format, level="DEBUG", enqueue=True, backtrace=True, diagnose=True, ) # Отдельные файлы по уровням for level_name in ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"): logs.add( sink=log_dir / f"{level_name.lower()}.log", rotation="10 MB", retention="7 days", format=self._log_format, level=level_name, filter=lambda rec, lvl=level_name: rec["level"].name == lvl, enqueue=True, ) self._setup_done = True if start: self.log_entry( level="INFO", text="Запуск Discord‑бота...", log_type="START", ) @staticmethod def format_user(user: Optional[str] = None) -> str: """ Вернуть строку пользователя для логов. """ return user or "@System" def log_entry( self, level: str, text: str, log_type: str = "BOT", user: Optional[str] = None, ) -> None: """ Записать строку лога. :param level: Уровень (DEBUG, INFO, WARNING, ERROR, CRITICAL). :param text: Текст сообщения. :param log_type: Категория/подсистема (например, SYSTEM, COGS, DB). :param user: Опционально — строка пользователя. """ actual_user: str = self.format_user(user) logs.bind( system=self.system_name, user=actual_user, log_type=log_type, ).log(level, text) def log( self, level: str = "INFO", log_type: str = "", text: Optional[str] = None, ) -> Callable[[F], F]: """ Декоратор для логирования вызовов функций/корутин. :param level: Уровень сообщения. :param log_type: Категория (например, HANDLER, TASK). :param text: Кастомный текст (по умолчанию 'Вызов <имя функции>'). """ def decorator(func: F) -> F: is_coroutine: bool = iscoroutinefunction(func) action_text: str = text or f"Вызов {func.__name__}" @wraps(func) def sync_wrapper(*args: Any, **kwargs: Any) -> Any: self.log_entry(level=level, text=f"[START] {action_text}", log_type=log_type) try: result: Any = func(*args, **kwargs) self.log_entry(level=level, text=f"[SUCCESS] {action_text}", log_type=log_type) return result except Exception as e: self.log_entry( level="ERROR", text=f"[ERROR] {action_text} | Exception: {e!r}", log_type=log_type, ) raise @wraps(func) async def async_wrapper(*args: Any, **kwargs: Any) -> Any: self.log_entry(level=level, text=f"[START] {action_text}", log_type=log_type) try: result: Any = await func(*args, **kwargs) self.log_entry(level=level, text=f"[SUCCESS] {action_text}", log_type=log_type) return result except Exception as e: self.log_entry( level="ERROR", text=f"[ERROR] {action_text} | Exception: {e!r}", log_type=log_type, ) raise return cast(F, async_wrapper if is_coroutine else sync_wrapper) return decorator def debug(self, text: str, log_type: str = "BOT", user: Optional[str] = None) -> None: self.log_entry(level="DEBUG", text=text, log_type=log_type, user=user) def info(self, text: str, log_type: str = "BOT", user: Optional[str] = None) -> None: self.log_entry(level="INFO", text=text, log_type=log_type, user=user) def warning(self, text: str, log_type: str = "BOT", user: Optional[str] = None) -> None: self.log_entry(level="WARNING", text=text, log_type=log_type, user=user) def error(self, text: str, log_type: str = "BOT", user: Optional[str] = None) -> None: self.log_entry(level="ERROR", text=text, log_type=log_type, user=user) def critical(self, text: str, log_type: str = "BOT", user: Optional[str] = None) -> None: self.log_entry(level="CRITICAL", text=text, log_type=log_type, user=user) # Глобальный экземпляр logger: _Logger = _Logger(system_name="DISCORD_BOT") log = logger.log