目录
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
中包含了,输出日志如下:
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 都是分开的
例如:
http-logging-non-combined
配置为 true
之后就会合并成一条日志,如下:
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
/crash
/req-intercept
可以看到每个请求的 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