前言
在微服务的大环境下,会出现这个服务调用这个接口,那个接口的情况。假设出了问题,需要排查的时候,我们要怎么关联不同服务之间的调用情况呢?换句话就是说,这个请求的结果不对,看看是那里出了问题。
最简单的思路应该就是请求头加一个标识,从头贯穿到尾,这样我们就可以知道,对于这一个请求,在不同的服务都经历了什么样的过程。
在.NET Core时代,相信大部分都是在用HttpClientFactory来创建HttpClient,然后在发起请求。
这篇短文就简单介绍一下如何实现。
示例
我们先定义一个自己的DelegatingHandler,这里取名为HeadersPropagationDelegatingHandler
代码如下:
public class HeadersPropagationDelegatingHandler : DelegatingHandler
{
private readonly IHttpContextAccessor _accessor;
public HeadersPropagationDelegatingHandler(IHttpContextAccessor accessor)
{
_accessor = accessor;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var traceId = string.Empty;
if (_accessor.HttpContext.Request.Headers.TryGetValue("traceId", out var tId))
{
traceId = tId.ToString();
Console.WriteLine($"{traceId} from request {DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}.");
}
else
{
traceId = System.Guid.NewGuid().ToString("N");
_accessor.HttpContext.Request.Headers.Add("traceId", new Microsoft.Extensions.Primitives.StringValues(traceId));
Console.WriteLine($"{traceId} from generated {DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}.");
}
if (!request.Headers.Contains("trace-id"))
{
request.Headers.TryAddWithoutValidation("traceId", traceId);
}
return await base.SendAsync(request, cancellationToken);
}
}
应该不用太多解释,就是在HttpClient发起请求之前,给它加多一个请求头,这个请求头的值要么是从上一个请求的请求头中取,要么就是重新生成一个。
下面就是主角IHttpMessageHandlerBuilderFilter出场了,它只是一个接口,我们需要自己去实现里面的Configure。
简单的示例如下:
public class HeadersPropagationMessageHandlerBuilderFilter : IHttpMessageHandlerBuilderFilter
{
private readonly IHttpContextAccessor httpContextAccessor;
public HeadersPropagationMessageHandlerBuilderFilter(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public Action<HttpMessageHandlerBuilder> Configure(Action<HttpMessageHandlerBuilder> next)
{
if (next == null)
{
throw new ArgumentNullException(nameof(next));
}
return (builder) =>
{
next(builder);
builder.AdditionalHandlers.Add(new HeadersPropagationDelegatingHandler(httpContextAccessor));
};
}
}
万事具备,下面我们只需要在Startup中进行注入即可。
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddTransient<Ext.HeadersPropagationDelegatingHandler>();
services.AddSingleton<IHttpMessageHandlerBuilderFilter, Ext.HeadersPropagationMessageHandlerBuilderFilter>();
services.AddHttpClient();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
最后就是调用看看效果,这里为了简单,选择创建多个路由,用路由间发起HTTP请求来模拟。当然,最好的还是多个项目模拟。
示例如下:
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
private readonly IHttpClientFactory _clientFactory;
public ValuesController(IHttpClientFactory clientFactory)
{
this._clientFactory = clientFactory;
}
// GET api/values
[HttpGet]
public async Task<string> GetAsync()
{
var traceId = string.Empty;
if (Request.Headers.TryGetValue("traceId", out var tId))
{
traceId = tId.ToString();
Console.WriteLine($"{traceId} from request {DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}.");
}
else
{
traceId = System.Guid.NewGuid().ToString("N");
Request.Headers.Add("traceId", new Microsoft.Extensions.Primitives.StringValues(traceId));
Console.WriteLine($"{traceId} from generated {DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}.");
}
using (HttpClient client = new HttpClient())
{
client.DefaultRequestHeaders.Clear();
client.DefaultRequestHeaders.TryAddWithoutValidation("traceId", traceId);
var res = await client.GetAsync("http://localhost:9898/api/values/demo1");
var str = await res.Content.ReadAsStringAsync();
Console.WriteLine($"{traceId} demo1 return {str} at {DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}");
return str;
}
}
// GET api/values/demo1
[HttpGet("demo1")]
public async Task<string> GetDemo1()
{
var client = _clientFactory.CreateClient("demo2");
var res = await client.GetAsync("http://localhost:9898/api/values/demo2");
var str = await res.Content.ReadAsStringAsync();
return str;
}
// GET api/values/demo2
[HttpGet("demo2")]
public async Task<string> GetDemo2()
{
var client = _clientFactory.CreateClient("demo3");
var res = await client.GetAsync("http://localhost:9898/api/values/demo3");
var str = await res.Content.ReadAsStringAsync();
return str;
}
// GET api/values/demo3
[HttpGet("demo3")]
public ActionResult<string> GetDemo3()
{
return "demo3";
}
// GET api/values/demo4
[HttpGet("demo4")]
public async Task<string> GetDemo4()
{
var client = _clientFactory.CreateClient("demo1");
var res = await client.GetAsync("http://localhost:9898/api/values/demo3");
var str = await res.Content.ReadAsStringAsync();
var traceId = string.Empty;
if (Request.Headers.TryGetValue("traceId", out var tId)) traceId = tId.ToString();
Console.WriteLine($"{traceId} demo3 return {str} at {DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}");
return str;
}
}
先访问 api/values
再访问 api/values/demo4
可以看到下面的结果。
可以看到用传统的方法和用HttpClientFactory都达到了一样的效果。