在 ASP.NET Core Web API 中,添加
Microsoft.AspNetCore.HttpLogging
以包含请求和响应非常容易。
在我的启动中,在使用
builder.Build()
构建应用程序之前,我添加以下代码:
builder.Services.AddHttpLogging(logging =>
{
logging.LoggingFields =
HttpLoggingFields.RequestPath |
HttpLoggingFields.RequestBody |
HttpLoggingFields.ResponseStatusCode |
HttpLoggingFields.ResponseBody |
HttpLoggingFields.Duration;
logging.MediaTypeOptions.AddText("application/json");
logging.MediaTypeOptions.AddText("text/plain");
logging.RequestBodyLogLimit = 4096;
logging.ResponseBodyLogLimit = 4096;
logging.CombineLogs = true;
});
建成后,我打电话
app.UseHttpLogging();
在我的
appsettings.json
中,我添加了:
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
}
}
然后我在日志中得到了我想要的信息:
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[9]
Request and Response:
PathBase:
Path: /api/v1/products
StatusCode: 200
ResponseBody: {"products":[{"family":"ProductFamily","products":["C91C","C90C","C89C","C89B","C23A","C09C","C09B","A43A","A31B"]}]}
Duration: 533.5687
(在这种情况下没有请求正文)
大约 20 分钟即可完成所有设置。问题是
Microsoft.AspNetCore.HttpLogging
只写入控制台,不支持记录到文件或任何其他目标。
要使用 Serilog 完成日志记录,显然需要编写一个自定义中间件来捕获信息。我一直无法在任何地方找到关于如何设置的明确答案。
这里我们找到了如何设置中间件来捕获请求和响应主体的很好的解释:
https://exceptionnotfound.net/using-middleware-to-log-requests-and-responses-in-asp-net-core/
但它不包括如何使用 Serilog 实际记录它们。
这个问题
如何在serilog的输出.net core中添加'请求正文'?
有几个关于如何记录请求正文的答案,但它们不适用于响应正文,因为其流无法重新定位为与请求相同的位置。
此贴
https://github.com/serilog/serilog-aspnetcore/issues/168
还有这个问题
Serilog 记录 web-api 方法,在中间件中添加上下文属性
似乎正是我所需要的,但使用它们对我来说不起作用,因为通过
添加的属性LogContext.PushProperty("RequestBody", requestBodyPayload);
即使我指定,也不会出现在我的日志中
"Enrich": [ "FromLogContext" ]
in
appsettings.json
用于 Serilog。无法从 LogContext
自行检索属性,因此我无法采取解决方法。
我首先尝试的是:我在构建器中激活 Serilog 并指示它从设置中检索其配置:
builder.Host.UseSerilog((context, configuration) =>
{
configuration.ReadFrom.Configuration(context.Configuration);
});
在我的
appsettings.json
中,我有登录控制台和文件的设置:
"Serilog": {
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"System": "Warning"
}
},
"WriteTo": [
{ "Name": "Console" },
{
"Name": "File",
"Args": {
"path": "Log_.log",
"rollingInterval": "Day",
"rollOnFileSizeLimit": true,
"filesizeLimitBytes": 67108864
}
}
],
"Enrich": [ "FromLogContext" ]
},
应用程序构建后,我调用
app.UseSerilogRequestLogging();
这为我提供了很好的请求/响应日志记录,但没有正文。
日志条目示例:
[16:02:21 INF] HTTP GET /api/v1/products responded 200 in 504.9038 ms
然后我实现了一个中间件来捕获尸体并将它们推送到
LogContext
。
public class MyRequestResponseLoggingMiddlewareForSerilog
{
private readonly RequestDelegate _next;
public MyRequestResponseLoggingMiddlewareForSerilog(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
// Allow multiple reading of request body
context.Request.EnableBuffering();
// Add the request body to the LogContext
string requestBodyText = await new StreamReader(context.Request.Body).ReadToEndAsync();
LogContext.PushProperty("RequestBody", requestBodyText);
// Rewind the Body stream so the next middleware can use it.
context.Request.Body.Position = 0;
//Save the reference to the original response body stream
var originalBodyStream = context.Response.Body;
// Create a new rewindable memory stream...
using (var responseBody = new MemoryStream())
{
//...and use that for the temporary response body
context.Response.Body = responseBody;
// Continue down the Middleware pipeline, eventually returning to this class
await _next(context);
// Get the response from the server and save it in the LogContext
context.Response.Body.Position = 0;
string responseBodyText = await new StreamReader(context.Response.Body).ReadToEndAsync();
context.Response.Body.Position = 0;
LogContext.PushProperty("ResponseBody", responseBodyText);
// Copy the contents of the new memory stream (which contains the response) to the original stream,
// which is then returned to the client.
await responseBody.CopyToAsync(originalBodyStream);
}
}
}
我在激活 Serilog 后激活它,以便当控件返回到 Serilog 请求日志记录时其上下文可用。
app.UseSerilogRequestLogging(options =>
{
options.MessageTemplate =
"HTTP {RequestMethod} {RequestPath} {RequestBody} responded {StatusCode} with {ResponseBody} in {Elapsed:0.0000}";
}
);
app.UseMiddleware<MyRequestResponseLoggingMiddlewareForSerilog>();
不幸的是,日志没有显示尸体。相反,我看到模板 {RequestBody} 和 {ResponseBody} 中的文本,但没有被替换。
[16:08:55 INF] HTTP GET /api/v1/products {RequestBody} responded 200 with {ResponseBody} in 520.2372
为了最终让它工作,我用这个替换了我的中间件:
public class MyRequestResponseLoggingMiddlewareForSerilog
{
private readonly RequestDelegate _next;
public MyRequestResponseLoggingMiddlewareForSerilog(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
// Allow multiple reading of request body
context.Request.EnableBuffering();
// Create a new rewindable memory stream for the response which downstream middleware will use
context.Response.Body = new MemoryStream();
// Continue down the Middleware pipeline, eventually returning to this class
await _next(context);
}
public static async void EnrichDiagnosticContext(IDiagnosticContext diagnosticContext, HttpContext httpContext)
{
Stream requestBodyStream = httpContext.Request.Body;
requestBodyStream.Position = 0;
string requestBodyAsText = await new StreamReader(requestBodyStream).ReadToEndAsync();
requestBodyStream.Position = 0;
diagnosticContext.Set("RequestBody", requestBodyAsText);
Stream responseBodyStream = httpContext.Response.Body;
responseBodyStream.Position = 0;
string responseBodyAsText = await new StreamReader(responseBodyStream).ReadToEndAsync();
responseBodyStream.Position = 0;
diagnosticContext.Set("ResponseBody", responseBodyAsText);
}
}
当我激活请求日志记录时,激活 Serilog 的“丰富器”。
app.UseSerilogRequestLogging(options =>
{
options.MessageTemplate =
"HTTP {RequestMethod} {RequestPath} {RequestBody} responded {StatusCode} with {ResponseBody} in {Elapsed:0.0000}";
options.EnrichDiagnosticContext = MyRequestResponseLoggingMiddlewareForSerilog.EnrichDiagnosticContext;
}
);
这给了我想要的日志条目:
[16:21:40 INF] HTTP GET /api/v1/products {} responded 200 with {"products":[{"family":"ProductFamily","products":["C91C","C90C","C89C","C89B","C23A","C09C","C09B","A43A","A31B"]}]} in 574.4265
但这给我留下了以下问题:
MemoryStream
以使响应正文的流可回滚,但我无法将其丢弃在代码中的任何位置,因为后续中间件需要它。此外,原始响应正文流也仍然存在,但不再使用,并且可能也未处理。我如何以及在哪里处理这些流以进行清理?
如果我在 builder.build() 之前使用下面的代码初始化 serilog 记录器,则不会记录来自 HttpLogging 的日志:
Log.Logger = new LoggerConfiguration()
.WriteTo.MSSqlServer(configuration.GetConnectionString("LoggerDbContext"), sinkOptions: new MSSqlServerSinkOptions()
{
AutoCreateSqlTable = true,
SchemaName = "dbo",
TableName = "Logs"
}, logEventFormatter: new RenderedCompactJsonFormatter())
.WriteTo.Console()
.ReadFrom.Configuration(configuration)
.CreateLogger();
我更改了初始化,如下所示,serilog 开始记录整个请求和响应主体:
// HttpLogging
builder.Services.AddHttpLogging(logging =>
{
logging.LoggingFields = HttpLoggingFields.All;
logging.RequestBodyLogLimit = int.MaxValue;
logging.ResponseBodyLogLimit = int.MaxValue;
logging.MediaTypeOptions.AddText("application/javascript");
logging.CombineLogs = true;
});
builder.Services.AddSerilog(config =>
{
config.WriteTo.MSSqlServer(configuration.GetConnectionString("LoggerDbContext"),
sinkOptions: new MSSqlServerSinkOptions()
{
AutoCreateSqlTable = true, SchemaName = "dbo", TableName = "Logs"
}, logEventFormatter: new RenderedCompactJsonFormatter())
.WriteTo.Console()
.ReadFrom.Configuration(configuration);
});