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
now = 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