Sanad_lite/core/logger.py

91 lines
2.7 KiB
Python

"""Unified logging with RotatingFileHandler for all Sanad modules."""
from __future__ import annotations
import io
import logging
import sys
from logging.handlers import RotatingFileHandler
from pathlib import Path
def _ascii_safe_stream(stream):
"""Wrap an ASCII-only stream so non-ASCII chars don't crash logging.
cPanel / LiteSpeed often hands Python a stream with `errors='strict'` and
encoding='ANSI_X3.4-1968'. Any em-dash in a log message then raises
UnicodeEncodeError and dumps a "--- Logging error ---" block into
stderr.log. This wrapper reopens the stream's buffer with errors='replace'
so unencodable chars become '?' instead of crashing.
"""
enc = (getattr(stream, "encoding", None) or "").lower()
if "utf" in enc:
return stream # already Unicode-safe
buf = getattr(stream, "buffer", None)
if buf is None:
return stream
try:
return io.TextIOWrapper(buf, encoding="utf-8", errors="replace",
line_buffering=True, write_through=True)
except Exception:
return stream
from Project.Sanad.config import LOGS_DIR
_MAX_BYTES = 10 * 1024 * 1024 # 10 MB
_BACKUP_COUNT = 3
_FMT = "%(asctime)s [%(name)s] %(levelname)s %(message)s"
_formatter = logging.Formatter(_FMT)
# Callback for the WebSocket log stream — set by log_stream.py at import time.
_ws_push_fn = None
def set_ws_push(fn):
"""Register the push function from dashboard.websockets.log_stream."""
global _ws_push_fn
_ws_push_fn = fn
class _WSHandler(logging.Handler):
"""Forwards every log record to the WebSocket log stream."""
def emit(self, record: logging.LogRecord):
if _ws_push_fn is not None:
try:
_ws_push_fn(self.format(record))
except Exception:
pass
def get_logger(name: str, *, to_console: bool = True) -> logging.Logger:
"""Return a module-level logger that writes to logs/<name>.log (rotating)."""
logger = logging.getLogger(f"sanad.{name}")
if logger.handlers:
return logger
logger.setLevel(logging.DEBUG)
logger.propagate = False
LOGS_DIR.mkdir(parents=True, exist_ok=True)
fh = RotatingFileHandler(
LOGS_DIR / f"{name}.log", maxBytes=_MAX_BYTES, backupCount=_BACKUP_COUNT
)
fh.setFormatter(_formatter)
fh.setLevel(logging.DEBUG)
logger.addHandler(fh)
if to_console:
sh = logging.StreamHandler(_ascii_safe_stream(sys.stdout))
sh.setFormatter(_formatter)
sh.setLevel(logging.INFO)
logger.addHandler(sh)
# WebSocket stream handler
wsh = _WSHandler()
wsh.setFormatter(_formatter)
wsh.setLevel(logging.INFO)
logger.addHandler(wsh)
return logger