HttpClientFactory 日志不好用,自己扩展一个?
前言
.NetCore2.1新推出HttpClientFactory工厂类, 替代了早期的HttpClient, 并新增了弹性Http调用机制 (集成Policy组件)。
替换的初衷还是简单说下:
① using(var client= new HttpClient()) 调用Dispose()方法,并不会很快释放底层Socket连接, 同时新建Socket需要时间,这在高并发场景下Socket耗尽。 传送门
② 由于①很多人会想到用单例或静态类构建HttpClient实例,但是这里还有一个坑,HttpClient 会忽略DNS的变化。 传送门
HttpClientFactory 以一种模块化、可命名、弹性可预期的方式重建了HttpClient的使用方式。
现在的HttpClientFactory以依赖注入的方式集成到.NETCore 框架:
// 截取自Startup.cs文件服务配置部分
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient("bce-request", x =>
x.BaseAddress = new Uri(Configuration.GetSection("BCE").GetValue<string>("BaseUrl")))
.ConfigurePrimaryHttpMessageHandler(_ => new BceAuthClientHandler()
{
AccessKey = Configuration.GetSection("BCE").GetValue<string>("AccessKey"),
SerectAccessKey = Configuration.GetSection("BCE").GetValue<string>("SecretAccessKey"),
AllowAutoRedirect = true,
UseDefaultCredentials = true
})
.SetHandlerLifetime(TimeSpan.FromHours())
.AddPolicyHandler(GetRetryPolicy());
} static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy(int retry)
{
var retryPolicy = HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)
.WaitAndRetryAsync(retry, retryAttempt => TimeSpan.FromSeconds(Math.Pow(, retryAttempt)));
return retryPolicy;
}
HttpClientFactory典型用法
使用时从 IHttpClientFactory工厂创建所需HttpClient实例,发起业务端请求。
以下是利用NLog观察到的文件日志:
// :: [Info].[System.Net.Http.HttpClient.bce-request.LogicalHandler].[}].[]
Start processing HTTP request GET http://localhost:5000/v1/eqid/b827a9400004132a000000065dc26470
// :: [Info].[System.Net.Http.HttpClient.bce-request.ClientHandler].[}].[]
Sending HTTP request GET http://localhost:5000/v1/eqid/b827a9400004132a000000065dc26470
// :: [Info].[System.Net.Http.HttpClient.bce-request.ClientHandler].[}].[]
Received HTTP response after .5088ms - OK
// :: [Info].[System.Net.Http.HttpClient.bce-request.LogicalHandler].[}].[]
End processing HTTP request after .1478ms - OK
头脑风暴
观察上面单次请求的日志,由外层LogicHandler和内层ClientHandler 日志头组成。 这样的日志可以想象到有2个问题:
① 在高并发使用HttpClient,日志条数众多,没有类似TraceId 这样的机制定位 某次HttpClient调用的完整日志。
② 若是微服务/ 分布式调用,可能还有 将本次HttpClient调用日志与后置api日志 结合分析的需求, 这个日志也支持不了。
因此本文打算重新自定义HttpClientFactory日志处理器(给请求的全部日志设置TraceId),实际上CustomLoggingHttpMessageHandler只是一个引子,掌握如何扩展才是关键。
结合我给出的典型用法来看IHttpClientFactory组件原理:

示例中System.Net.Http.HttpClient.bce-request.LogicalHandler,System.Net.Http.HttpClient.bce-request.ClientHandler 日志头即是来自LoggingScopeHttpMessageHandler ,LoggingHttpMessageHandler 两个处理器,
给出手绘的UML类图:

