import logging import os import sys import time from datetime import timedelta from pathlib import Path from loguru import logger import config as cfg try: from core.cache_config import ( ERROR_THROTTLE_MAX_KEYS, ERROR_THROTTLE_MESSAGE_MAX_LEN, ERROR_THROTTLE_WINDOW_SEC, ) except ImportError: ERROR_THROTTLE_WINDOW_SEC = 60 ERROR_THROTTLE_MAX_KEYS = 500 ERROR_THROTTLE_MESSAGE_MAX_LEN = 120 LEVELS = { "critical": 50, "error": 40, "warning": 30, "info": 20, "debug": 10, "notset": 0, } PANEL_XUI = "[3x-ui]" PANEL_REMNA = "[Remnawave]" CLOGGER = logger.opt(colors=True) def _lvl(v, default="info"): if isinstance(v, int): return v if isinstance(v, str): for tok in v.replace(",", " ").split(): t = tok.strip().lower() if t in LEVELS: return LEVELS[t] return LEVELS[default] BASE_LEVEL = _lvl(getattr(cfg, "LOGGING_LEVEL", getattr(cfg, "LOG_LEVEL", "info"))) LOG_ROTATION_TIME = getattr(cfg, "LOG_ROTATION_TIME", "1 day") log_folder = "logs" os.makedirs(log_folder, exist_ok=True) logger.remove() logger.configure( patcher=lambda r: r["extra"].update( module_tag=( f"[MODULE:{Path(r['file'].path).parts[Path(r['file'].path).parts.index('modules') + 1]}]" if "modules" in Path(r["file"].path).parts else "" ) ) ) level_mapping = {50: "CRITICAL", 40: "ERROR", 30: "WARNING", 20: "INFO", 10: "DEBUG", 0: "NOTSET"} class InterceptHandler(logging.Handler): def emit(self, record): message = record.getMessage() if ( record.name.startswith("aiohttp.") and "Invalid method encountered" in message and "b'\\x16\\x03\\x01'" in message ): logger.opt(depth=6).warning("[HTTP] На порт пришли TLS/HTTPS данные вместо HTTP, соединение закрыто") return logger.opt(depth=6, exception=record.exc_info).log(level_mapping.get(record.levelno, "INFO"), message) logging.basicConfig(handlers=[InterceptHandler()], level=0) for name in ( "httpcore", "httpx", "apscheduler", "apscheduler.executors.default", "apscheduler.scheduler", "async_api_base", "async_api", "async_api_client", ): lg = logging.getLogger(name) lg.setLevel(logging.ERROR) lg.propagate = False _EXCLUDE = {"async_api_base", "async_api", "async_api_client"} _error_throttle = {} def _error_throttle_key(record): msg = record.get("message", "") if isinstance(msg, str): first_line = msg.split("\n")[0].strip()[:ERROR_THROTTLE_MESSAGE_MAX_LEN] else: first_line = str(msg)[:ERROR_THROTTLE_MESSAGE_MAX_LEN] return (record.get("module", ""), record.get("function", ""), first_line) def _error_throttle_prune(): if len(_error_throttle) <= ERROR_THROTTLE_MAX_KEYS: return time.monotonic() by_ts = [(v[0], k) for k, v in _error_throttle.items()] by_ts.sort() for _, k in by_ts[: len(_error_throttle) - ERROR_THROTTLE_MAX_KEYS]: _error_throttle.pop(k, None) def _filter(record): if record.get("name") in _EXCLUDE or record.get("module") in _EXCLUDE: return False level_no = getattr(record.get("level"), "no", 20) if level_no < 40: return True key = _error_throttle_key(record) now = time.monotonic() if key in _error_throttle: first_ts, count = _error_throttle[key] if now - first_ts < ERROR_THROTTLE_WINDOW_SEC: _error_throttle[key] = (first_ts, count + 1) return False if count > 0: suffix = f" (повторялась {count} раз за последние {int(ERROR_THROTTLE_WINDOW_SEC)} сек)" record["message"] = record["message"] + suffix _error_throttle[key] = (now, 0) else: _error_throttle_prune() _error_throttle[key] = (now, 0) return True logger.add( sys.stderr, level=BASE_LEVEL, format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {module}:{function}:{line} | {extra[module_tag]} {message}", colorize=True, filter=_filter, ) log_file_path = os.path.join(log_folder, "logging.log") logger.add( log_file_path, level=BASE_LEVEL, format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {module}:{function}:{line} | {extra[module_tag]} {message}", rotation=LOG_ROTATION_TIME, retention=timedelta(days=3), filter=_filter, ) logger = logger