"""结构化日志: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 "" 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)