Merge remote-tracking branch 'origin/main' into fix-homelist
This commit is contained in:
+51
-2
@@ -22,13 +22,13 @@ from __future__ import annotations
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from contextvars import ContextVar
|
||||
from datetime import datetime
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# 请求级 trace_id:入口(如 compare.py 透传壳)set 之后, 本请求上下文(含 run_in_threadpool
|
||||
# 拷贝出去的线程)内所有日志自动带上。默认空串 = 非请求上下文(启动/后台 worker)。
|
||||
trace_id_ctx: ContextVar[str] = ContextVar("trace_id", default="")
|
||||
@@ -91,6 +91,55 @@ class TextFormatter(logging.Formatter):
|
||||
return f"{base} trace={tid}" if tid else base
|
||||
|
||||
|
||||
class SafeRotatingFileHandler(RotatingFileHandler):
|
||||
"""Windows 下不会被外部句柄卡死的 RotatingFileHandler。
|
||||
|
||||
stdlib 轮转靠 rename 活动文件(app-server.log → .1);Windows 只要有别的句柄(IDE 索引、
|
||||
app.admin.main 第二进程、残留 --reload worker、杀软扫描)开着它, rename 就 WinError 32,
|
||||
轮转永久卡死——文件停在 maxBytes、之后每条日志被丢。这里 Windows 改用 copytruncate:把活动
|
||||
文件拷进备份、再通过自己的句柄原地清空, 从不 rename 活动文件, 故外部句柄开着也能转。
|
||||
POSIX(生产 Linux)rename 打开中的文件本就合法, 保留 stdlib 的原子轮转不变。
|
||||
|
||||
代价:copytruncate 在“拷贝→清空”极窄窗口内并发写可能丢几行(仅跨进程;同进程 emit 有
|
||||
handler 锁串行, 无此问题)。对本地开发日志可接受。
|
||||
"""
|
||||
|
||||
def doRollover(self) -> None:
|
||||
if os.name != "nt":
|
||||
super().doRollover()
|
||||
return
|
||||
if self.stream is None:
|
||||
self.stream = self._open()
|
||||
else:
|
||||
self.stream.flush()
|
||||
try:
|
||||
self._copytruncate_backups()
|
||||
except OSError:
|
||||
# 备份腾挪是尽力而为:任一备份被占用也绝不能挡住下面的清空, 否则活动文件继续涨、
|
||||
# 轮转又卡死——那就白改了。
|
||||
pass
|
||||
# 通过自己独占的句柄原地清空:不涉及 rename, 外部只读句柄不受影响。
|
||||
self.stream.seek(0)
|
||||
self.stream.truncate()
|
||||
self.stream.flush()
|
||||
|
||||
def _copytruncate_backups(self) -> None:
|
||||
"""把 .N-1→.N 逐级腾挪, 再把活动文件拷到 .1(不动活动文件本身)。"""
|
||||
if self.backupCount <= 0:
|
||||
return
|
||||
for i in range(self.backupCount - 1, 0, -1):
|
||||
sfn = self.rotation_filename(f"{self.baseFilename}.{i}")
|
||||
dfn = self.rotation_filename(f"{self.baseFilename}.{i + 1}")
|
||||
if os.path.exists(sfn):
|
||||
if os.path.exists(dfn):
|
||||
os.remove(dfn)
|
||||
os.replace(sfn, dfn)
|
||||
dfn = self.rotation_filename(f"{self.baseFilename}.1")
|
||||
if os.path.exists(dfn):
|
||||
os.remove(dfn)
|
||||
shutil.copyfile(self.baseFilename, dfn)
|
||||
|
||||
|
||||
_CONFIGURED = False
|
||||
|
||||
|
||||
@@ -126,7 +175,7 @@ def setup_logging(debug: bool = False) -> None:
|
||||
Path(os.getenv("LOG_DIR", "logs")) / "app-server.log"
|
||||
)
|
||||
Path(log_file).parent.mkdir(parents=True, exist_ok=True)
|
||||
file_handler = RotatingFileHandler(
|
||||
file_handler = SafeRotatingFileHandler(
|
||||
log_file, maxBytes=10 * 1024 * 1024, backupCount=5, encoding="utf-8",
|
||||
)
|
||||
file_handler.setFormatter(JsonFormatter(service))
|
||||
|
||||
Reference in New Issue
Block a user