解剖HttpClientFactory,自由扩展HttpMessageHandler

前言

  .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(12))
               .AddPolicyHandler(GetRetryPolicy(3));
}

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(2, retryAttempt)));
            return retryPolicy;
}
HttpClientFactory典型用法

使用时从 IHttpClientFactory工厂创建所需HttpClient实例,发起业务端请求。

以下是利用NLog观察到的文件日志:

19/12/04 11:06:46 [Info].[System.Net.Http.HttpClient.bce-request.LogicalHandler].[18}].[]
Start processing HTTP request GET http://localhost:5000/v1/eqid/b827a9400004132a000000065dc26470 
19/12/04 11:06:46 [Info].[System.Net.Http.HttpClient.bce-request.ClientHandler].[18}].[]
Sending HTTP request GET http://localhost:5000/v1/eqid/b827a9400004132a000000065dc26470 
19/12/04 11:06:46 [Info].[System.Net.Http.HttpClient.bce-request.ClientHandler].[34}].[]
Received HTTP response after 174.5088ms - OK 
19/12/04 11:06:46 [Info].[System.Net.Http.HttpClient.bce-request.LogicalHandler].[34}].[]
End processing HTTP request after 211.1478ms - OK 

头脑风暴

  观察上面单次请求的日志,由外层LogicHandler和内层ClientHandler 日志头组成。 这样的日志可以想象到有2个问题:

 ① 在高并发使用HttpClient,日志条数众多,没有类似TraceId 这样的机制定位 某次HttpClient调用的完整日志。 

 ②  若是微服务/ 分布式调用,可能还有 将本次HttpClient调用日志与后置api日志 结合分析的需求, 这个日志也支持不了。

因此本文打算重新自定义HttpClientFactory日志处理器(给请求的全部日志设置TraceId),实际上CustomLoggingHttpMessageHandler只是一个引子,掌握如何扩展才是关键。

结合我给出的典型用法来看IHttpClientFactory组件原理:

 

 示例中System.Net.Http.HttpClient.bce-request.LogicalHandlerSystem.Net.Http.HttpClient.bce-request.ClientHandler 日志头即是来自LoggingScopeHttpMessageHandler ,LoggingHttpMessageHandler 两个处理器,

 给出手绘的UML类图: 

 

本次要扩展的入口便是 IHttpMessageHandlerFilter接口, 核心是自定义DelegatingHandler日志处理器

https://github.com/dotnet/extensions/blob/master/src/HttpClientFactory/Http/src/Logging/LoggingHttpMessageHandlerBuilderFilter.cs

编程实践

   如以上分析,

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(100, "RequestPipelineStart");
                public static readonly EventId PipelineEnd = new EventId(101, "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请求, 输出的日志如下:

19/12/04 12:59:22 [Info].[System.Net.Http.HttpClient.bce-request.LogicalHandler].[17}].[]
Start processing HTTP request GET http://localhost:5000/v1/eqid/ad78deef00444ed7000000035de704e8 [Correlation: 03de676d-680e-4a92-aef5-749bcc3ba499] 
19/12/04 12:59:22 [Info].[System.Net.Http.HttpClient.bce-request.LogicalHandler].[4}].[]
End processing HTTP request - OK, [Correlation: 03de676d-680e-4a92-aef5-749bcc3ba499] 
19/12/04 12:59:48 [Info].[System.Net.Http.HttpClient.bce-request.LogicalHandler].[17}].[]
Start processing HTTP request GET http://localhost:5000/v1/eqid/8ea0c3b66b60f0ff100000005de704fb [Correlation: 6f14393a-3a2b-45c4-a9b4-0b4ab874ef1d] 
19/12/04 12:59:48 [Info].[System.Net.Http.HttpClient.bce-request.LogicalHandler].[42}].[]
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[100]
      => ConnectionId:0HLRQ6DAF0JKV => RequestId:0HLRQ6DAF0JKV:00000004 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[100]
      => ConnectionId:0HLRQ6DAF0JKV => RequestId:0HLRQ6DAF0JKV:00000004 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[101]
      => ConnectionId:0HLRQ6DAF0JKV => RequestId:0HLRQ6DAF0JKV:00000004 RequestPath:/eqid/f53990dc0002adf0000000045de9c421 => EqidManager.Controllers.DebugController.ResolveEqid (EqidManager) => HTTP GET http://localhost:5000/v1/eqid/f53990dc0002adf0000000045de9c421
      Received HTTP response after 195.1112ms - OK
info: System.Net.Http.HttpClient.bce-request.LogicalHandler[101]
      => ConnectionId:0HLRQ6DAF0JKV => RequestId:0HLRQ6DAF0JKV:00000004 RequestPath:/eqid/f53990dc0002adf0000000045de9c421 => EqidManager.Controllers.DebugController.ResolveEqid (EqidManager) => HTTP GET http://localhost:5000/v1/eqid/f53990dc0002adf0000000045de9c421
      End processing HTTP request after 232.4906ms - OK

Scope需要LoggingProvider 支持,而我们使用的NLog不支持scope, 所以最上面的nlog 文件日志没有输出Scope。

这就引出了本文的目的,所以本文通过解构HttpClientFactory的HttpMessageHandler,为请求响应添加TraceId, 当然你也可以根据HttpClient业务加入其它HttpMessageHandler

原文地址:https://www.cnblogs.com/JulianHuang/p/11982021.html

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


