ASP.NET 8 中 HttpLoggingMiddleware 的改进

ASP.NET 8 中 HttpLoggingMiddleware 的改进

Intro

.NET 6 开始引入了一个 http logging 的中间件,我们可以借助于 http logging 的中间件记录请求和响应的信息,但是扩展性不是很强,在 .NET 8 版本中进行了一些优化,引入了一些新的配置和 HttpLoggingInterceptor 使得它更加容易扩展了

New Config

[Flags]
public enum HttpLoggingFields : long
{
+    /// <summary>
+    /// Flag for logging how long it took to process the request and response in milliseconds.
+    /// </summary>
+    Duration = 0x1000,

- All = Request | Response
+ All = Request | Response | Duration
}

HttpLoggingFields 中新增了一个 Duration 枚举值,会记录请求处理的耗时,并且在 All 中包含了,输出日志如下:

edc48ce45910bed24fd9dcd2c41e3062.png

http-logging-duration
public sealed class HttpLoggingOptions
{
+    /// <summary>
+    /// Gets or sets if the middleware will combine the request, request body, response, response body,
+    /// and duration logs into a single log entry. The default is <see langword="false"/>.
+    /// </summary>
+    public bool CombineLogs { get; set; }
}

HttpLoggingOptions 中增加了一个 CombineLogs 的配置,默认是 false,默认 request/response/duration 的 log 都是分开的

例如:

04a845f8bd8bf2d17bd1a6256a263051.png

http-logging-non-combined

配置为 true 之后就会合并成一条日志,如下:

00eb960329113b104fd3d9f69538233a.png

HttpLoggingInterceptor

.NET 8 还引入了 IHttpLoggingInterceptor,借助于此可以更好的扩展 http logging

public interface IHttpLoggingInterceptor
{
    /// <summary>
    /// A callback to customize the logging of the request and response.
    /// </summary>
    /// <remarks>
    /// This is called when the request is first received and can be used to configure both request and response options. All settings will carry over to
    /// <see cref="OnResponseAsync(HttpLoggingInterceptorContext)"/> except the <see cref="HttpLoggingInterceptorContext.Parameters"/>
    /// will be cleared after logging the request. <see cref="HttpLoggingInterceptorContext.LoggingFields"/> may be changed per request to control the logging behavior.
    /// If no request fields are enabled, and the <see cref="HttpLoggingInterceptorContext.Parameters"/> collection is empty, no request logging will occur.
    /// If <see cref="HttpLoggingOptions.CombineLogs"/> is enabled then <see cref="HttpLoggingInterceptorContext.Parameters"/> will carry over from the request to response
    /// and be logged together.
    /// </remarks>
    ValueTask OnRequestAsync(HttpLoggingInterceptorContext logContext);

    /// <summary>
    /// A callback to customize the logging of the response.
    /// </summary>
    /// <remarks>
    /// This is called when the first write to the response happens, or the response ends without a write, just before anything is sent to the client. Settings are carried
    /// over from <see cref="OnRequestAsync(HttpLoggingInterceptorContext)"/> (except the <see cref="HttpLoggingInterceptorContext.Parameters"/>) and response settings may
    /// still be modified. Changes to request settings will have no effect. If no response fields are enabled, and the <see cref="HttpLoggingInterceptorContext.Parameters"/>
    /// collection is empty, no response logging will occur.
    /// If <see cref="HttpLoggingOptions.CombineLogs"/> is enabled then <see cref="HttpLoggingInterceptorContext.Parameters"/> will carry over from the request to response
    /// and be logged together. <see cref="HttpLoggingFields.RequestBody"/> and <see cref="HttpLoggingFields.ResponseBody"/>  can also be disabled in OnResponseAsync to prevent
    /// logging any buffered body data.
    /// </remarks>
    ValueTask OnResponseAsync(HttpLoggingInterceptorContext logContext);
}

HttpLoggingInterceptorContext 定义如下:

