把需要账号登录态的链路从抓取服务里拆出成独立进程。分界线不是「要不要登录」, 而是抓取无状态、幂等、可多开实例,而交易的写操作不可逆、登录态全局唯一、 订单监控是常驻轮询——同进程时抓取一扩容就会复制出 N 份登录态与 N 个轮询, 同一账号会被并发操作。 - app/shared:配置、错误码、日志、ApiResponse 信封 + Bearer 鉴权 + 异常处理器、 导航请求头构造器 - app/scraping:站点常量、会话、解析器与 10 个抓取接口,:31107,可多开 - app/trading:登录态查询/重载与健康检查,:31108,只能单实例 - 依赖方向锁为 scraping→shared、trading→shared,两侧互不 import; tests/test_architecture.py 用 AST 检查 import 并校验两个 app 的路径不串 - 登录态 UA 在 trading 独立持有:与抓取 UA 值相同但变更理由不同,抓取 UA 为绕 反爬可随时调整,登录 UA 一改可能触发设备校验使已落盘 cookie 失效 - scripts/login.py 与 AuthSession 共用 auth_site.PROFILES 与 is_logged_in,判据只写一遍 - 同一镜像两个启动命令,交易容器覆盖 command 并设 RAKUTEN_HEALTH_PORT 同时带上此前未提交的 ラクマ 分类接口与登录态基础设施。 验证:239 个离线用例全绿;两个入口真实启动,/health 与鉴权正常。 未验证:真实探测登录态(当前开发机无外网,对站点的连接全部超时)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
87 lines
2.7 KiB
Python
87 lines
2.7 KiB
Python
"""日志配置:使用 loguru 替代标准 logging,统一管理日志输出
|
|
|
|
- 控制台输出:始终启用
|
|
- 文件输出:生产环境默认启用,支持日志轮转、压缩和保留策略
|
|
- 拦截标准 logging:将 uvicorn 等第三方库的日志转发到 loguru
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import sys
|
|
from pathlib import Path
|
|
from types import FrameType
|
|
|
|
from loguru import logger as loguru_logger
|
|
|
|
from app.shared.config import Settings
|
|
|
|
|
|
class InterceptHandler(logging.Handler):
|
|
"""拦截标准 logging 日志,转发到 loguru"""
|
|
|
|
def emit(self, record: logging.LogRecord) -> None:
|
|
try:
|
|
level: str | int = loguru_logger.level(record.levelname).name
|
|
except ValueError:
|
|
level = record.levelno
|
|
|
|
frame: FrameType | None = logging.currentframe()
|
|
depth = 2
|
|
while frame and frame.f_code.co_filename == logging.__file__:
|
|
frame = frame.f_back
|
|
depth += 1
|
|
|
|
loguru_logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
|
|
|
|
|
|
def configure_logging(settings: Settings) -> None:
|
|
"""根据配置初始化日志系统
|
|
|
|
- 移除 loguru 默认 handler
|
|
- 添加控制台输出
|
|
- 生产环境添加文件输出(轮转、压缩、保留策略)
|
|
- 拦截标准 logging,统一走 loguru 输出
|
|
"""
|
|
loguru_logger.remove()
|
|
loguru_logger.add(
|
|
sys.stdout,
|
|
level=settings.log_level.upper(),
|
|
format=settings.log_format,
|
|
enqueue=settings.log_enqueue,
|
|
backtrace=False,
|
|
diagnose=False,
|
|
)
|
|
|
|
if settings.log_to_file_effective:
|
|
log_dir = Path(settings.log_dir)
|
|
log_dir.mkdir(parents=True, exist_ok=True)
|
|
log_path = log_dir / f"app_{settings.app_env}.log"
|
|
loguru_logger.add(
|
|
str(log_path),
|
|
level=settings.log_level.upper(),
|
|
format=settings.log_format,
|
|
rotation=settings.log_rotation,
|
|
retention=settings.log_retention,
|
|
compression=settings.log_compression,
|
|
enqueue=settings.log_enqueue,
|
|
encoding="utf-8",
|
|
backtrace=False,
|
|
diagnose=False,
|
|
)
|
|
|
|
intercept = InterceptHandler()
|
|
logging.basicConfig(
|
|
handlers=[intercept],
|
|
level=getattr(logging, settings.log_level.upper(), logging.INFO),
|
|
force=True,
|
|
)
|
|
|
|
for name in ("uvicorn", "uvicorn.error", "uvicorn.access"):
|
|
logger = logging.getLogger(name)
|
|
logger.handlers = [intercept]
|
|
logger.propagate = False
|
|
|
|
logging.getLogger("asyncio").setLevel(logging.WARNING)
|
|
# httpx 每次请求都会打一条 INFO,抓取服务下噪音过大
|
|
logging.getLogger("httpx").setLevel(logging.WARNING)
|