feat(m0): 后端骨架 + Web/安卓前端骨架 + 设计文档
- backend: FastAPI 应用工厂、统一错误信封、JWT、短信登录、工作区/成员/邀请、幂等中间件 - alembic 0001: workspace/member/invitation/audit_log/app_user + RLS 策略 + 双角色 GRANT - tests: 认证/工作区/租户隔离/幂等 四组用例(尚未在本机执行) - web: React18+Vite 骨架(登录、工作区、邀请落地页) - android: Compose 骨架(登录、工作区列表) - 2.Design: PRODUCT-PLAN / ROADMAP-M0-M1 / NAMING / DEVELOPMENT
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
"""结构化日志:JSON 行 + request_id 贯穿 + 敏感字段脱敏。
|
||||
|
||||
脱敏是**无条件**的:任何日志调用里的 dict 都会过一遍 _mask。
|
||||
不要为了方便调试关掉它——AGENTS.md 第 7 条。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from contextvars import ContextVar
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
request_id_ctx: ContextVar[str] = ContextVar("request_id", default="-")
|
||||
user_id_ctx: ContextVar[str] = ContextVar("user_id", default="-")
|
||||
workspace_id_ctx: ContextVar[str] = ContextVar("workspace_id", default="-")
|
||||
|
||||
_SENSITIVE_HINTS = (
|
||||
"password",
|
||||
"passwd",
|
||||
"token",
|
||||
"secret",
|
||||
"api_key",
|
||||
"apikey",
|
||||
"authorization",
|
||||
"cookie",
|
||||
"credential",
|
||||
"private_key",
|
||||
)
|
||||
MASK = "***"
|
||||
|
||||
|
||||
def _mask(value: Any, depth: int = 0) -> Any:
|
||||
if depth > 6:
|
||||
return "<deep>"
|
||||
if isinstance(value, dict):
|
||||
return {
|
||||
key: (MASK if any(h in str(key).lower() for h in _SENSITIVE_HINTS) else _mask(val, depth + 1))
|
||||
for key, val in value.items()
|
||||
}
|
||||
if isinstance(value, (list, tuple)):
|
||||
return [_mask(item, depth + 1) for item in value]
|
||||
return value
|
||||
|
||||
|
||||
class JsonFormatter(logging.Formatter):
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
payload: dict[str, Any] = {
|
||||
"ts": datetime.now(UTC).isoformat(timespec="milliseconds"),
|
||||
"level": record.levelname,
|
||||
"logger": record.name,
|
||||
"msg": record.getMessage(),
|
||||
"request_id": request_id_ctx.get(),
|
||||
"user_id": user_id_ctx.get(),
|
||||
"ws_id": workspace_id_ctx.get(),
|
||||
}
|
||||
extra = getattr(record, "extra_fields", None)
|
||||
if isinstance(extra, dict):
|
||||
payload.update(_mask(extra))
|
||||
if record.exc_info:
|
||||
payload["exc"] = self.formatException(record.exc_info)
|
||||
return json.dumps(payload, ensure_ascii=False, default=str)
|
||||
|
||||
|
||||
def setup_logging(level: str = "INFO") -> None:
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
handler.setFormatter(JsonFormatter())
|
||||
root = logging.getLogger()
|
||||
root.handlers = [handler]
|
||||
root.setLevel(level)
|
||||
# uvicorn 的 access log 交给自己的中间件,避免重复
|
||||
for noisy in ("uvicorn.access", "sqlalchemy.engine.Engine"):
|
||||
logging.getLogger(noisy).handlers = []
|
||||
logging.getLogger(noisy).propagate = False
|
||||
|
||||
|
||||
def get_logger(name: str) -> logging.Logger:
|
||||
return logging.getLogger(name)
|
||||
Reference in New Issue
Block a user