public sealed class HttpLoggingInterceptorContext
{
    /// <summary>
    /// The request context.
    /// </summary>
    /// <remarks>
    /// This property should not be set by user code except for testing purposes.
    /// </remarks>
    public HttpContext HttpContext { get; set; }

    /// <summary>
    /// Gets or sets which parts of the request and response to log.
    /// </summary>
    /// <remarks>
    /// This is pre-populated with the value from <see cref="HttpLoggingOptions.LoggingFields"/>,
    /// <see cref="HttpLoggingAttribute.LoggingFields"/>, or
    /// <see cref="HttpLoggingEndpointConventionBuilderExtensions.WithHttpLogging{TBuilder}(TBuilder, HttpLoggingFields, int?, int?)"/>.
    /// </remarks>
    public HttpLoggingFields LoggingFields { get; set; }

    /// <summary>
    /// Gets or sets the maximum number of bytes of the request body to log.
    /// </summary>
    /// <remarks>
    /// This is pre-populated with the value from <see cref="HttpLoggingOptions.RequestBodyLogLimit"/>,
    /// <see cref="HttpLoggingAttribute.RequestBodyLogLimit"/>, or
    /// <see cref="HttpLoggingEndpointConventionBuilderExtensions.WithHttpLogging{TBuilder}(TBuilder, HttpLoggingFields, int?, int?)"/>.
    /// </remarks>
    public int RequestBodyLogLimit { get; set; }

    /// <summary>
    /// Gets or sets the maximum number of bytes of the response body to log.
    /// </summary>
    /// <remarks>
    /// This is pre-populated with the value from <see cref="HttpLoggingOptions.ResponseBodyLogLimit"/>,
    /// <see cref="HttpLoggingAttribute.ResponseBodyLogLimit"/>, or
    /// <see cref="HttpLoggingEndpointConventionBuilderExtensions.WithHttpLogging{TBuilder}(TBuilder, HttpLoggingFields, int?, int?)"/>.
    /// </remarks>
    public int ResponseBodyLogLimit { get; set; }

    /// <summary>
    /// Gets a list of parameters that will be logged as part of the request or response. Values specified in <see cref="LoggingFields"/>
    /// will be added automatically after all interceptors run. All values are cleared after logging the request.
    /// All other relevant settings will carry over to the response.
    /// </summary>
    /// <remarks>
    /// If <see cref="HttpLoggingOptions.CombineLogs"/> is enabled, the parameters will be logged as part of the combined log.
    /// </remarks>
    public IList<KeyValuePair<string, object?>> Parameters { get; }

    /// <summary>
    /// Adds data that will be logged as part of the request or response. See <see cref="Parameters"/>.
    /// </summary>
    /// <param name="key">The parameter name.</param>
    /// <param name="value">The parameter value.</param>
    public void AddParameter(string key, object? value);

    /// <summary>
    /// Adds the given fields to what's currently enabled in <see cref="LoggingFields"/>.
    /// </summary>
    /// <param name="fields">Additional fields to enable.</param>
    public void Enable(HttpLoggingFields fields);

    /// <summary>
    /// Checks if any of the given fields are currently enabled in <see cref="LoggingFields"/>.
    /// </summary>
    /// <param name="fields">One or more field flags to check.</param>
    public bool IsAnyEnabled(HttpLoggingFields fields);

    /// <summary>
    /// Removes the given fields from what's currently enabled in <see cref="LoggingFields"/>.
    /// </summary>
    /// <param name="fields">Fields to disable.</param>
    public void Disable(HttpLoggingFields fields);

    /// <summary>
    /// Disables the given fields if any are currently enabled in <see cref="LoggingFields"/>.
    /// </summary>
    /// <param name="fields">One or more field flags to disable if present.</param>
    /// <returns><see langword="true" /> if any of the fields were previously enabled.</returns>
    public bool TryDisable(HttpLoggingFields fields);
}

我们可以根据 Request 或者 Response 信息来动态地调整要记录的 field 或者动态调整 RequestBodyLogLimit/ResponseBodyLogLimit

