tracing.py 2.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. """链路追踪:trace_id 的 ContextVar 载体。
  2. trace_id 在一次请求内贯穿「HTTP 入口 → SSE 生成器 → 工具调用 → 计费落库」,
  3. 用于把散落在各层的日志串成一条线。2.0 的做法是不依赖任何第三方 tracing
  4. SDK:一个 ContextVar 加两个绑定/解绑函数即可,跨 asyncio 任务传播由
  5. ContextVar 自身保证。
  6. """
  7. from __future__ import annotations
  8. import uuid
  9. import re
  10. from contextlib import contextmanager
  11. from contextvars import ContextVar
  12. from typing import Any
  13. # 当前请求的 trace_id;未绑定时为空串(而非 None,省掉调用方的判空)
  14. _trace_id: ContextVar[str] = ContextVar("trace_id", default="")
  15. _context: ContextVar[dict[str, Any]] = ContextVar("log_context", default={})
  16. _TRACE_PATTERN = re.compile(r"[A-Za-z0-9._:-]{1,128}\Z")
  17. def normalize_trace_id(value: str | None) -> str:
  18. return value if isinstance(value, str) and _TRACE_PATTERN.fullmatch(value) else new_trace_id()
  19. def get_log_context() -> dict[str, Any]:
  20. return dict(_context.get())
  21. def bind_log_context(**fields):
  22. return _context.set({**_context.get(), **fields})
  23. def unbind_log_context(token) -> None:
  24. try:
  25. _context.reset(token)
  26. except (ValueError, LookupError):
  27. pass
  28. @contextmanager
  29. def log_context(**fields):
  30. token = bind_log_context(**fields)
  31. try:
  32. yield
  33. finally:
  34. unbind_log_context(token)
  35. def new_trace_id() -> str:
  36. """生成一个新的 trace_id(32 位十六进制,无连字符,便于塞进日志与响应头)。"""
  37. return uuid.uuid4().hex
  38. def get_trace_id() -> str:
  39. """读取当前上下文绑定的 trace_id;未绑定返回空串。"""
  40. return _trace_id.get()
  41. def bind_trace_id(trace_id: str | None) -> Any:
  42. """绑定 trace_id,返回可用于还原的 token。
  43. 调用方应在 finally 中 ``unbind_trace_id(token)``。用 token 还原而不是
  44. 直接重置为空串,是为了支持嵌套场景(外层已有 trace 时内层不要抹掉)。
  45. """
  46. return _trace_id.set(normalize_trace_id(trace_id))
  47. def unbind_trace_id(token: Any) -> None:
  48. """还原到 ``bind_trace_id`` 之前的 trace_id。"""
  49. try:
  50. _trace_id.reset(token)
  51. except (ValueError, LookupError):
  52. # token 已被消费或来自别的上下文:静默降级,不影响主流程
  53. pass
  54. def trace_headers(trace_id: str | None = None) -> dict[str, str]:
  55. """构造向下游(MCP / 外部 HTTP)透传 trace_id 的请求头。"""
  56. tid = trace_id or get_trace_id()
  57. return {"X-Trace-Id": tid} if tid else {}