在分布式追踪中,Span 记录的是单个操作元数据。很多自动埋点工具会默认采集 HTTP 方法、URL、耗时等信息,但在实际排障时,最需要的往往不是这些。比如一个接口返回 503,但自动埋点只展示了请求耗时变长,没有把状态码写入 Span 属性;当你在追踪平台筛选所有 5xx 请求时,这些 Span 就会被漏掉。手动把状态码和错误信息补充到 Span,可以让检索条件变得精确。
另一个原因是网络请求的状态不一定体现在异常上。Ruby 的 Net::HTTP 在收到 4xx、5xx 响应时不会主动抛异常,只有连接超时、DNS 解析失败等底层错误才会进入 rescue 分支。如果只围绕异常来记录错误,很多服务端明确返回失败的业务请求就丢失了。把状态码视为一等属性,而不是只关心异常,是更贴近真实流量的做法。

从可观测性角度看,Span 属性应该具备可聚合性。http.status_code 可以用数字直接参与筛选和统计,比如按 502、503 分桶查看影响范围;error.message 虽然高基数,但能提供堆栈之外的上下文。两者配合,既能快速缩小范围,又能进入具体请求定位根因。
从Ruby网络请求中提取状态码和错误信息
以最基础的 Net::HTTP 为例,响应对象提供 code 和 message 两个方法。需要注意 code 返回字符串,例如 404 对应的是 404,写入 Span 时通常要转成整数。错误信息的来源则要区分两种:一种是连接层异常,例如 Errno::ECONNREFUSED、Net::OpenTimeout;另一种是 HTTP 响应体中的错误描述,这部分并没有统一标准,需要根据接口约定截取关键字段。
对于 Faraday 这类封装更完善的客户端,响应结构更统一。Faraday::Response 的 status 直接返回整数,body 通常是想解析的内容。如果使用 raise_error 中间件,Faraday 会在状态码大于等于 400 时抛出 Faraday::ServerError 或 Faraday::ClientError,异常对象本身带有 response 属性,这样既能拿到状态码,也能从异常中恢复原始响应。下面代码演示了从 Net::HTTP 和 Faraday 中提取关键信息并交给 SetAttribute 方法的过程。
require "opentelemetry"
def write_span_attrs_from_response(span, response, error = nil)
if response
status_code = if response.respond_to?(:code)
response.code.to_i
elsif response.respond_to?(:status)
response.status.to_i
else
0
end
span.set_attribute("http.status_code", status_code)
if response.respond_to?(:request) && response.request.respond_to?(:method)
span.set_attribute("http.method", response.request.method.to_s.upcase)
end
end
if error
span.set_attribute("error.message", error.message)
span.record_exception(error)
end
end
上面的方法将响应对象和异常对象分开处理,原因是很多场景下二者并不同时存在。比如请求成功拿到 200 时没有异常;连接被拒绝时只有异常没有响应;而服务端返回 500 时既有响应对象,也没有抛出异常,除非显式启用了 raise_error。因此不要在方法内部假设响应一定存在。
如果请求体很大,不建议把整个 body 写入 error.message。可以限制长度,例如取前 500 个字符,或者只提取 JSON 中的 error 字段。错误信息的重点是可读性,而不是完整还原响应。
在Rack中间件里自动写入Span属性
手动在每次请求后写入属性容易遗漏,更稳定的做法是在 Rack 层统一处理。一个 Rack 中间件可以包裹整个应用,在请求开始前从环境变量里拿到当前 Span,在响应返回时写入状态码,在异常被抛出时写入错误信息。Rack 的 env 里通常可以通过 rack.response 或直接读取 status 来获取状态码。
下面是一个最小可用的中间件示例。它不依赖具体的 Web 框架,Rails、Sinatra、Grape 都能使用。代码中把 Span 获取放在应用调用之后,因为只有进入请求上下文,OpenTelemetry 才会创建或传播当前 Span。异常处理分支必须 raise 重新抛出,否则会影响上层框架的状态码和错误页逻辑。
require "opentelemetry"
class TraceSpanAttrMiddleware
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
span = OpenTelemetry::Trace.current_span
if span && status
span.set_attribute("http.status_code", status.to_i)
end
[status, headers, body]
rescue Exception => e
span = OpenTelemetry::Trace.current_span
if span
span.set_attribute("error.message", e.message)
span.record_exception(e)
end
raise
end
end
需要注意的是,OpenTelemetry::Trace.current_span 可能返回一个无效的 Span 或 nil。在 OpenTelemetry Ruby SDK 中,如果没有活跃的 Span,返回的是一个无效 Span 而不是 nil,但对无效 Span 调用 set_attribute 不会报错,只是静默丢弃。为了代码可读性,这里仍用 if span 判断,可以兼容其他追踪库。
如果应用里已经使用了 ActionDispatch::DebugExceptions 之类的异常处理中间件,记得把自定义中间件放在它之前。Rack 中间件的顺序决定了异常是否会被你的代码捕获到。Rails 中可以通过 config.middleware.insert_before 来插入,具体位置要保证不会绕过框架自身的异常报告。
属性命名规范与避坑建议
给 Span 添加属性时,命名不能随心所欲。OpenTelemetry 语义约定里已经定义了 http.status_code、http.method、http.url 等标准名称,自己造一个 status 或 http_code 会导致跨服务的聚合分析失效。错误信息虽然没有严格的统一规范,但使用 error.message 与 error.type 能让大多数追踪平台自动识别为异常。
属性值也有建议。状态码保持整数类型,不要写成 404 Not Found 这样的字符串,否则筛选条件可能匹配不到;错误信息要截断,高基数的堆栈文本会极大增加存储成本。如果异常包含嵌套的 cause,可以把根因写入 error.message,完整堆栈则交给 record_exception 处理。
还需要注意不要在异步任务或后台线程中直接读取当前 Span。Ruby 的线程调度下,当前 Span 的上下文可能已经切换到另一个请求,此时写入属性会污染其他链路。如果需要把 Span 传到线程里,应当使用 OpenTelemetry::Context.with_current 显式封装,或者把 Span 作为变量传递过去。重试逻辑同理,每次重试最好创建子 Span,而不是在同一个 Span 上反复覆盖状态码。
最后,如果团队使用多种 HTTP 客户端,统一封装一个 TracingHttpClient 或 Faraday 中间件,能避免每个调用点散落重复代码。核心原则是:状态码在响应返回后立即写入,错误信息在异常捕获分支里写入,两者互不替代,才能让网络 API 的 Span 真正具备排障价值。
RubyOpenTelemetryHTTP状态码修改时间:2026-10-02 09:27:16