xxy 43f3e0b746 Initial commit
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 18:41:06 +08:00

143 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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)