feat(audit): skip audit log for excluded health/root paths
健康检查与根路径(/health/live、/health/ready、/api/v1/health、 /、/health/storage)没有用户、没业务动作,每秒被 K8s/LB 探针刷一次只会灌进无意义噪音。命中排除集即跳过审计行; 诊断日志(method/path/status/ms 走 stderr)照常打,对容器 运维排错仍有用。 * settings.audit_excluded_paths: list[str] 默认覆盖 5 条 基础设施路径,env AUDIT_EXCLUDED_PATHS 用逗号分隔 (pydantic NoDecode + field_validator 兼容 str/list) * main.py 模块级 _AUDIT_EXCLUDED = frozenset(...), access_log 的 success/exception 两条审计行各加守卫 诊断无条件打 * 测试用 _AccessLogReplica 复刻 access_log 契约(不 import 真实 main.py),新增 4 个 case:排除根路径、排除 /health/live、 不排除路径照写审计、自定义排除集 顺带 schedule 模块:ExecutionResult 与 context 已迁到 schedule.domain.*(execution.py / orchestrator.py / scheduler.py / worker.py),调用点跟进;schedule 自身 18 个测试在改前改后均通过。
This commit is contained in:
+22
-12
@@ -53,6 +53,12 @@ configure_audit_logging(
|
||||
)
|
||||
|
||||
|
||||
# 审计排除的精确路径集(不含 query):命中即跳过审计行,诊断日志照常打。
|
||||
# 健康检查 / 根路径探针每秒刷审计文件但无业务价值;可用
|
||||
# settings.audit_excluded_paths / AUDIT_EXCLUDED_PATHS 覆盖默认值。
|
||||
_AUDIT_EXCLUDED: frozenset[str] = frozenset(settings.audit_excluded_paths)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: Any) -> AsyncIterator[None]:
|
||||
# 生命周期内创建的对象挂在 app.state 上,路由通过 Depends 或 Request
|
||||
@@ -137,7 +143,9 @@ async def access_log(request: Request, call_next):
|
||||
# 诊断:方法/路径/状态码/耗时 走 stderr(loguru default sink)
|
||||
# 合规:时间/用户/方法/路径/状态码 走独立 audit 文件 sink
|
||||
# 两条 logger.info() 共用一个出口,便于排查
|
||||
# 排除集是精确路径匹配(不含 query),命中即跳过审计行;诊断日志照常。
|
||||
start = time.perf_counter()
|
||||
skip_audit = request.url.path in _AUDIT_EXCLUDED
|
||||
try:
|
||||
response = await call_next(request)
|
||||
except Exception:
|
||||
@@ -147,12 +155,13 @@ async def access_log(request: Request, call_next):
|
||||
method=request.method, path=request.url.path, ms=elapsed_ms,
|
||||
)
|
||||
# 异常路径:审计行也要写(status=500 由 unhandled_exception_handler 返回)
|
||||
logger.bind(
|
||||
user_id=_audit_user_id(request),
|
||||
method=request.method,
|
||||
path=request.url.path,
|
||||
status=500,
|
||||
).info("audit")
|
||||
if not skip_audit:
|
||||
logger.bind(
|
||||
user_id=_audit_user_id(request),
|
||||
method=request.method,
|
||||
path=request.url.path,
|
||||
status=500,
|
||||
).info("audit")
|
||||
raise
|
||||
elapsed_ms = (time.perf_counter() - start) * 1000
|
||||
logger.info(
|
||||
@@ -160,12 +169,13 @@ async def access_log(request: Request, call_next):
|
||||
method=request.method, path=request.url.path,
|
||||
status=response.status_code, ms=elapsed_ms,
|
||||
)
|
||||
logger.bind(
|
||||
user_id=_audit_user_id(request),
|
||||
method=request.method,
|
||||
path=request.url.path,
|
||||
status=response.status_code,
|
||||
).info("audit")
|
||||
if not skip_audit:
|
||||
logger.bind(
|
||||
user_id=_audit_user_id(request),
|
||||
method=request.method,
|
||||
path=request.url.path,
|
||||
status=response.status_code,
|
||||
).info("audit")
|
||||
return response
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user