HttpClientFactory in ASP.NET Core 2.1 Part 5: 日志

原文地址:https://www.stevejgordon.co.uk/httpclientfactory-asp-net-core-logging

在 2.1 版本的 IHttpClientFactory 发布中,ASP.NET 团队包括了对由工厂创建的 HttpClient 对象的内建的对 Http 调用的日志。它对诊断失效很有用,以及了解完成对外部服务的 Http 调用所花费的时间有帮助。

本文中,我将探索默认可用的日志,如何控制记录的内容,日志是如何实现的,最后,如果在我们的实现中替换日志。

本文完全是技术细节。我希望它证明 IHttpClientFactory 有用和有趣的工作。点击这里,如果你希望跳到通过替换默认日志实现来进行定制。

什么是日志?

IHttpClientFactory 包含两个级别的日志,在 Information 级别,花费的处理和发送请求的时间被包括。在监控外部请求的缓慢响应时非常有用,例如,这里是在 Information 级别启用的时候,在控制台中的输出。

info: System.Net.Http.HttpClient.MyClient.LogicalHandler[100]
Start processing HTTP request GET https://api.github.com/repos/aspnet/docs/branches info: System.Net.Http.HttpClient.MyClient.ClientHandler[100]
Sending HTTP request GET https://api.github.com/repos/aspnet/docs/branches info: System.Net.Http.HttpClient.MyClient.ClientHandler[101]
Received HTTP response after 682.9818ms - OK info: System.Net.Http.HttpClient.MyClient.LogicalHandler[101]
End processing HTTP request after 693.1094ms - OK

如果你需要更详细的日志,可以使用 Trace 级别的日志。随着 Trace 级别的日志启用。关于请求和响应头的详细内容将被包含到日志信息中,这里是 Trace 级别日志启用后的日志示例。

info: System.Net.Http.HttpClient.MyClient.LogicalHandler[100]
Start processing HTTP request GET https://api.github.com/repos/aspnet/docs/branches trce: System.Net.Http.HttpClient.MyClient.LogicalHandler[102]
Request Headers:
Accept: application/vnd.github.v3+json
User-Agent: HttpClientFactory-Sample info: System.Net.Http.HttpClient.MyClient.ClientHandler[100]
Sending HTTP request GET https://api.github.com/repos/aspnet/docs/branches trce: System.Net.Http.HttpClient.MyClient.ClientHandler[102]
Request Headers:
Accept: application/vnd.github.v3+json
User-Agent: HttpClientFactory-Sample info: System.Net.Http.HttpClient.MyClient.ClientHandler[101]
Received HTTP response after 795.6954ms - OK trce: System.Net.Http.HttpClient.MyClient.ClientHandler[103]
Response Headers:
Server: GitHub.com
Date: Sun, 08 Jul 2018 09:44:09 GMT
Status: 200 OK
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58
X-RateLimit-Reset: 1531046594
Cache-Control: public, max-age=60, s-maxage=60
Vary: Accept
ETag: "f0452653b55e5fef139a58372e3a7bf3"
X-GitHub-Media-Type: github.v3; format=json
Access-Control-Expose-Headers: ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval
Access-Control-Allow-Origin: *
Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
X-Frame-Options: deny
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
Content-Security-Policy: default-src 'none'
X-Runtime-rack: 0.029792
X-GitHub-Request-Id: DCD6:3C9D:688D222:D064A9D:5B41DCE9
Content-Type: application/json; charset=utf-8
Content-Length: 2642 info: System.Net.Http.HttpClient.MyClient.LogicalHandler[101]
End processing HTTP request after 818.4525ms - OK trce: System.Net.Http.HttpClient.MyClient.LogicalHandler[103]
Response Headers:
Server: GitHub.com
Date: Sun, 08 Jul 2018 09:44:09 GMT
Status: 200 OK
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58
X-RateLimit-Reset: 1531046594
Cache-Control: public, max-age=60, s-maxage=60
Vary: Accept
ETag: "f0452653b55e5fef139a58372e3a7bf3"
X-GitHub-Media-Type: github.v3; format=json
Access-Control-Expose-Headers: ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval
Access-Control-Allow-Origin: *
Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
X-Frame-Options: deny
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
Content-Security-Policy: default-src 'none'
X-Runtime-rack: 0.029792
X-GitHub-Request-Id: DCD6:3C9D:688D222:D064A9D:5B41DCE9
Content-Type: application/json; charset=utf-8
Content-Length: 2642

