143 lines
4.4 KiB
Python
143 lines
4.4 KiB
Python
"""
|
||
log/logger.py
|
||
统一日志配置:
|
||
|
||
- 控制台输出(强制 UTF-8,修复 Windows 控制台中文乱码)
|
||
- logs/app.log 全量日志(按大小轮转)
|
||
- logs/error.log 仅 WARNING 及以上
|
||
- logs/upload.log 上传/解析/入库链路(routers.template、services.*)
|
||
- 接管 uvicorn 的 access/error 日志,统一落盘
|
||
|
||
幂等:重复调用只配置一次。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import sys
|
||
from logging.handlers import RotatingFileHandler
|
||
from pathlib import Path
|
||
|
||
_CONFIGURED = False
|
||
|
||
_FORMAT = "%(asctime)s | %(levelname)-7s | %(name)s | %(message)s"
|
||
_DATEFMT = "%Y-%m-%d %H:%M:%S"
|
||
|
||
# 上传/解析/入库链路相关的 logger 前缀(额外汇总到 upload.log)
|
||
_UPLOAD_PREFIXES = (
|
||
"routers.template",
|
||
"services.file_parse_client",
|
||
"services.section_extractor",
|
||
"services.declaration_service",
|
||
"services.llm_client",
|
||
)
|
||
|
||
# 交由 root 统一处理的第三方/框架 logger
|
||
_DELEGATED_LOGGERS = ("uvicorn", "uvicorn.error", "uvicorn.access")
|
||
|
||
|
||
class _PrefixFilter(logging.Filter):
|
||
def __init__(self, prefixes: tuple[str, ...]) -> None:
|
||
super().__init__()
|
||
self.prefixes = prefixes
|
||
|
||
def filter(self, record: logging.LogRecord) -> bool:
|
||
name = str(record.name or "")
|
||
return any(name == p or name.startswith(p + ".") for p in self.prefixes)
|
||
|
||
|
||
def _force_utf8_stream(stream):
|
||
"""让控制台以 UTF-8 输出,避免 Windows GBK 控制台中文乱码。"""
|
||
reconfigure = getattr(stream, "reconfigure", None)
|
||
if callable(reconfigure):
|
||
try:
|
||
reconfigure(encoding="utf-8", errors="replace")
|
||
except (ValueError, OSError):
|
||
pass
|
||
return stream
|
||
|
||
|
||
def configure_logging(
|
||
*,
|
||
log_dir: str | Path | None = None,
|
||
level: str | int | None = None,
|
||
to_console: bool | None = None,
|
||
max_bytes: int | None = None,
|
||
backup_count: int | None = None,
|
||
) -> Path:
|
||
"""配置全局日志。返回 app.log 路径。"""
|
||
global _CONFIGURED
|
||
|
||
# 延迟导入,避免与 config 形成循环依赖问题
|
||
from config import settings
|
||
|
||
log_dir = log_dir if log_dir is not None else settings.LOG_DIR
|
||
level = level if level is not None else settings.LOG_LEVEL
|
||
to_console = to_console if to_console is not None else settings.LOG_TO_CONSOLE
|
||
max_bytes = max_bytes if max_bytes is not None else settings.LOG_MAX_BYTES
|
||
backup_count = backup_count if backup_count is not None else settings.LOG_BACKUP_COUNT
|
||
|
||
if isinstance(level, str):
|
||
level = getattr(logging, level.strip().upper(), logging.INFO)
|
||
|
||
target_dir = Path(log_dir).resolve()
|
||
target_dir.mkdir(parents=True, exist_ok=True)
|
||
app_log_path = target_dir / "app.log"
|
||
|
||
if _CONFIGURED:
|
||
return app_log_path
|
||
|
||
formatter = logging.Formatter(_FORMAT, datefmt=_DATEFMT)
|
||
|
||
def _rotating(name: str, *, backups: int | None = None) -> RotatingFileHandler:
|
||
h = RotatingFileHandler(
|
||
target_dir / name,
|
||
maxBytes=max_bytes,
|
||
backupCount=backups if backups is not None else backup_count,
|
||
encoding="utf-8",
|
||
)
|
||
h.setFormatter(formatter)
|
||
return h
|
||
|
||
# 全量日志
|
||
app_handler = _rotating("app.log")
|
||
app_handler.setLevel(level)
|
||
|
||
# 错误日志(WARNING+)
|
||
error_handler = _rotating("error.log")
|
||
error_handler.setLevel(logging.WARNING)
|
||
|
||
# 上传/解析链路日志
|
||
upload_handler = _rotating("upload.log", backups=max(backup_count, 10))
|
||
upload_handler.setLevel(level)
|
||
upload_handler.addFilter(_PrefixFilter(_UPLOAD_PREFIXES))
|
||
|
||
handlers: list[logging.Handler] = [app_handler, error_handler, upload_handler]
|
||
|
||
if to_console:
|
||
console_handler = logging.StreamHandler(_force_utf8_stream(sys.stdout))
|
||
console_handler.setLevel(level)
|
||
console_handler.setFormatter(formatter)
|
||
handlers.append(console_handler)
|
||
|
||
root = logging.getLogger()
|
||
root.setLevel(level)
|
||
root.handlers.clear()
|
||
for h in handlers:
|
||
root.addHandler(h)
|
||
|
||
# 让 uvicorn 的日志走 root 统一落盘
|
||
for name in _DELEGATED_LOGGERS:
|
||
lg = logging.getLogger(name)
|
||
lg.handlers.clear()
|
||
lg.propagate = True
|
||
lg.setLevel(level)
|
||
|
||
_CONFIGURED = True
|
||
logging.getLogger(__name__).info("日志系统已初始化 | dir=%s | level=%s", target_dir, logging.getLevelName(level))
|
||
return app_log_path
|
||
|
||
|
||
def get_logger(name: str) -> logging.Logger:
|
||
return logging.getLogger(name)
|