ASP.NET Core 让 OpenTelemetry 适配自定义 HTTP Header

W3C TraceContext V1 标准 直到 2021 年 11 月才发布。在此之前,很多系统使用自定义的 HTTP Header 字段进行请求和调用链路的追踪。例如 AWS S3 使用 X-Amzn-Trace-Id 进行跟踪。这篇文章告诉你在 ASP.NET 中如何把旧系统中的 X-Request-IdX-Trace-Id 嫁接到最新的 OpenTelemetry 框架上。

显然你总是能在某个地方读 HTTP Request Header 中的 X-Request-IdX-Trace-Id 字段,然后自己生成一个 traceparent 字段的。问题主要是在什么地方做这个事情。显然这个事情应该发生在 ASP.NET 及 OpenTelemetry 框架开始处理 traceparent 字段之前。那么对我们来说,这个转换操作越早进行越好,毕竟我们是增加新信息,不会造成信息损耗或丢失。

如果你的程序前面还有一层 SLB(Software Load Balancer),例如 Nginx,那你完全可以在这一层实现这个转换逻辑。

如果你的程序前面没有 SLB 了,在 ASP.NET 框架内需要处理这个问题,该怎么进行呢?

经过一番调研,发现 HttpContextFactory 是一个比较合适的地方。HttpContextFactory 创建了 HttpContext,然后 ASP.NET 才开始进行 pipeline 处理。相关文档参考 ASP.NET Core Middleware。当然你在 pipeline 的最开始跑一个自己的 Middleware 干这个事情应该也是可以的,但是别人一不小心在你前面又注册个 Middleware 可能就出问题了,还是使用自定义的 HttpContextFactory 比较靠谱。

接下来考虑一下转换的逻辑。在 W3C TraceContext V1 标准 3.2 Traceparent Header 中有详细含义的解释。这里摘取一下其中给出的例子看一下。

Value = 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
base16(version) = 00
base16(trace-id) = 4bf92f3577b34da6a3ce929d0e0e4736
base16(parent-id) = 00f067aa0ba902b7
base16(trace-flags) = 01 // sampled

Value = 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00
base16(version) = 00
base16(trace-id) = 4bf92f3577b34da6a3ce929d0e0e4736
base16(parent-id) = 00f067aa0ba902b7
base16(trace-flags) = 00 // not sampled

我们使用的 X-Request-Id 其实并不能完美的对应上这里的 parent-id,但是在此时能获取到的最接近 parent-id 的含义就是它了。通常,我们在系统中使用的 X-Trace-IdX-Parent-Id 都是 UUID。这里 X-Trace-Id 正好符合 trace-id 的要求,都是 32 个 HEX。但是 X-Parent-Id 的长度就远远超出了 parent-id 的要求,可以考虑截取其中的高 16 位或者低 16 位。

样例代码

using Microsoft.AspNetCore.Http.Features;
using System.Diagnostics;

namespace Microsoft.Azure.Compute.Specialized.HpcAi..PlatformController.Middlewares
{
public class HttpContextFactory : IHttpContextFactory
{
private readonly DefaultHttpContextFactory _defaultHttpContextFactory;

public HttpContextFactory(IServiceProvider serviceProvider)
{
_defaultHttpContextFactory = new DefaultHttpContextFactory(serviceProvider);
}

public HttpContext Create(IFeatureCollection featureCollection)
{
var context = _defaultHttpContextFactory.Create(featureCollection);

// If request header has traceparent, it follows the W3C Trace Context specification.
// Else we generate traceparent from X--TraceId.
if (context.Request.Headers.ContainsKey("traceparent"))
{
context.Items["Has-W3C-Trace-Context"] = true;
return context;
}

context.Items["Has-W3C-Trace-Context"] = false;

if (!context.Request.Headers.TryGetValue("X-TraceId", out var traceId))
{
return context;
}

if (!Guid.TryParse(traceId, CultureInfo.InvariantCulture, out var _))
{
return context;
}

if (!context.Request.Headers.TryGetValue("X-RequestId", out var requestId))
{
return context;
}

if (!Guid.TryParse(requestId, CultureInfo.InvariantCulture, out var _))
{
return context;
}

context.Request.Headers.TraceParent = $"00-{traceId.ToString().Replace("-", string.Empty)}-{requestId.ToString().Replace("-", string.Empty)[15..]}-01";

return context;
}

public void Dispose(HttpContext httpContext)
{
_defaultHttpContextFactory.Dispose(httpContext);
}
}
}

如果你正确配置了 OpenTelemetry,这些字段可以在后面用 HttpContext.Features.Get<IHttpActivityFeature>()!.Activity 中的 TraceIdParentId 提取出来。