如你所见和所愿,在 Trace 级别,详情非常丰富。这对于在开发阶段获得更详细的理解请求头和响应头很有帮助。并不建议在生产环境这样启用,因为它将不仅很快填满日志,还可能暴露安全数据,例如验证令牌等。

每条日志信息包含一个事件 Id,所以你可以快速过滤感兴趣的日志,默认有 2 个日志器可用。一个是 LogicalHandler 日志器,它封装整个的处理器管线。它支持整条管线的计时包含到日志中。另外,它是整个管线中的第一个,在其它处理器之前,它记录开始的请求头。其它处理器可能会修改这些请求头。使用 trace 级别日志,它可以在日志中捕获这些信息,在诊断失效的时候比较有用。

内部的日志器在类别名称上有一个 ClientHandler 的前缀。它将是最里面的处理器,因此是在请求被发送到网上之前最终的定制处理器,结果,它将可以记录最终的请求头。

作为参考,这些日志起包含的事件 ID 如下所示:

Outer “LogicalHandler”

100 RequestPipelineStart
101 RequestPipelineEnd
102 RequestPipelineRequestHeader
103 RequestPipelineResponseHeader

Inner “ClientHandler”

100 RequestStart
101 RequestEnd
102 RequestHeader
103 ResponseHeader

IHttpClientFactory 是如何进行日志工作的?

IHttpClientFactory 正好在 HttpMessageHandlerBuilder.Build() 方法被调用之前被应用到管线,并返回 HttpMessageHandler 管线。

这是通过 HttpMessageHandlerBuilder 过滤器的帮助做到的,他通过 DefaultHttpClientFactory 实现被应用。名为 IHttpMessageHandlerBuilderFilter 的接口存在并被实现,以便于定义过滤器。默认,该接口有一个被称为 LoggingHttpMessageHandlerBuilderFilter 的实现,它在 DI 中被注册。基于该接口,可以有多于一个的实现。随着该接口注册到 DI 中,当构建管线的时候每一个都可以被执行。

在 LoggingHttpMessageHandlerBuilderFilter 中的代码实现 Configure() 方法,负责创建两个日志器,并传递给日志处理器。它们自己被实现为抽象类 DelegatingHandler 的实现。LoggingHttpMessageHandlerBuilderFilter.Configure() 中的代码如下:

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); // We want all of our logging message to show up as-if they are coming from HttpClient,
// but also to include the name of the client for more fine-grained control.
var outerLogger = _loggerFactory.CreateLogger($"System.Net.Http.HttpClient.{builder.Name}.LogicalHandler");
var innerLogger = _loggerFactory.CreateLogger($"System.Net.Http.HttpClient.{builder.Name}.ClientHandler"); // The 'scope' handler goes first so it can surround everything.
builder.AdditionalHandlers.Insert(0, new LoggingScopeHttpMessageHandler(outerLogger)); // We want this handler to be last so we can log details about the request after
// service discovery and security happen.
builder.AdditionalHandlers.Add(new LoggingHttpMessageHandler(innerLogger));
};
}

当委托链被调用的时候,next(builder) 调用 (line 11) 将执行下一个委托 ( Action<HttpMessageHandlerBuilder>) 以确保整个处理器管线被配置。

然后,2 个日志器被创建出来,使用来自构建器的名称。它将是给予命名客户端的名称或者类型化客户端的名称。

外部的处理器被添加到索引 0 到 AdditionalHandlers 列表中,所以它包围了所有其它的处理器并首先执行。内部的处理器被添加到 AdditionalHandlers 列表的最后,所以它将是在内部的处理器处理 Http 请求时最后的处理器。

每个日志处理器负责记录在调用其它的 SendAsync() 之前和之后的消息。使用这种方式,定时器可以在 SendAsync() 调用之前启动,且在最后被使用以记录整个的 Http 请求时间。我不再复制这些处理器的代码,它们很长,相反,如果你感兴趣的话,可以在 GitHub 中查阅。

外部处理器的代码可以在这里查看

