了解OpenTelemetry的朋友应该知道,为了将率属于同一个请求的多个操作(Span)串起来,上游应用会生成一个唯一的TraceId。在进行跨应用的Web调用时,这个TraceId和代表跟踪操作标识的SpanID一并发给目标应用,W3C还专门指定了一份名为Trace Context的标准,该标准确定了一个名为trace-parent的请求报头来传递TraceId、(Parent)SpanID以及其他两个跟踪属性。其实我们的应用也可能会使用到分布式跟踪这种类似的功能,我们需要在某个应用中添加一些“埋点”,当它调用另一个应用时,这些埋点会自动添加到请求的报头集合中,从而实现在整个调用链中自动传递。为了实现这个功能,我创建了一个名为HeaderForwarder(Github)的框架。本文不会介绍HeaderForwarder的设计,仅仅介绍它的使用方式,有兴趣的朋友可以查看源代码。
一、 请求报头的自动转发
二、 屏蔽自动转发功能
三、 为请求添加请求报头
四、 同名报头的处理
五、 屏蔽“外部”添加的请求报头
我们创建App1、App2和App3三个应用,ASP.NET Core应用App2和App3以路由的形式提供一个简单的API,App1则是一个简单的控制台应用。App1利用HttpClient调用App2承载的API,后者进一步调用App3。我们让处于中间的App2安装HeaderForwarder。如下所示的是控制台应用App1的定义。我们利用创建的HttpClient调用App2承载的API,发送的请求中人为添加了名为 “foo” 、“bar” 和 “baz” 的三个报头。
var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost:5000/test");
request.Headers.Add("foo", "123");
request.Headers.Add("bar", "456");
request.Headers.Add("baz", "789");
using (var httpClient = new HttpClient())
{
await httpClient.SendAsync(request);
}
App2定义如下。HeaderForwarder设计的服务通过调用IServiceCollection接口的AddHeaderForwarder进行注册,该方法中同时指定了需要自动转发的报头名称 “foo” 和 “bar” (不区分大小写)。后面调用AddHttpClient扩展方法是为了使用注入的IHttpClientFactory对象所需的HttpClient对象。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHeaderForwarder("foo", "bar").AddHttpClient();
var app = builder.Build();
app.MapGet("/test", async (HttpRequest request, IHttpClientFactory httpClientFactory) =>
{
foreach (var kv in request.Headers)
{
Console.WriteLine($"{kv.Key}:{kv.Value}");
}
await httpClientFactory.CreateClient().GetAsync("http://localhost:5001/test");
});
app.Run("http://localhost:5000");
App1调用的API体现为针对路径 “/test” 注册的路由。路由处理程序会再控制台上输出接收到的所有请求报头,并在此之后利用IHttpClientFactory对象创建的HttpClient完成针对App3的调用。App3提供的API仅仅按照如下的方式将接收到的请求报头输出到控制台上。
var app = WebApplication.CreateBuilder(args).Build();
app.MapGet("/test", (HttpRequest request) =>
{
foreach (var kv in request.Headers)
{
Console.WriteLine($"{kv.Key}:{kv.Value}");
}
});
app.Run("http://localhost:5001");
三个应用先后启动后,App1调用App2添加的三个请求报头(“foo” 、 “bar” 和 “baz”)会出现在App2的控制台上。HeaderForwarder只会自动转发指定的请求报头“foo” 和“bar” ,所有只有这两个报头会出现在App3的控制台上。从图中还可以看到,默认由HttpClientFactory创建的HttpClient的调用添加和转发用于分布式跟踪的traceparent报头。
HeaderForwarder能够获得当前的HttpContext上下文,并提取并转发所需的请求报头。如果App2在调用App3的时候并不希望将报头转发出去,可以按照如下的方式注入IOutgoingHeaderProcessor对象,并调用其SuppressHeaderForwarder方法将报头自动转发功能屏蔽掉。
using HeaderForwarder;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHeaderForwarder("foo", "bar").AddHttpClient();
var app = builder.Build();
app.MapGet("/test", async (IHttpClientFactory httpClientFactory,IOutgoingHeaderProcessor processor ) =>
{
using (processor.SuppressHeaderForwarder())
{
await httpClientFactory.CreateClient().GetAsync("http://localhost:5001/test");
}
});
app.Run("http://localhost:5000");
SuppressHeaderForwarder利用返回的IDisposable对象代表“屏蔽上下文”,意味着该创建的“屏障”会在其Dispose方法后失效,所以App2在此上下文中完成针对App3的调用,它接收的请求报头“foo” 和“bar”并不会被转发出去。
当我们利用HttpClient进行Web调用时,如果需要认为地添加报头,典型的做法就是按照App1异常创建一个HttpRequestMessage对象,并将需要的报头以键值对的形式添加到它的Headers属性中。HeaderForwarder提供了一种更加快捷易用的编程模式。
var processor = OutgoingHeaderProcessor.Create();
using(var httpClient = new HttpClient())
using (processor.AddHeaders(("foo", "123"), ("bar", "456"), ("baz", "789")))
await httpClient.GetAsync("http://localhost:5000/test");
如上面的代码片段所示,我们调用OutgoingHeaderProcessor类型的静态方法Create创建了一个IOutgoingHeaderProcessor对象,并调用其AddHeaders完成了三个请求报头的添加。这个方法同样返回一个通过IDisposable对象表示的执行上下文,在此上下文中针对HttpClient的调用生成的请求均会自动附加这三个报头。
由于IOutgoingHeaderProcessor接口的AddHeaders方法返回的时一个IDisposable对象表示的上下文,意味着上下文之间可能出现嵌套的关系。在默认情况下,如果HttpClient在这样一个嵌套的上下文中被使用,这些上下文携带的请求报头都将被转发。一般来说,这种情况正是我们希望的,但是如果我们在一个具有嵌套关系的多个上下文中添加了多个同名的报头,就有可能出现我们不愿看到的结果。
using HeaderForwarder;
var processor = OutgoingHeaderProcessor.Create();
using(var httpClient = new HttpClient())
await FooAsync(httpClient);
async Task FooAsync(HttpClient httpClient)
{
using (processor.AddHeaders(("foobarbaz", "abc")))
await BarAsync(httpClient);
}
async Task BarAsync(HttpClient httpClient)
{
using (processor.AddHeaders(("foobarbaz", "abc")))
await BazAsync(httpClient);
}
async Task BazAsync(HttpClient httpClient)
{
using (processor.AddHeaders(("foobarbaz", "abc")))
await httpClient.GetAsync("http://localhost:5000/test");
}
如上面的代码所示,三个嵌套调用的方法FooAsync、BarAsync和BazAsync采用相同的方式调用IOutgoingHeaderProcessor对象的AddHeaders方法添加相同的请求报头“foobarbaz”。意味着在BazAsync方法针对HttpClient的调用会在三个嵌套的上下文中进行,这意味着App2会接收到三个同名的请求报头。
如果不希望出现这种情况下,可以将针对AddHeaders方法的调用按照如下的方式替换成ReplaceHeaders。
async Task FooAsync(HttpClient httpClient)
{
using (processor.ReplaceHeaders(("foobarbaz", "abc")))
await BarAsync(httpClient);
}
async Task BarAsync(HttpClient httpClient)
{
using (processor.ReplaceHeaders(("foobarbaz", "abc")))
await BazAsync(httpClient);
}
async Task BazAsync(HttpClient httpClient)
{
using (processor.ReplaceHeaders(("foobarbaz", "abc")))
await httpClient.GetAsync("http://localhost:5000/test");
}
如果不愿意收到嵌套的“外部”上下文的干扰,我们可以调用IOutgoingHeaderProcessor接口的AddHeadersAfterClear方法。顾名思义,这个方法在添加指定请求报头之前,会先将现有的报头清除。
var processor = OutgoingHeaderProcessor.Create();
using(var httpClient = new HttpClient())
await FooAsync(httpClient);
async Task FooAsync(HttpClient httpClient)
{
using (processor.AddHeadersAfterClear(("foo", "123")))
await BarAsync(httpClient);
}
async Task BarAsync(HttpClient httpClient)
{
using (processor.AddHeadersAfterClear(("barbaz", "456")))
await BazAsync(httpClient);
}
async Task BazAsync(HttpClient httpClient)
{
using (processor.AddHeadersAfterClear(("barbaz", "789")))
await httpClient.GetAsync("http://localhost:5000/test");
}
如上面的代码片段所示,FooAsync调用AddHeadersAfterClear方法添加了一个名为“foo”的报头,BarAsync和BazAsync则采用相同的方式添加了两个同名的请求报头“Barbaz”。App2只会接收到由BazAsync设置的报头。
AddHeadersAfterClear针对现有报头的清除只会体现在它创建的上下文中,当前上下文并不会收到影响。因为该方法根本没有做任何清除工作,而是创建一个全新的上下文。AddHeaders和ReplaceHeaders方法其实重用了外部的上下文。