merge: 合并远程 ft_wyt 分支,解决权限与日志模块冲突

- 冲突解决原则:本地权限治理(require_admin/current_user/资源ACL)与远程 op_log 日志装饰器双向保留
- platform.py: 9 处冲突,@op_log 与管理员校验叠加,避免远程丢失 require_admin 的安全回归
- logging.py: 合并 get_client_ip 与 user_id_var,X-Trace-Id 优先 + ContextVar 卫生处理
- op_log.py: 采纳远程将变量计算上移到函数顶部的结构
- compute_poller.py: 中文日志 + 失败去重限流/断连重置逻辑
- data_process.py: 保留租户归属字段 + biz_logger 成功日志
This commit is contained in:
wuyongtao
2026-08-21 09:58:43 +08:00
27 changed files with 2778 additions and 688 deletions

View File

@@ -1,22 +1,26 @@
from __future__ import annotations
from contextvars import ContextVar
from datetime import date, datetime, timedelta
import json
import logging
import re
import socket
import sys
import time
from contextvars import ContextVar
from datetime import date, datetime, timedelta
from logging import Handler, LogRecord
from pathlib import Path
import re
import time
from typing import Any, Callable, Optional
from typing import Any, Callable
from uuid import uuid4
from fastapi import FastAPI, Request
from app.core.config import Settings, get_settings
# ==================== 链路追踪 ContextVar ====================
request_id_var: ContextVar[str] = ContextVar("request_id", default="-")
user_id_var: ContextVar[str] = ContextVar("user_id", default="")
client_ip_var: ContextVar[str] = ContextVar("client_ip", default="")
@@ -30,54 +34,74 @@ def get_client_ip(request: Request | None) -> str:
return value.split(",", 1)[0].strip()
return request.client.host if request.client else ""
# ==================== 敏感数据脱敏规则 ====================
# ==================== 敏感数据脱敏 ====================
SENSITIVE_PATTERNS: dict[str, Callable | str] = {
"token": "***",
"password": "***",
"access_token": "***",
"refresh_token": "***",
"secret_key": "***",
"authorization": "***",
"bearer": "***",
"api_key": "***",
"private_key": "***",
SENSITIVE_KEYS: set[str] = {
"password", "token", "access_token", "refresh_token",
"secret_key", "authorization", "bearer", "api_key",
"private_key", "secret", "cookie",
}
def mask_value(key: str, value: Any) -> str:
"""对单个值进行脱敏处理"""
FULL_MASK_KEYS: set[str] = {
"password", "token", "access_token", "refresh_token",
"secret_key", "authorization", "bearer", "api_key",
"private_key", "secret", "cookie",
}
def _mask_phone(value: str) -> str:
"""手机号脱敏138****5678"""
if len(value) >= 11:
return value[:3] + "****" + value[-4:]
return value
def _mask_id_card(value: str) -> str:
"""身份证号脱敏110***********1234"""
if len(value) >= 18:
return value[:3] + "***********" + value[-4:]
return value
def mask_value(key: str, value: Any) -> Any:
"""对单个值进行脱敏处理。"""
if value is None:
return ""
key_lower = key.lower()
if key_lower in FULL_MASK_KEYS:
return "***"
str_val = str(value)
handler = SENSITIVE_PATTERNS.get(key)
if callable(handler):
return handler(str_val)
elif isinstance(handler, str):
# 支持正则替换模式,如 r"1\d{3}\d{4}"
try:
return re.sub(handler, "***", str_val)
except re.error:
return "***"
return handler
# 手机号模式11位数字1开头
if re.match(r"^1[3-9]\d{9}$", str_val):
return _mask_phone(str_val)
# 身份证模式18位
if re.match(r"^\d{17}[\dXx]$", str_val):
return _mask_id_card(str_val)
return value
def mask_sensitive_dict(data: dict) -> dict:
"""递归脱敏字典中的敏感字段"""
"""递归脱敏字典中的敏感字段"""
if not data or not isinstance(data, dict):
return data
result = {}
result: dict[str, Any] = {}
for key, value in data.items():
result[key] = mask_value(key, value)
if isinstance(value, dict):
result[key] = mask_sensitive_dict(value)
elif isinstance(value, list):
result[key] = [
mask_sensitive_dict(item) if isinstance(item, dict) else item
for item in value
]
else:
result[key] = mask_value(key, value)
return result
def mask_sensitive_string(text: str) -> str:
"""从文本中脱敏常见敏感信息"""
"""从文本中脱敏常见敏感信息"""
if not text:
return text
# Mask the value as well as the key. Replacing only ``api_key=`` would
# still leak the credential in audit messages and exception text.
assignment_pattern = (
@@ -90,92 +114,165 @@ def mask_sensitive_string(text: str) -> str:
except re.error:
pass
patterns = [
(r'Bearer\s+[A-Za-z0-9\-._]+', 'Bearer ***'),
(r'\d{11}', r'\d{3}\*\d{4}'), # 手机号/身份证
(r'1[3-9]\d{9}', r'1\*{3}\*{4}'), # 手机号
patterns: list[tuple[str, str]] = [
(r"Bearer\s+[A-Za-z0-9\-._]+", "Bearer ***"),
(r"(?i)token\s*[:=]\s*\S+", "token=***"),
(r"(?i)password\s*[:=]\s*\S+", "password=***"),
(r"(?i)secret[_-]?key\s*[:=]\s*\S+", "secret_key=***"),
(r"(?i)api[-_]?key\s*[:=]\s*\S+", "api_key=***"),
(r"(?i)private[_-]?key\s*[:=]\s*\S+", "private_key=***"),
(r"(?i)authorization\s*[:=]\s*\S+", "authorization=***"),
]
for pattern, replacement in patterns:
try:
text = re.sub(pattern, replacement, text, flags=re.IGNORECASE)
except re.error:
pass
text = re.sub(pattern, replacement, text)
# 手机号脱敏
text = re.sub(r"\b1[3-9]\d{9}\b", lambda m: _mask_phone(m.group()), text)
return text
# ==================== RequestId Filter ====================
# ==================== 大对象截断 ====================
MAX_FIELD_SIZE = 1024 # 超过 1KB 的内容自动截断
def truncate_large_value(value: Any, max_size: int = MAX_FIELD_SIZE) -> Any:
"""超过 max_size 的字符串自动截断(前 500 + 后 500"""
if isinstance(value, str) and len(value) > max_size:
half = max_size // 2
return value[:half] + f"...[truncated {len(value) - max_size} chars]..." + value[-half:]
if isinstance(value, dict):
return {k: truncate_large_value(v, max_size) for k, v in value.items()}
if isinstance(value, list):
return [truncate_large_value(v, max_size) for v in value]
return value
# ==================== TraceId Filter ====================
class TraceIdFilter(logging.Filter):
"""自动注入 traceId / userId / clientIp 到每条日志记录。"""
class RequestIdFilter(logging.Filter):
def filter(self, record: LogRecord) -> bool:
record.request_id = request_id_var.get()
record.traceId = request_id_var.get()
record.userId = user_id_var.get("")
record.clientIp = client_ip_var.get("")
record.host = getattr(self, "_host", None) or socket.gethostname()
record.app = getattr(self, "_app", "yg-ft-platform")
record.env = getattr(self, "_env", "dev")
return True
def set_context(self, app: str, env: str, host: str) -> None:
self._app = app
self._env = env
self._host = host
# ==================== Enhanced JSON Formatter ====================
# ==================== JSON Formatter ====================
class JsonLogFormatter(logging.Formatter):
"""
增强的 JSON 日志格式化器,支持结构化字段输出
生产级 JSON 日志格式化器,符合方案文档 §3.2 字段规范
输出示例:
{
"@timestamp": "2026-08-17T18:30:00.123Z",
"@timestamp": "2026-08-19T10:30:45.123+08:00",
"level": "INFO",
"logger": "dataset.router",
"logger": "app.api.v1.endpoints.platform",
"traceId": "abc-123-def-456",
"userId": "u_admin",
"message": "数据集创建成功",
"module": "dataset.router",
"function": "create_dataset",
"file": "dataset/router.py",
"line": 45,
"process": 12345,
"fields": {"datasetId": "ds_001", "costMs": 23},
"file": "platform.py:156",
"thread": "MainThread",
"request_id": "req-abc123",
"user_id": "u_admin",
"client_ip": "192.168.1.100",
"extra": {...}
"host": "pod-7x9k2",
"app": "yg-ft-platform",
"env": "dev"
}
"""
# 标准 LogRecord 属性名集合,用于区分 extra 字段
_STD_ATTRS: set[str] = set(vars(logging.LogRecord("", 0, "", 0, "", None, None)).keys()) | {
"traceId", "userId", "clientIp", "host", "app", "env",
"request_id", "user_id", "client_ip",
"asctime", "message", "module", "function", "process",
"thread", "threadName", "levelname", "levelno", "name",
"pathname", "filename", "lineno", "funcName", "created",
"msecs", "relativeCreated", "exc_info", "exc_text",
"stack_info", "msg", "args", "processName", "process",
}
def format(self, record: LogRecord) -> str:
# 时间戳ISO8601 带时区
timestamp = datetime.fromtimestamp(record.created).astimezone().isoformat(
timespec="milliseconds"
)
payload: dict[str, Any] = {
"@timestamp": datetime.fromtimestamp(record.created).astimezone().isoformat(
timespec="milliseconds"
),
"@timestamp": timestamp,
"level": record.levelname,
"logger": record.name,
"traceId": getattr(record, "traceId", "-"),
"message": record.getMessage(),
"module": record.module,
"function": record.funcName,
"file": record.pathname,
"line": record.lineno,
"process": record.process,
"thread": record.thread,
"thread_name": record.threadName,
"request_id": getattr(record, "request_id", "-"),
"file": f"{Path(record.pathname).name}:{record.lineno}",
"thread": record.threadName,
"host": getattr(record, "host", ""),
"app": getattr(record, "app", ""),
"env": getattr(record, "env", ""),
}
# 从 record 中提取额外字段(通过 extra 参数传入
for attr in ("user_id", "client_ip", "target_type", "target_id",
"duration_ms", "status_code", "error"):
# userId业务必填未登录可为空
user_id = getattr(record, "userId", "") or getattr(record, "user_id", "")
if user_id:
payload["userId"] = user_id
# clientIp
client_ip = getattr(record, "clientIp", "") or getattr(record, "client_ip", "")
if client_ip:
payload["clientIp"] = client_ip
# 提取结构化业务字段:只收集通过 extra 传入的非标准属性
fields: dict[str, Any] = {}
for attr in dir(record):
if attr.startswith("_"):
continue
if attr in self._STD_ATTRS:
continue
if attr in ("traceId", "userId", "clientIp", "host", "app", "env"):
continue
val = getattr(record, attr, None)
if val is not None:
payload[attr] = val
# 处理异常信息
if record.exc_info:
if val is not None and not callable(val):
fields[attr] = truncate_large_value(val)
if fields:
payload["fields"] = mask_sensitive_dict(fields)
# ERROR 级别额外字段
if record.levelname == "ERROR" or record.exc_info:
error_obj: dict[str, Any] = {
"type": type(record.exc_info[1]).__name__ if record.exc_info and record.exc_info[1] else "Error",
"message": record.getMessage(),
}
if record.exc_info:
error_obj["stack_trace"] = self.formatException(record.exc_info)
if record.stack_info:
error_obj["stack_trace"] = self.formatStack(record.stack_info)
payload["error"] = error_obj
# 兼容旧字段名 exception
if record.exc_info and "error" not in payload:
payload["exception"] = self.formatException(record.exc_info)
if record.stack_info:
payload["stack"] = self.formatStack(record.stack_info)
return json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
# ==================== DateSizeRotatingFileHandler ====================
# (保持不变,已有实现)
class DateSizeRotatingFileHandler(Handler):
"""Rotate log files by date and size while keeping date in every file name."""
"""按日期+大小滚动的文件日志处理器。
- 按天创建文件,文件名包含日期
- 单文件超过 max_bytes 时自动滚动(带序号后缀)
- 自动清理超过 retention_days 的旧日志
"""
def __init__(
self,
@@ -233,10 +330,8 @@ class DateSizeRotatingFileHandler(Handler):
today = date.today()
if not force and self._stream and self._current_date == today:
return
if self._stream and not self._stream.closed:
self._stream.close()
self._current_date = today
self._current_path = self._dated_path(today)
self._stream = self._current_path.open("a", encoding=self.encoding)
@@ -251,11 +346,9 @@ class DateSizeRotatingFileHandler(Handler):
def _rotate_by_size(self) -> None:
if not self._current_path or not self._current_path.exists():
return
if self._stream and not self._stream.closed:
self._stream.close()
self._stream = None
stem = self._current_path.stem
suffix = self._current_path.suffix
index = 1
@@ -269,7 +362,6 @@ class DateSizeRotatingFileHandler(Handler):
def _cleanup_expired_files(self) -> None:
if self.retention_days <= 0:
return
cutoff = date.today() - timedelta(days=self.retention_days - 1)
pattern = re.compile(
rf"^{re.escape(self.file_prefix)}-(\d{{4}}-\d{{2}}-\d{{2}})(?:\.\d+)?\.log$"
@@ -283,99 +375,126 @@ class DateSizeRotatingFileHandler(Handler):
path.unlink(missing_ok=True)
# ==================== Structured Logger 封装 ====================
# ==================== StructuredLogger 封装 ====================
class StructuredLogger:
"""
结构化日志记录器,提供统一的日志接口。
结构化日志记录器,提供符合方案文档 §4.2 的 5W1H 日志接口。
使用方式:
logger = get_structured_logger('dataset.router')
logger.info('创建数据集', dataset_id='ds_123')
logger = get_structured_logger('app.api.dataset')
logger.info('数据集创建成功', datasetId='ds_001', costMs=23)
"""
def __init__(self, name: str, module: str = ""):
self.logger = logging.getLogger(name)
self.name = name
self.module = module
@property
def trace_id(self) -> str:
return request_id_var.get("-")
def info(self, message: str, **extra: Any) -> None:
self._log("INFO", message, **extra)
def warning(self, message: str, **extra: Any) -> None:
self._log("WARNING", message, **extra)
def info(self, message: str, **fields: Any) -> None:
self._log(logging.INFO, message, **fields)
def error(self, message: str, **extra: Any) -> None:
self._log("ERROR", message, **extra)
def warning(self, message: str, **fields: Any) -> None:
self._log(logging.WARNING, message, **fields)
def debug(self, message: str, **extra: Any) -> None:
self._log("DEBUG", message, **extra)
def error(self, message: str, **fields: Any) -> None:
self._log(logging.ERROR, message, **fields)
def _log(self, level: str, message: str, **extra: Any) -> None:
"""统一日志记录方法"""
log_entry: dict[str, Any] = {
"timestamp": datetime.utcnow().isoformat(),
"level": level,
"logger": self.name,
"module": self.module,
"message": message,
"trace_id": self.trace_id,
"extra": extra,
}
self.logger.log(getattr(logging, level, logging.INFO), json.dumps(log_entry, ensure_ascii=False, default=str))
def debug(self, message: str, **fields: Any) -> None:
self._log(logging.DEBUG, message, **fields)
def _log(self, level: int, message: str, **fields: Any) -> None:
"""统一日志记录方法,通过 extra 传递结构化字段。"""
extra: dict[str, Any] = {}
if self.module:
extra["module"] = self.module
# 脱敏 + 截断
for k, v in fields.items():
extra[k] = truncate_large_value(v)
self.logger.log(level, message, extra=extra, stack_info=False)
def get_structured_logger(name: str, module: str = "") -> StructuredLogger:
"""获取结构化日志记录器"""
"""获取结构化日志记录器"""
return StructuredLogger(name, module)
# ==================== 快捷函数 ====================
def get_logger(name: str) -> logging.Logger:
"""获取标准 Python logger"""
"""获取标准 Python logger"""
return logging.getLogger(name)
def set_request_id(request_id: str) -> None:
"""设置当前请求的追踪 ID"""
"""设置当前请求的追踪 ID"""
request_id_var.set(request_id)
def set_user_context(user_id: str = "", client_ip: str = "") -> None:
"""设置当前请求的用户上下文(在鉴权后调用)。"""
if user_id:
user_id_var.set(user_id)
if client_ip:
client_ip_var.set(client_ip)
# ==================== 请求日志中间件 ====================
def setup_request_logging(app: FastAPI) -> None:
"""配置 FastAPI 请求日志中间件"""
"""配置 FastAPI 请求日志中间件,符合方案文档 §五(链路追踪)和 §十(访问日志)。"""
logger = get_logger("app.access")
@app.middleware("http")
async def request_logging_middleware(request: Request, call_next): # type: ignore[no-untyped-def]
request_id = request.headers.get("X-Request-ID") or str(uuid4())
token = request_id_var.set(request_id)
# 入口生成 traceId优先使用前端传入的 X-Trace-Id
trace_id = request.headers.get("X-Trace-Id") or request.headers.get("X-Request-ID") or str(uuid4())
token = request_id_var.set(trace_id)
ip_token = client_ip_var.set(get_client_ip(request))
started_at = time.perf_counter()
# 提取客户端 IP
client_ip = "-"
if request.client:
client_ip = request.client.host
# 支持反向代理传递的真实 IP
forwarded_for = request.headers.get("X-Forwarded-For", "")
if forwarded_for:
client_ip = forwarded_for.split(",")[0].strip()
client_ip_var.set(client_ip)
try:
response = await call_next(request)
elapsed_ms = (time.perf_counter() - started_at) * 1000
# 噪声路径降级为 DEBUG健康检查等
noisy_paths = ("/health", "/system-info", "/compute/jobs/", "/model-eval/", "/model-compare/")
log_method = logger.debug if request.method == "GET" and response.status_code < 400 else logger.info
if any(request.url.path.endswith(path) or path in request.url.path for path in noisy_paths) and response.status_code < 400:
log_method = logger.info
if any(request.url.path.endswith(p) or p in request.url.path for p in noisy_paths) and response.status_code < 400:
log_method = logger.debug
if response.status_code >= 400:
if response.status_code >= 500:
log_method = logger.error
elif response.status_code >= 400:
log_method = logger.warning
# 结构化访问日志(中文 message方便直接阅读
log_method(
"request completed method=%s path=%s status_code=%s duration_ms=%.2f client=%s",
request.method,
request.url.path,
response.status_code,
elapsed_ms,
request.client.host if request.client else "-",
f"HTTP请求 {request.method} {request.url.path} {response.status_code}(耗时{round(elapsed_ms, 2)}ms",
extra={
"request_method": request.method,
"request_path": request.url.path,
"status_code": response.status_code,
"duration_ms": round(elapsed_ms, 2),
"client_ip": client_ip,
"user_agent": request.headers.get("User-Agent", "")[:200],
},
)
# 5xx 系统错误自动写入操作日志(未被 @op_log 覆盖的系统级异常)
# 5xx 系统错误自动写入操作日志
if response.status_code >= 500:
try:
from app.core.op_log import log_operation, OpModule, OpStatus
@@ -393,18 +512,23 @@ def setup_request_logging(app: FastAPI) -> None:
duration_ms=elapsed_ms,
)
except Exception:
pass # 日志写入失败不影响主流程
pass
response.headers["X-Request-ID"] = request_id
response.headers["X-Trace-Id"] = trace_id
response.headers["X-Request-ID"] = trace_id
return response
except Exception:
elapsed_ms = (time.perf_counter() - started_at) * 1000
logger.exception(
"request failed method=%s path=%s duration_ms=%.2f client=%s",
request.method,
request.url.path,
elapsed_ms,
request.client.host if request.client else "-",
logger.error(
f"HTTP请求异常 {request.method} {request.url.path}(耗时{round(elapsed_ms, 2)}ms— 服务内部错误",
extra={
"request_method": request.method,
"request_path": request.url.path,
"duration_ms": round(elapsed_ms, 2),
"client_ip": client_ip,
},
exc_info=True,
)
# 未被捕获的异常,写入操作日志
@@ -426,7 +550,7 @@ def setup_request_logging(app: FastAPI) -> None:
duration_ms=elapsed_ms,
)
except Exception:
pass # 日志写入失败不影响主流程
pass
raise
finally:
@@ -437,53 +561,122 @@ def setup_request_logging(app: FastAPI) -> None:
# ==================== 配置函数 ====================
def configure_logging(settings: Settings | None = None) -> None:
"""
生产级日志配置,符合方案文档 §二(分类分流)和 §六(性能安全)。
日志分类:
- 业务日志 (app-biz): INFO+ 业务流程(保留 7 天)
- 系统日志 (app-sys): 框架/中间件日志(保留 7 天)
- 访问日志 (app-access): HTTP 请求日志(保留 15 天)
- 错误日志 (app-error): ERROR 级别(保留 30 天)
"""
settings = settings or get_settings()
root_logger = logging.getLogger()
root_logger.handlers.clear()
root_logger.setLevel(settings.log_level.upper())
# ---- Formatter ----
console_formatter = logging.Formatter(
fmt=(
"%(asctime)s | %(levelname)s | pid=%(process)d | %(threadName)s | "
"request_id=%(request_id)s | %(name)s | %(pathname)s:%(lineno)d | %(message)s"
"traceId=%(traceId)s | %(name)s | %(pathname)s:%(lineno)d | %(message)s"
),
datefmt="%Y-%m-%d %H:%M:%S",
)
json_formatter = JsonLogFormatter()
request_filter = RequestIdFilter()
# ---- TraceIdFilter全局注入 traceId/userId/host/app/env----
trace_filter = TraceIdFilter()
trace_filter.set_context(
app=settings.app_name,
env=settings.app_env,
host=socket.gethostname(),
)
# ---- 控制台 Handler ----
console_handler = logging.StreamHandler()
console_handler.setFormatter(console_formatter)
console_handler.addFilter(request_filter)
console_handler.addFilter(trace_filter)
file_handler = DateSizeRotatingFileHandler(
# ---- 业务日志文件 Handler (app-biz) ----
biz_file_handler = DateSizeRotatingFileHandler(
log_dir=settings.log_dir,
file_prefix=settings.log_file_prefix,
file_prefix="app-biz",
max_bytes=settings.log_max_bytes,
retention_days=settings.log_retention_days,
retention_days=7,
)
file_handler.setFormatter(json_formatter)
file_handler.addFilter(request_filter)
biz_file_handler.setFormatter(json_formatter)
biz_file_handler.addFilter(trace_filter)
# ---- 访问日志文件 Handler (app-access) ----
access_file_handler = DateSizeRotatingFileHandler(
log_dir=settings.log_dir,
file_prefix="app-access",
max_bytes=settings.log_max_bytes,
retention_days=15,
)
access_file_handler.setFormatter(json_formatter)
access_file_handler.addFilter(trace_filter)
# ---- 错误日志文件 Handler (app-error) ----
error_file_handler = DateSizeRotatingFileHandler(
log_dir=settings.log_dir,
file_prefix=settings.log_error_file_prefix,
file_prefix="app-error",
max_bytes=settings.log_max_bytes,
retention_days=settings.log_retention_days,
retention_days=30,
)
error_file_handler.setLevel(logging.ERROR)
error_file_handler.setFormatter(json_formatter)
error_file_handler.addFilter(request_filter)
error_file_handler.addFilter(trace_filter)
# ---- 注册 Handler ----
root_logger.addHandler(console_handler)
root_logger.addHandler(file_handler)
root_logger.addHandler(biz_file_handler)
root_logger.addHandler(access_file_handler)
root_logger.addHandler(error_file_handler)
for logger_name in ("uvicorn", "uvicorn.error", "uvicorn.access"):
logger = logging.getLogger(logger_name)
logger.handlers.clear()
logger.propagate = True
# ---- 访问日志 Logger 独立路由到访问日志文件 ----
access_logger = logging.getLogger("app.access")
access_logger.propagate = False # 不向 root 传播,避免重复写入业务日志
access_logger.addHandler(console_handler)
access_logger.addHandler(access_file_handler)
# 访问日志中的 ERROR 也要进错误日志
access_logger.addHandler(error_file_handler)
# ---- 框架类 Logger 降级 ----
for logger_name in ("uvicorn", "uvicorn.error", "uvicorn.access"):
lg = logging.getLogger(logger_name)
lg.handlers.clear()
lg.propagate = True
# 框架类日志归入系统日志,生产环境设为 WARN
logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
logging.getLogger("psycopg.pool").setLevel(logging.ERROR)
logging.getLogger("httpx").setLevel(logging.WARNING)
# ---- 兼容旧文件前缀(向后兼容)----
# 如果配置了旧的 log_file_prefix也创建一个对应的 handler
if settings.log_file_prefix and settings.log_file_prefix != "app-biz":
legacy_file_handler = DateSizeRotatingFileHandler(
log_dir=settings.log_dir,
file_prefix=settings.log_file_prefix,
max_bytes=settings.log_max_bytes,
retention_days=settings.log_retention_days,
)
legacy_file_handler.setFormatter(json_formatter)
legacy_file_handler.addFilter(trace_filter)
root_logger.addHandler(legacy_file_handler)
# 旧错误日志前缀兼容
if settings.log_error_file_prefix and settings.log_error_file_prefix != "app-error":
legacy_error_handler = DateSizeRotatingFileHandler(
log_dir=settings.log_dir,
file_prefix=settings.log_error_file_prefix,
max_bytes=settings.log_max_bytes,
retention_days=settings.log_retention_days,
)
legacy_error_handler.setLevel(logging.ERROR)
legacy_error_handler.setFormatter(json_formatter)
legacy_error_handler.addFilter(trace_filter)
root_logger.addHandler(legacy_error_handler)