该类创建了一个日志范围,并记录日志消息。可选地,如果 Trace 被启用,还将遍历请求和响应头。将它们记录到日志中。此类中使用的模式是 LoggerMessage 方式的一个示例,提供缓存日志委托以提高性能。你可以在 official documentation at docs.microsoft.com 中阅读关于此方式的更多信息。这超出了本文的范围。

内部的 "ClientHandler" 日志器使用类似的方式来记录日志消息,源代码可以在这里查看

配置日志输出

由于所有的日志使用 Microsoft.Extensions.Logging 库,你可以使用配置来控制日志的生成。

在 appsettings.json 文件中,你可以控制和过滤记录的日志。默认的产品配置 json 文件如下所示:

{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
}
}

在上面的配置中,只有 Warning 和更高级别被记录并输出。没有来自 IHttpClientFactory 处理器的日志。为了启用日志,我们可以添加附加的日志级别设置。如果你阅读了上面的 "如何工作" 一节,你会回忆起使用的用来记录 Http 请求的日志消息的日志器,它的类型名以 "System.Net.Http" 开头。

一种方式是针对 System 命名空间启用 Information 日志。

{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information"
}
}
}

但是,这将会启用其它 System 命名空间中所有组件的日志。因此最好限制配置为 System.Net.Http.HttpClient 以便仅仅记录通过 HttpClientFactory 进行的 Http 请求的日志。

{
"Logging": {
"LogLevel": {
"Default": "Warning",
"System.Net.Http.HttpClient": "Information"
}
}
}

我们还可以进一步控制到命名或者类型化的客户端。对于类型话的客户端,名字来自注册的名称。

想象我们仅仅希望记录名为 MyClient 的类型化客户端日志,另外,我们还可能仅仅希望看到 Http 请求的计时,在该例中,我们可以仅仅启用 MyClient 的 ClientHandler 日志:

{
"Logging": {
"LogLevel": {
"Default": "Warning",
"System.Net.Http.HttpClient.MyClient.ClientHandler": "Information"
}
}
}

定制日志消息

你也可能希望针对由 IHttpClientFactory 管理的客户端增加额外的日志。一种方式是,引入额外的处理器到管线中,本系列的 #3,我探索了添加新的中间件处理器到客户端配置中。使用这种处理器,你可以查看请求和响应,记录任何需要的数据。

如果你想完全替换默认的日志器,以完全定制输出的消息,来自开发团队的建议方式是替换默认的 IHttpMessageHandlerBuilderFilter 接口实现,实际上,本文的灵感来自 IHttpClientFactory GitHub 仓库的一个问题。我们看一下如何替换日志器,以记录 correlation ID 到控制台消息中。因为我需要额外的日志消息,所以我们希望替换默认的实现。

首先,我们需要创建 IHttpMessageHandlerBuilderFilter 接口的新的实现。

public class CustomLoggingFilter : IHttpMessageHandlerBuilderFilter
{
private readonly ILoggerFactory _loggerFactory; public CustomLoggingFilter(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.Insert(0, new CustomLoggingScopeHttpMessageHandler(outerLogger));
};
}
}

基本上我复制了过滤器的默认实现。主要的区别是我仅仅使用一个外部的日志器进行演示。创建了一个日志器,然后添加新的 CustomLoggingScopeHttpMessageHandler 到管线中。

CustomLoggingScopeHttpMessageHandler 如下所示:

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, Exception> _requestPipelineEnd =
LoggerMessage.Define<HttpStatusCode>(
LogLevel.Information,
EventIds.PipelineEnd,
"End processing HTTP request - {StatusCode}"); 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)
{
_requestPipelineEnd(logger, response.StatusCode, null);
} private static string GetCorrelationIdFromRequest(HttpRequestMessage request)
{
var correlationId = "Not set"; if (request.Headers.TryGetValues("X-Correlation-ID", out var values))
{
correlationId = values.First();
} return correlationId;
}
}
}

确实有点多,但是多数是静态的日志类和它的方法。我不会深入它们,因为它们遵循 LoggerMessage 建议来更有效日志,详见 read in the docs。多数是根据需要对现存的 LoggingScopeHttpMessageHandler 进行调整。

