修改用户设置的新增用户的权限点击操作

This commit is contained in:
wangjiming
2026-08-17 16:04:04 +08:00
parent 6c1bf61ff7
commit 4e27b98a84
12 changed files with 1065 additions and 192 deletions

View File

@@ -8,7 +8,7 @@ from logging import Handler, LogRecord
from pathlib import Path
import re
import time
from typing import Any
from typing import Any, Callable, Optional
from uuid import uuid4
from fastapi import FastAPI, Request
@@ -17,6 +17,74 @@ from app.core.config import Settings, get_settings
request_id_var: ContextVar[str] = ContextVar("request_id", default="-")
# ==================== 敏感数据脱敏规则 ====================
SENSITIVE_PATTERNS: dict[str, Callable | str] = {
"token": "***",
"password": "***",
"access_token": "***",
"refresh_token": "***",
"secret_key": "***",
"authorization": "***",
"bearer": "***",
"api_key": "***",
"private_key": "***",
}
def mask_value(key: str, value: Any) -> str:
"""对单个值进行脱敏处理"""
if value is None:
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
def mask_sensitive_dict(data: dict) -> dict:
"""递归脱敏字典中的敏感字段"""
if not data or not isinstance(data, dict):
return data
result = {}
for key, value in data.items():
result[key] = mask_value(key, value)
return result
def mask_sensitive_string(text: str) -> str:
"""从文本中脱敏常见敏感信息"""
if not text:
return text
patterns = [
(r'Bearer\s+[A-Za-z0-9\-._]+', '***'),
(r'token\s*[:=]\s*', '***'),
(r'password\s*[:=]\s*', '***'),
(r'secret[_-]?key\s*[:=]', '***'),
(r'api[-_]?key\s*[:=]', '***'),
(r'private[_-]?key\s*[:=]', '***'),
(r'\d{11}', r'\d{3}\*\d{4}'), # 手机号/身份证
(r'1[3-9]\d{9}', r'1\*{3}\*{4}'), # 手机号
]
for pattern, replacement in patterns:
try:
text = re.sub(pattern, replacement, text, flags=re.IGNORECASE)
except re.error:
pass
return text
# ==================== RequestId Filter ====================
class RequestIdFilter(logging.Filter):
def filter(self, record: LogRecord) -> bool:
@@ -24,8 +92,30 @@ class RequestIdFilter(logging.Filter):
return True
# ==================== Enhanced JSON Formatter ====================
class JsonLogFormatter(logging.Formatter):
"""Format one JSON object per line for ELK/Filebeat collection."""
"""
增强的 JSON 日志格式化器,支持结构化字段输出。
输出示例:
{
"@timestamp": "2026-08-17T18:30:00.123Z",
"level": "INFO",
"logger": "dataset.router",
"message": "数据集创建成功",
"module": "dataset.router",
"function": "create_dataset",
"file": "dataset/router.py",
"line": 45,
"process": 12345,
"thread": "MainThread",
"request_id": "req-abc123",
"user_id": "u_admin",
"client_ip": "192.168.1.100",
"extra": {...}
}
"""
def format(self, record: LogRecord) -> str:
payload: dict[str, Any] = {
@@ -44,13 +134,26 @@ class JsonLogFormatter(logging.Formatter):
"thread_name": record.threadName,
"request_id": getattr(record, "request_id", "-"),
}
# 从 record 中提取额外字段(通过 extra 参数传入)
for attr in ("user_id", "client_ip", "target_type", "target_id",
"duration_ms", "status_code", "error"):
val = getattr(record, attr, None)
if val is not None:
payload[attr] = val
# 处理异常信息
if record.exc_info:
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."""
@@ -160,6 +263,114 @@ class DateSizeRotatingFileHandler(Handler):
path.unlink(missing_ok=True)
# ==================== Structured Logger 封装 ====================
class StructuredLogger:
"""
结构化日志记录器,提供统一的日志接口。
使用方式:
logger = get_structured_logger('dataset.router')
logger.info('创建数据集', dataset_id='ds_123')
"""
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 error(self, message: str, **extra: Any) -> None:
self._log("ERROR", message, **extra)
def debug(self, message: str, **extra: Any) -> None:
self._log("DEBUG", message, **extra)
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 get_structured_logger(name: str, module: str = "") -> StructuredLogger:
"""获取结构化日志记录器"""
return StructuredLogger(name, module)
# ==================== 快捷函数 ====================
def get_logger(name: str) -> logging.Logger:
"""获取标准 Python logger"""
return logging.getLogger(name)
def set_request_id(request_id: str) -> None:
"""设置当前请求的追踪 ID"""
request_id_var.set(request_id)
def setup_request_logging(app: FastAPI) -> None:
"""配置 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)
started_at = time.perf_counter()
try:
response = await call_next(request)
elapsed_ms = (time.perf_counter() - started_at) * 1000
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.debug
if response.status_code >= 400:
log_method = logger.warning
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 "-",
)
response.headers["X-Request-ID"] = request_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 "-",
)
raise
finally:
request_id_var.reset(token)
# ==================== 配置函数 ====================
def configure_logging(settings: Settings | None = None) -> None:
settings = settings or get_settings()
@@ -209,57 +420,5 @@ def configure_logging(settings: Settings | None = None) -> None:
logger.handlers.clear()
logger.propagate = True
logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
logging.getLogger("psycopg.pool").setLevel(logging.ERROR)
def get_logger(name: str) -> logging.Logger:
return logging.getLogger(name)
def set_request_id(request_id: str) -> None:
request_id_var.set(request_id)
def setup_request_logging(app: FastAPI) -> None:
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)
started_at = time.perf_counter()
try:
response = await call_next(request)
elapsed_ms = (time.perf_counter() - started_at) * 1000
# Docker/frontend probes and polling endpoints are intentionally
# quiet at INFO; failures remain visible at WARNING/ERROR.
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.debug
if response.status_code >= 400:
log_method = logger.warning
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 "-",
)
response.headers["X-Request-ID"] = request_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 "-",
)
raise
finally:
request_id_var.reset(token)
logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
logging.getLogger("psycopg.pool").setLevel(logging.ERROR)