FastAPI 接入 OpenTelemetry 的核心不是“把 SDK 装上”,而是建立一条可验证、可限流、不会泄露敏感数据的遥测链路:应用创建标准 HTTP span,关键业务步骤补充自定义 span,经 OTLP 批量送到 Collector,再由 Collector 过滤、采样并转发。上线前应同时验证上下文传播、异常状态、后台任务、基数和脱敏,而不能只看追踪后端里是否出现一条记录。
FastAPI 链路追踪应该解决什么问题?
一次 API 请求常跨过反向代理、FastAPI、MongoDB、Redis 和外部模型服务。只记录一条“请求耗时 800ms”无法回答耗时落在哪一段、是否发生重试、错误是否来自下游,也无法把同一用户动作的多条日志串起来。分布式追踪把一次调用表示为 trace,把每个可观测步骤表示为 span,并通过 traceparent 等传播字段维持父子关系。
OpenTelemetry 的 HTTP semantic conventions 已定义 HTTP client/server span 的命名和属性边界。工程上应优先复用这些稳定语义,避免自造 method_name、status_code_text 一类难以跨服务查询的字段。若还没有稳定的异步执行边界,可先参考站内的 FastAPI 后台任务架构指南,把任务生命周期和请求生命周期分清,再决定 span 应在哪里结束。
自动插桩和手动插桩该怎么分工?
自动插桩适合覆盖框架级事实:路由、HTTP 方法、响应状态、时长和传播上下文。手动插桩适合表达业务步骤,例如“加载检索候选”“调用模型”“写入发布快照”。两者不是二选一:只做自动插桩会得到大量相似 HTTP span,却看不见真正的业务瓶颈;全部手写又容易漏掉异常、传播和规范字段。
OpenTelemetry FastAPI instrumentation 文档 同时提供 FastAPIInstrumentor.instrument_app(app)、排除 URL、请求/响应 hook 和 header 脱敏配置。最小接入可以从显式代码开始:
from fastapi import FastAPI
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
resource = Resource.create({"service.name": "catalog-api"})
provider = TracerProvider(resource=resource)
provider.add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(endpoint="http://otel-collector:4318/v1/traces")
)
)
trace.set_tracer_provider(provider)
app = FastAPI()
FastAPIInstrumentor.instrument_app(
app,
excluded_urls="/healthz,/metrics",
http_capture_headers_sanitize_fields=["authorization", "cookie", "set-cookie"],
)
tracer = trace.get_tracer("catalog-api")
这里使用 BatchSpanProcessor 而不是同步导出,是为了避免每个请求等待网络发送;service.name 则是多数后端聚合服务的基本维度。OpenTelemetry Python exporter 文档 推荐生产环境把 OTLP 发送到 Collector,并给出了 HTTP/protobuf 和 gRPC 两种导出方式。示例端点只表示拓扑,不代表任何真实生产地址。
如何给业务步骤添加有用的 span?
span 应围绕可独立诊断的工作单元,而不是给每个函数都加装饰器。名称保持低基数,例如 catalog.search,把具体商品 ID、查询文本或用户邮箱放进日志或受控字段,而不是放进 span 名称。
@app.get("/search")
async def search(q: str):
with tracer.start_as_current_span("catalog.search") as span:
span.set_attribute("search.mode", "hybrid")
span.set_attribute("search.query_length", len(q))
try:
rows = await repository.search(q)
except TimeoutError as exc:
span.record_exception(exc)
span.set_status(trace.Status(trace.StatusCode.ERROR))
raise
span.set_attribute("search.result_count", len(rows))
return {"items": rows}
不要记录原始查询、Authorization、Cookie、完整数据库语句或模型 prompt,除非已经完成数据分类、保留周期和访问控制。OpenTelemetry 明确指出,框架无法自动判断业务中的敏感数据,实施者仍需负责审查插桩输出。对于长连接,还要避免把每条消息无限追加到同一个 span;可以结合 FastAPI SSE 生产指南 的连接生命周期,把握手、会话和单次发送拆成可控层级。
Collector 为什么不该被跳过?
应用直发某个厂商后端虽然能快速看到数据,却把重试、凭据、采样和后端切换耦合进应用。Collector 提供 receiver、processor、exporter 和 pipeline 四类组件;Collector 配置文档 还强调,组件只有被加入 service.pipelines 才真正启用。
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
memory_limiter:
check_interval: 1s
limit_mib: 512
spike_limit_mib: 128
batch: {}
exporters:
otlp:
endpoint: tracing-backend:4317
tls:
insecure: false
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [otlp]
这是一份结构示例,不应原样复制到公网。官方配置示例为方便展示可能监听 0.0.0.0,但文档同时提醒:客户端都在本机时应优先绑定 localhost。生产环境还需要 TLS、认证、网络策略、资源上限和队列监控。若应用已把 Redis 用作缓存或队列,可结合 Redis 8.6 升级指南 对比业务缓存与遥测缓冲的故障域,不要让 Collector 的积压反过来拖垮在线 Redis。
采样怎么设置才不会漏掉关键错误?
全量追踪适合开发环境和短时排障,不是默认的成本策略。head sampling 在 trace 开始时决定保留比例,简单高效,但看不到完整 trace 的最终错误和总延迟;tail sampling 等待更多 span 后再决定,能优先保留错误或慢请求,却需要 Collector 缓存状态并承担额外资源成本。OpenTelemetry sampling 指南 对两者的边界有明确说明。
一个稳妥的演进顺序是:
- 在测试环境短时全量,验证传播和字段。
- 生产先用一致概率 head sampling,记录实际流量与成本。
- 若必须保证错误和慢请求可见,再在集中式 Collector 引入 tail sampling。
- 每次改规则都用固定请求集检查采样是否偏向某条路由、租户或状态码。
不要同时在多个层级独立随机采样,否则一条完整 trace 可能只剩零散 span。也不要把“采样率 10%”理解为“每类错误都有 10%”;低频错误可能长时间完全没有样本。
如何避免敏感数据和高基数失控?
高基数通常来自 URL 中的资源 ID、用户 ID、任务 ID、异常消息和原始 header。它会让索引、聚合与账单快速膨胀。路由 span 应使用模板 /users/{user_id},不要使用真实路径 /users/84721;业务标识若确需关联,优先保存不可逆的稳定散列或在受控日志系统中关联。
OpenTelemetry 敏感数据指南 列出了 attributes、filter、redaction 和 transform processor。实际门禁应采用 allowlist 思路:先列出允许上报的 header 和业务属性,再删除其余字段;仅靠黑名单很容易遗漏新的认证字段。必须特别检查:
authorization、cookie、set-cookie与 API key;- URL query、数据库参数和错误堆栈中的个人信息;
- 模型输入、检索文档片段与上传文件名;
user.id、tenant.id等会产生无限组合的属性;- Collector 自身日志是否再次打印被拒绝的数据。
后台任务和异常为什么经常断链?
请求返回后,父 span 可能已经结束。若把 FastAPI BackgroundTasks、队列任务或自建线程视为“仍在请求内部”,追踪后端会出现超长 span、错误父子关系或孤儿 span。正确做法取决于语义:同进程短任务可以继承当前 context,但应在任务执行时创建独立 span;跨进程队列要显式注入和提取传播 context;与请求无强因果关系的周期任务则应开启新 trace,并用 link 记录关联。
异常处理也不能只依赖 HTTP 500。被业务层转换成 200 的失败需要显式状态或事件;而 404、409 等预期业务响应不一定应该标成 trace error。状态策略应与 SLO 一致,并用测试固定下来。
上线前怎样验证,而不是“看起来有 trace”?
建议准备一组可重复的验收请求:正常 GET、校验失败、未捕获异常、下游超时、并发请求、后台任务和客户端主动取消。对每个场景检查:
| 门禁 | 通过条件 |
|---|---|
| 传播 | 同一次调用的 server、database、cache、HTTP client span 共用 trace ID |
| 父子关系 | 后台任务和重试没有错误挂到已结束的 span |
| 错误 | 异常被记录且状态符合约定,不泄露请求正文 |
| 命名 | span 名称和关键属性保持低基数 |
| 脱敏 | Cookie、token、个人信息和模型输入不可见 |
| 导出 | Collector 短暂不可用时应用延迟没有显著放大 |
| 采样 | 错误与慢请求保留策略可由固定样本验证 |
| 关闭 | 进程优雅退出时处理器有界 flush,不无限等待 |
最后还要给遥测链路本身加监控:导出失败数、丢弃 span 数、Collector 队列、内存和后端拒绝率。OpenTelemetry 的价值不在于生成漂亮瀑布图,而在于让故障定位从猜测变成可重复的证据链;若证据链本身没有容量、隐私和失败门禁,它就只是另一套不稳定依赖。
版本升级怎样避免仪表盘悄悄失真?
OpenTelemetry API、SDK、contrib instrumentation 与 semantic conventions 并不总在同一稳定阶段。升级前应保存一份“追踪契约”:固定请求生成哪些 span、span 的名称与父子关系、哪些属性必须存在、哪些敏感字段必须不存在。候选版本在隔离环境重放同一请求集,将结构化 span 与基线比较;只有有意变化得到迁移说明和查询适配后才发布。
依赖文件要锁定 instrumentation、exporter 与 SDK 的兼容组合,Collector 镜像也应使用明确版本而不是浮动标签。仪表盘和告警查询最好围绕稳定 semantic convention 字段,并对未知字段保持容错。若一次升级同时更换后端、采样和属性命名,出现数据下降时几乎无法定位原因,因此应把它们拆成独立、可回退的变更。
还应检查升级期间的双写窗口:新旧字段同时存在会提高数据量,旧查询过早删除又会制造监控盲区。先让新字段完成一段观察期,再迁移告警与仪表盘,最后停止旧字段;每一步都记录负责人、回退条件和预期数据量。这样即使后端聚合行为变化,也能区分真实业务波动与遥测 schema 迁移。
升级完成后保留契约报告,作为下一次变更的可比较基线。
常见问题
自动插桩会不会修改业务逻辑?
它通常通过框架中间件或 monkey patch 观测调用,但依然是运行时代码,版本和兼容性必须锁定并回归。应先在预发布环境验证中间件顺序、异常处理和延迟。
必须同时上 traces、metrics 和 logs 吗?
不必。可以先用 traces 解决跨组件延迟和错误定位,再按明确查询需求关联 metrics 与 logs。三种信号都采集但无人使用,反而会扩大成本和敏感数据面。
trace ID 可以直接返回给用户吗?
可以考虑返回无敏感含义的 request ID,并在内部映射到 trace;是否直接暴露 trace ID 取决于威胁模型和后端访问控制。无论选择哪种,都不要把内部追踪链接或 Collector 凭据返回给客户端。