91 lines
2.7 KiB
Python
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
|