来看一个 HttpLoggingInterceptor 示例:

file sealed class MyHttpLoggingInterceptor: IHttpLoggingInterceptor
{
    public ValueTask OnRequestAsync(HttpLoggingInterceptorContext logContext)
    {
        if (logContext.HttpContext.Request.Path.Value?.StartsWith("/req-") == true)
        {
            logContext.LoggingFields = HttpLoggingFields.ResponsePropertiesAndHeaders;
            logContext.AddParameter("req-path", logContext.HttpContext.Request.Path.Value);
        }
        
        return ValueTask.CompletedTask;
    }

    public ValueTask OnResponseAsync(HttpLoggingInterceptorContext logContext)
    {
        if (logContext.HttpContext is { Response.StatusCode: >=200 and < 300, Request.Path.Value: "/hello" })
        {
            logContext.TryDisable(HttpLoggingFields.All);
        }
        return ValueTask.CompletedTask;
    }
}

使用示例如下,使用 AddHttpLoggingInterceptor<TInterceptor>() 来注册:

var builder = WebApplication.CreateSlimBuilder(args);
builder.Services.AddControllers();
builder.Services.AddHttpLogging(options =>
{
    options.LoggingFields = HttpLoggingFields.All;
    options.CombineLogs = true;
});
builder.Services.AddHttpLoggingInterceptor<MyHttpLoggingInterceptor>();

var app = builder.Build();
app.UseHttpLogging();
app.MapGet("/hello", () => "Hello");
app.MapGet("/crash", () => Results.BadRequest());
app.MapGet("/req-intercept", () => "Hello .NET 8");
app.MapControllers();
await app.RunAsync();

访问一下示例的 path 看一下 log 的内容:

/hello

b5dc03290baf4a06c50efa1936d74414.png

/crash

d3a2b2ba23a0013829e32c74604342ee.png

/req-intercept

ce8173567c56781110898bdbc63fb54e.png

可以看到每个请求的 log 输出的结果都有所不同,第一个请求虽然我们设置了 ogContext.TryDisable(HttpLoggingFields.All) 但是还是有输出结果这是因为 httpLogging 目前的实现就是这样,在 Response 里处理的时候 request 信息已经被记录好了,详细可以参考 http logging middleware 的实现

https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/HttpLogging/src/HttpLoggingMiddleware.cs

如果想要完全 disable 需要在 OnRequestAsync 方法里处理

public ValueTask OnRequestAsync(HttpLoggingInterceptorContext logContext)
{
    if ("/no-log".Equals(logContext.HttpContext.Request.Path.Value, StringComparison.OrdinalIgnoreCase))
    {
        logContext.LoggingFields = HttpLoggingFields.None;
    }
    //
    return ValueTask.CompletedTask;
}

这样请求就不会有日志打印了

最后一个 req-intercept 在  request 的处理中设置了 ResponsePropertiesAndHeaders 并且加了一个自定义的 Parameter 从输出结果可以看到有输出到日志

More

大家可以自己尝试一下,比之前会好用一些,但是觉得还是有所欠缺

比如日志级别目前还都是 Information 不能动态的改变日志级别

另外就是前面提到的即使使用 CombineLogs 在 response 中设置为 HttpLoggingFields.None 时,依然会记录 request 信息,希望后面还会继续优化一下

References

  • https://devblogs.microsoft.com/dotnet/asp-net-core-updates-in-dotnet-8-rc-2/#servers

  • https://github.com/dotnet/aspnetcore/pull/50163

  • https://github.com/dotnet/aspnetcore/issues/31844

  • https://github.com/WeihanLi/SamplesInPractice/blob/master/net8sample/AspNetCore8Sample/HttpLoggingInterceptorSample.cs

  • https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/HttpLogging/src/HttpLoggingInterceptorContext.cs

  • https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/HttpLogging/src/IHttpLoggingInterceptor.cs

  • https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/HttpLogging/src/HttpLoggingMiddleware.cs

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值