Files
Steam-Panel/app/core/logging.py
T
Manchik 9cce9b5b36 v3.0.6
2026-05-15 17:17:42 +03:00

139 lines
3.9 KiB
Python

import logging
import re
import sys
from loguru import logger
from app.config import settings
LEVEL_ICONS = {
"TRACE": "[~]",
"DEBUG": "[.]",
"INFO": "[i]",
"SUCCESS": "[+]",
"WARNING": "[!]",
"ERROR": "[e]",
"CRITICAL": "[e]",
}
LEVEL_COLORS = {
"TRACE": "\033[35m",
"DEBUG": "\033[37m",
"INFO": "\033[34m",
"SUCCESS": "\033[32m",
"WARNING": "\033[33m",
"ERROR": "\033[31m",
"CRITICAL": "\033[31m",
}
RESET = "\033[0m"
def _loguru_fmt(record) -> str: # type: ignore[override]
level = record["level"].name
icon = LEVEL_ICONS.get(level, "[i]")
color = LEVEL_COLORS.get(level, "")
ts = record["time"].strftime("%H:%M:%S")
msg = record["message"].replace("<", "\\<").replace("{", "{{").replace("}", "}}")
return f"{RESET}[{ts}] {color}{icon}{RESET} {msg}\n"
class _UvicornInterceptHandler(logging.Handler):
"""Route all standard logging records (uvicorn, fastapi) into loguru."""
def emit(self, record: logging.LogRecord) -> None:
try:
level = logger.level(record.levelname).name
except ValueError:
level = record.levelno
frame, depth = sys._getframe(6), 6
while frame and frame.f_code.co_filename == logging.__file__:
frame = frame.f_back
depth += 1
logger.opt(depth=depth, exception=record.exc_info).log(
level, record.getMessage()
)
_logging_configured = False
def setup_logging() -> None:
global _logging_configured
if _logging_configured:
return
_logging_configured = True
logger.remove()
logger.add(
sys.stderr,
format=_loguru_fmt,
level="DEBUG" if settings.debug else "INFO",
colorize=False,
)
settings.logs_dir.mkdir(parents=True, exist_ok=True)
def _file_fmt(record) -> str: # type: ignore[override]
level = record["level"].name
icon = LEVEL_ICONS.get(level, "[i]")
ts = record["time"].strftime("%Y-%m-%d %H:%M:%S")
msg = (
record["message"].replace("<", "\\<").replace("{", "{{").replace("}", "}}")
)
return f"[{ts}] {icon} {msg}\n"
logger.add(
settings.logs_dir / "steampanel_{time:YYYY-MM-DD}.log",
format=_file_fmt,
level="DEBUG",
rotation="1 day",
retention="7 days",
compression="zip",
)
_install_intercept()
class _AioSQLiteFilter(logging.Filter):
"""Reformats raw aiosqlite debug messages into readable DB operation lines."""
_OP_RE = re.compile(r"method (\w+) of sqlite3\.(\w+)")
_SQL_RE = re.compile(r',\s*"(.*?)"(?:,\s*(\(.*?\)))?\s*\)$', re.DOTALL)
def filter(self, record: logging.LogRecord) -> bool:
msg = record.getMessage()
# Drop the redundant "operation X completed" lines
if " completed" in msg:
return False
if msg.startswith("executing"):
op_m = self._OP_RE.search(msg)
method = op_m.group(1).upper() if op_m else "OP"
sql_m = self._SQL_RE.search(msg)
if sql_m:
sql = sql_m.group(1).strip()
params = sql_m.group(2) or ""
record.msg = f"[DB] {sql}" + (f" {params}" if params else "")
else:
record.msg = f"[DB] {method}"
record.args = ()
return True
def _install_intercept() -> None:
intercept = _UvicornInterceptHandler()
for name in ("", "uvicorn", "uvicorn.error", "uvicorn.access", "fastapi"):
log = logging.getLogger(name)
log.handlers = [intercept]
log.setLevel(logging.DEBUG)
log.propagate = False
# Format aiosqlite SQL logs instead of silencing them
aiosqlite_log = logging.getLogger("aiosqlite")
aiosqlite_log.setLevel(logging.DEBUG)
aiosqlite_log.addFilter(_AioSQLiteFilter())
logging.getLogger("asyncio").setLevel(logging.WARNING)