feat(logging): 日志持久化落盘(LOG_FILE 滚动文件)
- LOG_FILE 配置: 空(默认)保持 stdout + Admin 内存日志页(重启清零); 填路径后额外写 RotatingFileHandler(单文件 10MB × 5 份,utf-8) - setup_logging 幂等挂载(按 abspath 判重,相对/绝对同文件算一个); 目录自动创建;文件基础设施失败只 warning 绝不拖垮启动 - docker-compose: api 挂 applogs 卷,默认 LOG_FILE=/app/logs/app.log, 容器重建日志不丢;.env.example/.gitignore/docs 同步 - tests/test_log_persistence.py: 落盘/幂等/空值禁用/自动建目录/失败降级 - test_p0_standings_cutoff: 匿名约束 name=None 使 any() 子串匹配 TypeError (集合顺序不定 → flaky),先判真值再匹配(存量缺陷顺手加固)
This commit is contained in:
+48
-8
@@ -65,14 +65,54 @@ def get_entries(
|
||||
return out
|
||||
|
||||
|
||||
def setup_memory_logging(level: str = "INFO") -> None:
|
||||
"""挂载内存 handler 到 root logger(幂等),并确保 root 级别不低于 INFO。"""
|
||||
def setup_logging(level: str = "INFO", log_file: str = "") -> None:
|
||||
"""配置应用日志:stdout(容器收集) + 内存环形缓冲(Admin 日志页) + 可选滚动文件(持久化)。
|
||||
|
||||
幂等:重复调用不会重复挂 handler(文件 handler 按 abspath 判重,相对/绝对
|
||||
路径指向同一文件视为同一个)。文件写入基础设施失败只记 warning,绝不
|
||||
影响启动与业务 —— 与 _safe_write_ingest_failure 同级约束:可观测性
|
||||
基础设施不许拖垮主流程。
|
||||
"""
|
||||
import os
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
root = logging.getLogger()
|
||||
if any(isinstance(h, MemoryLogHandler) for h in root.handlers):
|
||||
return
|
||||
handler = MemoryLogHandler()
|
||||
handler.setLevel(logging.INFO)
|
||||
handler.addFilter(_SQLNoiseFilter())
|
||||
root.addHandler(handler)
|
||||
if root.level == logging.NOTSET or root.level > logging.INFO:
|
||||
root.setLevel(getattr(logging, level.upper(), logging.INFO))
|
||||
|
||||
if not any(isinstance(h, MemoryLogHandler) for h in root.handlers):
|
||||
handler = MemoryLogHandler()
|
||||
handler.setLevel(logging.INFO)
|
||||
handler.addFilter(_SQLNoiseFilter())
|
||||
root.addHandler(handler)
|
||||
|
||||
if not log_file:
|
||||
return
|
||||
|
||||
# 滚动上限:单文件 10MB × 当前+5 份 ≈ 60MB,足够回溯数周的关键事件,
|
||||
# 又不会吃满磁盘。格式含时间/级别/logger 名,便于事后 grep 排查。
|
||||
target = os.path.abspath(log_file)
|
||||
if any(
|
||||
isinstance(h, RotatingFileHandler) and getattr(h, "baseFilename", None) == target
|
||||
for h in root.handlers
|
||||
):
|
||||
return
|
||||
try:
|
||||
os.makedirs(os.path.dirname(target), exist_ok=True)
|
||||
file_handler = RotatingFileHandler(
|
||||
target,
|
||||
maxBytes=10 * 1024 * 1024,
|
||||
backupCount=5,
|
||||
encoding="utf-8",
|
||||
)
|
||||
file_handler.setFormatter(
|
||||
logging.Formatter("%(asctime)s %(levelname)s %(name)s %(message)s")
|
||||
)
|
||||
file_handler.setLevel(logging.INFO)
|
||||
file_handler.addFilter(_SQLNoiseFilter())
|
||||
root.addHandler(file_handler)
|
||||
logging.getLogger(__name__).info("文件日志已启用: %s", log_file)
|
||||
except Exception:
|
||||
logging.getLogger(__name__).warning(
|
||||
"启用文件日志失败(%s),仅保留 stdout/内存日志", log_file, exc_info=True,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user