第一关注点是整个操作被封装在一个日志范围之内。在 SendAsync() 调用之前和之后,我们使用静态的 Log() 方法来记录事件 (行 20 和 22)

在日志类中,一些定义的私有委托定义为格式化期望的日志消息。

_beginRequestPipelineScope 和 _requestPipelineStart 两者接收一个字符串,它将是 correlation ID。它们使用该值来记录 correlation ID 到 Scope 属性中,也在请求开始的消息中。

简单的助手方法被添加,它对期望的 correlation id 解析 Http 请求头,如果提供了,返回它,BeginRequestPipelineScope 和 RequestPipelineStart 两者使用该方法来提取 correlation ID。

在我们过滤器中最终的步骤注册到 DI 容器中,替换现存的默认过滤器。

在 Startup 类内部,ConfigureServices() 方法中,我们可以在 ServiceCollection 上调用 Replace() 方法来使用我们的实现来清除默认实现。

services.Replace(ServiceDescriptor.Singleton<IHttpMessageHandlerBuilderFilter, CustomLoggingFilter>());

Replace() 方法将发现第一个注册的 IHttpMessageHandlerBuilderFilter 服务,并使用新的替换原来的注册。CustomLoggingFilter 是我们的实现。

现在,当我们运行应用的时候,控制台日志包含了我们的 Correlation ID

info: System.Net.Http.HttpClient.MyClient.LogicalHandler[100]
Start processing HTTP request GET https://api.github.com/repos/aspnet/docs/branches [Correlation: 447c8d6b-e280-4538-bd31-56d508266b5b] info: System.Net.Http.HttpClient.MyClient.LogicalHandler[101]
End processing HTTP request - OK

另外,过滤器方式是很好对整个应用程序添加公共的跨边界的方式。可以注册额外的过滤器,每个可以通过 HttpClientFactory 添加自己的处理器到所有的客户端上

总结

本文中,我们关注内建的日志器的可用类型信息,包括作为 HttpClientFactory 库中的一部分。我们如何使用日志配置来控制哪些日志信息可见,日志是如何在库中实现的,最后,我们探索了使用 IHttpMessageHandlerBuilderFilter 接口来替换默认的日志过滤器。

Part 1 – HttpClientFactory in ASP.NET Core 2.1 Part 1 介绍

Part 2 – HttpClientFactory in ASP.NET Core 2.1 Part 2:定义命名和类型化的客户端

Part 3 – HttpClientFactory in ASP.NET Core 2.1 Part 3: 对处理器使用对外请求中间件

Part 4 – HttpClientFacotry Part 4: 集成 Polly 处理瞬时失效

Part 5 – HttpClientFactory in ASP.NET Core 2.1 Part 5: 日志