引言 本文从Linux小白的视角, 在CentOS 7.x服务器上搭建一个Nginx-Powered AspNet Core Web准生产应用。 在开始之前,我们还是重温一下部署原理,正如你所常见的.Net Core 部署图: 在Linux上部署.Net Core App最好的方式是在Linux机器
引言: 多线程编程/异步编程非常复杂,有很多概念和工具需要去学习,贴心的.NET提供Task线程包装类和await/async异步编程语法糖简化了异步编程方式。 相信很多开发者都看到如下异步编程实践原则: 遵守以上冷冰冰的②③条的原则,可保证异步程序按照预期状态正常运作;我们在各大编程论坛常看到违背
一. 宏观概念 ASP.NET Core Middleware是在应用程序处理管道pipeline中用于处理请求和操作响应的组件。 每个组件是pipeline 中的一环。 自行决定是否将请求传递给下一个组件 在处理管道的下个组件执行之前和之后执行业务逻辑 二. 特性和行为 ASP.NET Core处
背景 在.Net和C#中运行异步代码相当简单,因为我们有时候需要取消正在进行的异步操作,通过本文,可以掌握 通过CancellationToken取消任务(包括non-cancellable任务)。 Task&#160;表示无返回值的异步操作, 泛型版本Task&lt;TResult&gt;表示有返
HTTP基本认证 在HTTP中,HTTP基本认证(Basic Authentication)是一种允许网页浏览器或其他客户端程序以(用户名:口令) 请求资源的身份验证方式,不要求cookie,session identifier、login page等标记或载体。 - 所有浏览器据支持HTTP基本认
1.Linq 执行多列排序 OrderBy的意义是按照指定顺序排序,连续两次OrderBy,后面一个有可能会打乱前面一个的排序顺序,可能与预期不符。 要实现sql中的order by word,name类似效果; LINQ 有ThenBy可以紧接使用, ThenBy记住原本排序的值,然后再排其他值,
ASP.NET Core 核心特性:开源、跨平台、高性能是其决战JAVA的必胜法宝,最引人关注的跨平台特性 到底是怎么实现? &#xA; 本文分Unix、Windows剖析跨平台内幕,读完让你大呼过瘾。
前导 Asynchronous programming Model(APM)异步编程模型以BeginMethod(...) 和 EndMethod(...)结对出现。 IAsyncResult BeginGetResponse(AsyncCallback callback, object state
引言 最近在公司开发了一个项目,项目部署架构图如下: 思路 如图中文本所述,公司大数据集群不允许直接访问外网,需要一个网关服务器代理请求,本处服务器A就是边缘代理服务器的作用。 通常技术人员最快捷的思路是在服务器A上部署IISʺpplication Request Routing Module组件
作为一枚后端程序狗,项目实践常遇到定时任务的工作,最容易想到的的思路就是利用Windows计划任务/wndows service程序/Crontab程序等主机方法在主机上部署定时任务程序/脚本。 但是很多时候,若使用的是共享主机或者受控主机,这些主机不允许你私自安装exe程序、Windows服务程序
引言 熟悉TPL Dataflow博文的朋友可能记得这是个单体程序,使用TPL Dataflow 处理工作流任务, 在使用Docker部署的过程中, 有一个问题一直无法回避: 在单体程序部署的瞬间(服务不可用)会有少量流量无法处理;更糟糕的情况下,迭代部署的这个版本有问题,上线后无法运作, 更多的流
合格的web后端程序员,除搬砖技能,还必须会给各种web服务器配置Https,本文结合ASP.NET Core部署模型聊一聊启用Https的方式。 温故知新 目前常见的Http请求明文传输,请求可能被篡改,访问的站点可能被伪造。 HTTPS是HTTP加上TLS/SSL协议构建的可进行加密传输、身份认
长话短说 前文《解剖HttpClientFactory,自由扩展HttpMessageHandler》主要讲如何为HttpClientFactory自定义HttpMessageHandler组件, 现在来完成课后的小作业: 将重点日志字段显示到Nlog的LayoutRenderer上。 本文实现一个
引言问题 作为资深老鸟,有事没事,出去面试;找准差距、定位价值。 面试必谈哈希, Q1:什么是哈希? Q2:哈希为什么快? Q3:你是怎么理解哈希算法利用空间换取时间的? Q4:你是怎么解决哈希冲突的? Q5:你有实际用写过哈希算法吗? 知识储备 哈希(也叫散列)是一种查找算法(可用于插入),哈希算
前言 如题,有感于博客园最近多次翻车,感觉像胡子眉毛一把抓, 定位不了生产环境的问题。 抛开流程问题,思考在生产环境中如何做故障排除,&#160;发现博客园里面这方面的文章比较少。 .Net 本身是提供了sos.dll工具帮助我们在生产中故障排除,通过提供有关内部公共语言运行时(CLR)环境的信息,
.NET程序是基于.NET Framework、.NET Core、Mono、【.NET实现】开发和运行的 ,定义以上【.NET实现】的标准规范称为.NET Standard .NET Standard .NET标准是一组API集合,由上层三种【.NET实现】的Basic Class Library
长话短说 上个月公司上线了一个物联网数据科学项目,我主要负责前端接受物联网事件,并提供 参数下载。 webapp 部署在Azure云上,参数使用Azure SQL Server存储。 最近从灰度测试转向全量部署之后,日志时常收到: SQL Session超限报错。 排查 我在Azure上使用的是 S
临近年关,搜狗,360浏览器出现页面无法成功跳转,同域Cookie丢失? 也许是服务端 SameSite惹的祸。&#xA;本文揭示由于Chrome低版本内核不识别 SameSite= None, 引发的单点登录故障。
本文聊一聊TraceID的作用和一般组成,衍生出ASP. NETCore 单体和分布式程序中 TraceId 的使用方式
通过给 HttpClint请求的日志增加 TraceId,解锁自定义扩展 HttpClientFacroty 的姿势