本次要扩展的入口便是 IHttpMessageHandlerFilter接口, 核心是自定义DelegatingHandler日志处理器
编程实践
如以上分析,
P1 实现 IHttpMessageHandlerFilter接口,在接口中移除默认的两个日志处理器
public class TraceIdLoggingMessageHandlerFilter : IHttpMessageHandlerBuilderFilter
{
private readonly ILoggerFactory _loggerFactory; public TraceIdLoggingMessageHandlerFilter(ILoggerFactory loggerFactory)
{
_loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
} public Action<HttpMessageHandlerBuilder> Configure(Action<HttpMessageHandlerBuilder> next)
{
if (next == null)
{
throw new ArgumentNullException(nameof(next));
} return (builder) =>
{
// Run other configuration first, we want to decorate.
next(builder); var outerLogger =_loggerFactory.CreateLogger($"System.Net.Http.HttpClient.{builder.Name}.LogicalHandler");
builder.AdditionalHandlers.Clear();
builder.AdditionalHandlers.Insert(0,new CustomLoggingScopeHttpMessageHandler(outerLogger));
};
}
}
P2 实现带有TraceId能力的HttpClient 日志处理器, 并加入到 IHttpMessageHandlerFilter接口实现类
public class CustomLoggingScopeHttpMessageHandler : DelegatingHandler
{
private readonly ILogger _logger; public CustomLoggingScopeHttpMessageHandler(ILogger logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
} protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
} using (Log.BeginRequestPipelineScope(_logger, request))
{
Log.RequestPipelineStart(_logger, request);
var response = await base.SendAsync(request, cancellationToken);
Log.RequestPipelineEnd(_logger, response); return response;
}
} private static class Log
{
private static class EventIds
{
public static readonly EventId PipelineStart = new EventId(, "RequestPipelineStart");
public static readonly EventId PipelineEnd = new EventId(, "RequestPipelineEnd");
} private static readonly Func<ILogger, HttpMethod, Uri, string, IDisposable> _beginRequestPipelineScope =
LoggerMessage.DefineScope<HttpMethod, Uri, string>(
"HTTP {HttpMethod} {Uri} {CorrelationId}"); private static readonly Action<ILogger, HttpMethod, Uri, string, Exception> _requestPipelineStart =
LoggerMessage.Define<HttpMethod, Uri, string>(
LogLevel.Information,
EventIds.PipelineStart,
"Start processing HTTP request {HttpMethod} {Uri} [Correlation: {CorrelationId}]"); private static readonly Action<ILogger, HttpStatusCode,string,Exception> _requestPipelineEnd =
LoggerMessage.Define<HttpStatusCode,string>(
LogLevel.Information,
EventIds.PipelineEnd,
"End processing HTTP request - {StatusCode}, [Correlation: {CorrelationId}]"); public static IDisposable BeginRequestPipelineScope(ILogger logger, HttpRequestMessage request)
{
var correlationId = GetCorrelationIdFromRequest(request);
return _beginRequestPipelineScope(logger, request.Method, request.RequestUri, correlationId);
} public static void RequestPipelineStart(ILogger logger, HttpRequestMessage request)
{
var correlationId = GetCorrelationIdFromRequest(request);
_requestPipelineStart(logger, request.Method, request.RequestUri, correlationId, null);
} public static void RequestPipelineEnd(ILogger logger, HttpResponseMessage response)
{
var correlationId = GetCorrelationIdFromRequest(response.RequestMessage);
_requestPipelineEnd(logger, response.StatusCode, correlationId, null);
} private static string GetCorrelationIdFromRequest(HttpRequestMessage request)
{
string correlationId;
if (request.Headers.TryGetValues("X-Correlation-ID", out var values))
correlationId = values.First();
else
{correlationId = Guid.NewGuid().ToString(); request.Headers.Add("X-Correlation-ID",correlationId);}
return correlationId; }
}
}
以上TraceId的实现思路,参考了我前一篇博文《被忽略的TraceId,可以用起来了》的思路,为每次HttpClient调用过程设定 全局唯一的GUID标记, 后置api服务可酌情修改以上代码处理。
其中写入日志的代码Copy自HttpClientFactory源代码。
P3 在DI框架中替换原有的 IHttpMessageHandlerFilter 实现
services.Replace(ServiceDescriptor.Singleton<IHttpMessageHandlerBuilderFilter, TraceIdLoggingMessageHandlerFilter>());
发起两次HttpClient请求, 输出的日志如下:
// :: [Info].[System.Net.Http.HttpClient.bce-request.LogicalHandler].[}].[]
Start processing HTTP request GET http://localhost:5000/v1/eqid/ad78deef00444ed7000000035de704e8 [Correlation: 03de676d-680e-4a92-aef5-749bcc3ba499]
// :: [Info].[System.Net.Http.HttpClient.bce-request.LogicalHandler].[}].[]
End processing HTTP request - OK, [Correlation: 03de676d-680e-4a92-aef5-749bcc3ba499]
// :: [Info].[System.Net.Http.HttpClient.bce-request.LogicalHandler].[}].[]
Start processing HTTP request GET http://localhost:5000/v1/eqid/8ea0c3b66b60f0ff100000005de704fb [Correlation: 6f14393a-3a2b-45c4-a9b4-0b4ab874ef1d]
// :: [Info].[System.Net.Http.HttpClient.bce-request.LogicalHandler].[}].[]
End processing HTTP request - OK, [Correlation: 6f14393a-3a2b-45c4-a9b4-0b4ab874ef1d]
可以看到每次请求的开始和结束都带上了设定的guid TraceId。
值得提醒的是:
① 这个TraceId 可以使用你业务上独具一格的标记,这样在排查时, 能根据上游业务更好的追踪日志。
② 现在这个TraceId位于LogMessage,实际上可以为nlog自定义LogoutRenderer,将该TraceId放在显著位置,便于ETL等日志集成框架过滤。
That's All, 本次为解决HttpClientFactory日志无追踪机制的探索,思考 + 实践 + UML制图。
实现CustomLoggingScopeHttpMessageHandler只是扩展HttpClientFactory能力的一个引子,如何扩展HttpClientFactory能力才是关键,希望能给大家一些启发。
--------------------------------------------------------------2019.12.06 更新------------------------
实际上HttpClientFactory内原生LoggingHandler是支持LoggingScope, 在Console 输出如下:
info: System.Net.Http.HttpClient.bce-request.LogicalHandler[]
=> ConnectionId:0HLRQ6DAF0JKV => RequestId:0HLRQ6DAF0JKV: RequestPath:/eqid/f53990dc0002adf0000000045de9c421 => EqidManager.Controllers.DebugController.ResolveEqid (EqidManager) => HTTP GET http://localhost:5000/v1/eqid/f53990dc0002adf0000000045de9c421
Start processing HTTP request GET http://localhost:5000/v1/eqid/f53990dc0002adf0000000045de9c421
info: System.Net.Http.HttpClient.bce-request.ClientHandler[]
=> ConnectionId:0HLRQ6DAF0JKV => RequestId:0HLRQ6DAF0JKV: RequestPath:/eqid/f53990dc0002adf0000000045de9c421 => EqidManager.Controllers.DebugController.ResolveEqid (EqidManager) => HTTP GET http://localhost:5000/v1/eqid/f53990dc0002adf0000000045de9c421
Sending HTTP request GET http://localhost:5000/v1/eqid/f53990dc0002adf0000000045de9c421 info: System.Net.Http.HttpClient.bce-request.ClientHandler[]
=> ConnectionId:0HLRQ6DAF0JKV => RequestId:0HLRQ6DAF0JKV: RequestPath:/eqid/f53990dc0002adf0000000045de9c421 => EqidManager.Controllers.DebugController.ResolveEqid (EqidManager) => HTTP GET http://localhost:5000/v1/eqid/f53990dc0002adf0000000045de9c421
Received HTTP response after .1112ms - OK
info: System.Net.Http.HttpClient.bce-request.LogicalHandler[]
=> ConnectionId:0HLRQ6DAF0JKV => RequestId:0HLRQ6DAF0JKV: RequestPath:/eqid/f53990dc0002adf0000000045de9c421 => EqidManager.Controllers.DebugController.ResolveEqid (EqidManager) => HTTP GET http://localhost:5000/v1/eqid/f53990dc0002adf0000000045de9c421
End processing HTTP request after .4906ms - OK
Scope需要LoggingProvider 支持,而我们使用的NLog不支持scope, 所以最上面的nlog 文件日志没有输出Scope。
这就引出了本文的目的,所以本文通过解构HttpClientFactory的HttpMessageHandler,为请求响应添加TraceId, 当然你也可以根据HttpClient业务加入其它HttpMessageHandler
HttpClientFactory 日志不好用,自己扩展一个?的更多相关文章
- 自定义日志阅读器——包括了一个load取Tomcat日志的分析器
最近在写往公司产品里添加Tomcat适配器,以支持Tomcat.有一些功能需要摘取到Tomcat的部分日志.没有合适的工具,也不想去网上找了,就自己写了一个. 简单的画了一下设计方案: 下面直接上代码 ...
- 利用jQuery来扩展一个瀑布流插件
简单了解jQuery.fn.extend() jQuery.fn.extend()函数用于为jQuery扩展一个或多个实例属性和方法(主要用于扩展方法). (截图来自jQuery文档) 为了更清晰 ...
- 扩展一个boot的插件—tooltip&做一个基于boot的表达验证
在线演示 本地下载 (代码太多请查看原文) 加班,加班加班,我爱加班··· 我已经疯了,哦也. 这次发一个刚接触boot的时候用boot做的表单验证,我们扩展一下tooltip的插件,让他可以换颜色. ...
- 给easyui datebox扩展一个清空按钮
/** * 给时间框控件扩展一个清除的按钮 */ $.fn.datebox.defaults.cleanText = '清空'; (function ($) { var buttons = $.ext ...
- 给easyui datebox时间框控件扩展一个清空的实例
给easyui datebox扩展一个清空的实例 步骤一:拓展插件 /** * 给时间框控件扩展一个清除的按钮 */ $.fn.datebox.defaults.cleanText = '清空'; ( ...
- 给easyui datebox扩展一个清空按钮,无侵入
/** * 给时间框控件扩展一个清除的按钮 */ $.fn.datebox.defaults.cleanText = '清空'; (function ($) { var buttons = $.ext ...
- [实战]扩展一个定制的sentinel JdbcDataSource
Sentinel是今年阿里开源的高可用防护的流量管理框架. git地址:https://github.com/alibaba/Sentinel wiki:https://github.com/alib ...
- EF架构~扩展一个分页处理大数据的方法
回到目录 最近总遇到大数据的问题,一次性处理几千万数据不实际,所以,我们需要对大数据进行分块处理,或者叫分页处理,我在EF架构里曾经写过类似的,那是在进行BulkInsert时,对大数据批量插入时候用 ...
- 为IEnumerable扩展一个ForEach方法
IEnumerable没有一个ForEach方法,我们可以使用C#写一个扩展方法: Source Code: using System; using System.Collections.Generi ...
随机推荐
- MarkDown的常用语法
个人比较喜欢Markdown的语法,常用来做一些笔记,下面就简单介绍一下它的语法. 概览 宗旨 Markdown 的目标是实现「易读易写」. 可读性,无论如何,都是最重要的.一份使用 Markdown ...
- 如何让多个不同类型的后端网站用一个nginx进行反向代理实际场景分析
前段时间公司根据要求需要将聚石塔上服务器从杭州整体迁移到张家口,刚好趁这次机会将这些乱七八糟的服务器做一次梳理和整合,断断续续一个月迁移完成 大概优化掉了1/3的机器,完成之后遇到了一些问题,比如曾今 ...
- 设计模式C++描述----06.适配器(Adapter)模式
一. 定义 适配器模式将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作. Adapter 模式的两种类别:类模式和对象模式. 二. 举例说明 实际中 ...
- Vue + Mui
概述 Vue套用Mui的外壳开发app项目,可以通过Mui的 manifest.json 文件添加权限 1.新建Mui项目 首先,新建一个空的Mui项目 window.location.href = ...
- Python中xml和dict格式转换
在做接口自动化的时候,请求数据之前都是JSON格式的,Python有自带的包来解决.最近在做APP的接口,遇到XML格式的请求数据,费了很大劲来解决,解决方式是:接口文档拿到的是XML,在线转化为js ...
- CTR@DeepFM
1. DeepFM算法 结合FM算法和DNN算法,同时提取低阶特征和高阶特征,然后组合.FM算法负责对一阶特征及由一阶特征两两组合成的二阶特征进行特征提取:DNN算法负责对由输入的一阶特征进行全连接等 ...
- JavaSE常用API
1.Math.round(11.5)等于多少?Math.round(-11.5)又等于多少? Math.round(11.5)的返回值是12,Math.round(-11.5)的返回值是-11.四舍五 ...
- python-->二进制的用法
1.10进制转换为其他进制 方法一:函数 十进制转二进制:bin(10) --> '0b1010' tpye:是字符串类型 0b:表示2进制 十进制转八进制:oct(10) --> '0o ...
- 自己实现 aop 和 spring aop
上文说到,我们可以在 BeanPostProcessor 中对 bean 的初始化前化做手脚,当时也说了,我完全可以生成一个代理类丢回去. 代理类肯定要为用户做一些事情,不可能像学设计模式的时候创建个 ...
- redis 底层数据结构
简单动态字符串SDS 包含字符串长度,剩余可用长度,字符数组 用于Redis中所有的string存储 字典(map) 数组+链表形式,跟hashMap很像 链地址法解决hash冲突 rehash使用新 ...