HttpClientFactory in ASP.NET Core 2.1 Part 5: 日志的更多相关文章

  1. asp.Net Core免费开源分布式异常日志收集框架Exceptionless安装配置以及简单使用图文教程

    最近在学习张善友老师的NanoFabric 框架的时了解到Exceptionless : https://exceptionless.com/ !因此学习了一下这个开源框架!下面对Exceptionl ...

  2. ASP.NET Core使用Elasticsearch记录NLog日志

    ASP.NET Core使用Elasticsearch记录NLog日志 1.新建一个 ASP.NET Core项目 2.安装Nuge包 运行:Install-Package NLog.Web.AspN ...

  3. C#实现多级子目录Zip压缩解压实例 NET4.6下的UTC时间转换 [译]ASP.NET Core Web API 中使用Oracle数据库和Dapper看这篇就够了 asp.Net Core免费开源分布式异常日志收集框架Exceptionless安装配置以及简单使用图文教程 asp.net core异步进行新增操作并且需要判断某些字段是否重复的三种解决方案 .NET Core开发日志

    C#实现多级子目录Zip压缩解压实例 参考 https://blog.csdn.net/lki_suidongdong/article/details/20942977 重点: 实现多级子目录的压缩, ...

  4. 【转】asp.Net Core免费开源分布式异常日志收集框架Exceptionless安装配置以及简单使用图文教程

    最近在学习张善友老师的NanoFabric 框架的时了解到Exceptionless : https://exceptionless.com/ !因此学习了一下这个开源框架!下面对Exceptionl ...

  5. ASP.NET Core 1.0基础之日志

    过年出去玩了一圈,回来继续翻译.前两天偷懒没有翻译,只是转了两篇C# 7计划中的新features,大家还是很支持的.现在继续完善这个系列. 来源https://docs.asp.net/en/lat ...

  6. asp.net core mcroservices 架构之 分布式日志(一)

    一 简介 无论是微服务还是其他任何分布式系统,都需要一个统一处理日志的系统,这个系统 必须有收集,索引,分析查询的功能.asp .net core自己的日志是同步方式的,正如文档所言: 所以必须自己提 ...

  7. 在 ASP.NET Core 中使用 Serilog 进行日志记录

    目录 从 NuGet 安装 Serilog 在 Main函数 中配置 Serilog 在项目中使用 Serilog 进行日志输出 从 NuGet 安装 Serilog 核心的包是 Serilog 和 ...

  8. asp.net core 多端口监听&日志服务

    1 配置多个端口监听 HostingAbstractionsWebHostBuilderExtensions. public static IWebHostBuilder UseUrls(this I ...

  9. asp.net core 2.1 增加Nlog日志到sql server数据库

    一.增加引用 <PackageReference Include="NLog.Extensions.Logging" Version="1.2.1" /& ...

  10. asp.net core mcroservices 架构之 分布式日志(三):集成kafka

    一 kafka介绍 kafka是基于zookeeper的一个分布式流平台,既然是流,那么大家都能猜到它的存储结构基本上就是线性的了.硬盘大家都知道读写非常的慢,那是因为在随机情况下,线性下,硬盘的读写 ...

随机推荐

  1. laravel框架中上传图片,并在本地显示

    1 //处理文件上传 2 if ($request->hasFile('image')&&$request->file('image')->isValid()){ 3 ...

  2. Go语言对接微信支付与退款全流程指南

    目录: 一.准备工作 二.初始化微信支付客户端 三.实现支付功能 1. 付款时序图 2. 实现不同场景下的支付 WAP端支付 PC端支付 Android端支付 3. 解析支付回调 四.实现退款功能 退 ...

  3. qemu的使用

    一.QEMU的运行模式 直接摘抄自己<揭秘家用路由器0day漏洞挖掘技术>,网上查了一下也没有找到令人满意的QEMU的使用说明,就采用这本书上的介绍.如果后期能够找到比较满意的QEMU的使 ...

  4. window和Linux下安装nvidia的apex

    两种方法: 1.去github下下载apex,之后安装到你的python环境下,我的安装路径:E:\Anaconda\anaconda\envs\pytorch\Lib\site-packages 注 ...

  5. 2022年7月中国数据库排行榜:墨天轮榜单榜眼易主,PolarDB得分涨幅最大

    信创元年,后起之秀,大有可为.2022年7月的 墨天轮中国数据库流行度排行榜 风起云涌,本月排行榜共有232个数据库参与排名,相比上月,新增 ShuangzhaoDB 数据库.榜单前十名的唯一变化是达 ...

  6. text-align的对齐方式

    text-align的6种取值 left:左对齐 right:右对齐 center:居中 start:如果内容方向是左至右,则等于left,反之则为right. end:如果内容方向是左至右,则等于r ...

  7. MYSQL存储过程-练习2 while 循环

    MYSQL存储过程-练习2 while 循环 1 #WHILE循环 2 DELIMITER $$ 3 4 CREATE PROCEDURE `sp_while`() 5 BEGIN 6 DECLARE ...

  8. js中window全局变量

    .markdown-body { line-height: 1.75; font-weight: 400; font-size: 16px; overflow-x: hidden; color: rg ...

  9. ToDesk云电脑推出Web端,这意味着什么?

    在数字化转型的浪潮中,云计算技术正在以前所未有的速度改变着我们的生活方式和工作模式.作为云计算领域的一股新生力量,ToDesk云电脑凭借其卓越的性能和便捷的使用体验,一经上线,便赢得了众多用户的青睐. ...

  10. 基于sqli-labs Less-5 sql报错注入详解

    按照之前的思路发现,是正常的'闭合的字符型,但是在联合注入0' union select 1,2,3--+没有回显注入点,只是回显You are in,因此无法使用联合注入,考虑使用报错注入或者盲注. ...