装饰器埋点不能直接return原函数结果,因需通过try/except/finally捕获返回值、异常、耗时等并统一上报,避免异常吞没、耗时为0、上下文丢失;须支持参数注入上报函数、闭包变量固化、框架隐式参数安全提取及异步适配。

装饰器埋点为什么不能直接 return 原函数结果
因为埋点逻辑必须和业务执行强绑定——不是“调用前打个日志”,而是“执行完才确认成功与否”。如果装饰器里直接 return func(*args, **kwargs),异常会被吞掉,无法捕获失败;同时耗时统计、上下文透传、错误分类都失去机会。
正确做法是用 try/except/finally 包裹执行过程,把耗时、返回值、异常类型、入参快照等统一收集。示例中常见错误是只记录开始时间,却在 return 前没更新结束时间,导致所有耗时都是 0。
- 必须在
finally或except中完成上报,否则异常中断会导致埋点丢失 - 不要在装饰器内做异步上报(如
asyncio.create_task),主线程退出后任务可能被丢弃 - 入参建议只序列化浅层结构(
str(type(arg))+len(str(arg))),避免深拷贝大对象拖慢主流程
@trace 与 @log_performance 的参数设计差异
@trace 类装饰器通常要支持业务标识(如 service_name、endpoint),用于后续归类分析;而 @log_performance 更关注阈值控制(slow_threshold_ms=500)和采样率(sample_rate=0.1),避免日志爆炸。
两者都不该把配置写死在装饰器定义里。比如硬编码 logging.info(...) 就无法对接 Sentry 或 OpenTelemetry。应通过参数注入统一的上报函数:
def trace(service_name: str, reporter: Callable = default_reporter):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# ...
reporter(event_type="enter", service=service_name, args=args)
# ...
return wrapper
return decorator
-
reporter参数必须可替换,否则无法做单元测试或灰度开关 - 避免用
**kwargs透传装饰器参数(如@trace(timeout=30)),容易和业务函数的**kwargs冲突 - 若需动态获取 trace_id,应从
contextvars.ContextVar读取,而非依赖全局变量或 threading.local
带参数的装饰器如何避免闭包变量污染
写成三层嵌套时(def decorator_factory(...): def decorator(func): def wrapper(...): ...),中间层的参数容易被多个被装饰函数共享。典型现象是:A 函数设置了 env="prod",B 函数设为 "test",但实际运行时 B 仍读到 "prod"。
根本原因是 wrapper 函数闭包捕获的是外层变量引用,而非值。修复方式只有两种:
- 用默认参数固化值:
def wrapper(..., _env=env, _service=service_name): - 用
functools.partial绑定参数,比手动写三层更安全 - 绝对不要在 wrapper 外部修改
env这类变量,哪怕只是调试打印
另外,@wraps(func) 必须放在最内层 wrapper 上,否则 inspect.signature 拿不到原函数签名,影响 FastAPI 或 Pydantic 的自动文档生成。
Flask / Django 路由函数埋点的特殊处理
Web 框架路由函数常有隐式参数(如 Flask 的 request、Django 的 request 对象),它们不能直接序列化。若装饰器 naïve 地打印 args,会看到 <request></request> 这种无意义字符串。
正确做法是提取关键字段:HTTP 方法、路径、查询参数长度、请求体大小、响应状态码。但注意——Django 中 request.body 是 bytes 流,读一次就空了;Flask 的 request.get_data() 默认不缓存,重复调用会报错。
- 应在
before_request钩子中提前读取并缓存 body(仅限小请求),或改用request.headers.get("X-Trace-ID")这类轻量字段 - 不要在装饰器里调用
request.json,它会触发解析,可能抛出BadRequest并打断正常流程 - 对异步视图(
async def),装饰器必须用async def wrapper,且内部 await 上报函数,否则事件循环会卡住
真正难的不是加装饰器,而是让埋点数据能和链路追踪 ID、用户 ID、设备指纹对齐。这些上下文往往散落在中间件、全局变量、甚至 HTTP header 里,漏一个,排查时就断一环。










