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_namestatus_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 指南 对两者的边界有明确说明。

一个稳妥的演进顺序是:

  1. 在测试环境短时全量,验证传播和字段。
  2. 生产先用一致概率 head sampling,记录实际流量与成本。
  3. 若必须保证错误和慢请求可见,再在集中式 Collector 引入 tail sampling。
  4. 每次改规则都用固定请求集检查采样是否偏向某条路由、租户或状态码。

不要同时在多个层级独立随机采样,否则一条完整 trace 可能只剩零散 span。也不要把“采样率 10%”理解为“每类错误都有 10%”;低频错误可能长时间完全没有样本。

如何避免敏感数据和高基数失控?

高基数通常来自 URL 中的资源 ID、用户 ID、任务 ID、异常消息和原始 header。它会让索引、聚合与账单快速膨胀。路由 span 应使用模板 /users/{user_id},不要使用真实路径 /users/84721;业务标识若确需关联,优先保存不可逆的稳定散列或在受控日志系统中关联。

OpenTelemetry 敏感数据指南 列出了 attributes、filter、redaction 和 transform processor。实际门禁应采用 allowlist 思路:先列出允许上报的 header 和业务属性,再删除其余字段;仅靠黑名单很容易遗漏新的认证字段。必须特别检查:

  • authorizationcookieset-cookie 与 API key;
  • URL query、数据库参数和错误堆栈中的个人信息;
  • 模型输入、检索文档片段与上传文件名;
  • user.idtenant.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 凭据返回给客户端。