Serilog 日志记录 web-api 方法,在中间件中添加上下文属性
Serilog logging web-api methods, adding context properties inside middleware
我一直在努力使用 serilog 从中间件记录响应主体有效负载数据。
我正在开发 WEB API Core 应用程序,将 swagger 添加到端点,我的目标是记录每个端点调用带有 serilog 的 .json 文件(请求和响应数据)。
对于 GET 请求,应记录响应主体(作为 属性 添加到 serilog 上下文),对于 POST 请求,两者应记录请求和响应的正文。
我已经创建了中间件并设法从请求和响应流中正确检索数据,并将其作为字符串获取,但只有 "RequestBody" 被正确记录。
调试时,我可以看到读取 request/response 主体工作正常。
以下是 Program->Main 方法的代码摘录:
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.Enrich.FromLogContext()
.CreateLogger();
中间件中的代码:
public async Task Invoke(HttpContext context)
{
// Read and log request body data
string requestBodyPayload = await ReadRequestBody(context.Request);
LogContext.PushProperty("RequestBody", requestBodyPayload);
// Read and log response body data
var originalBodyStream = context.Response.Body;
using (var responseBody = new MemoryStream())
{
context.Response.Body = responseBody;
await _next(context);
string responseBodyPayload = await ReadResponseBody(context.Response);
if (!context.Request.Path.ToString().EndsWith("swagger.json") && !context.Request.Path.ToString().EndsWith("index.html"))
{
LogContext.PushProperty("ResponseBody", responseBodyPayload);
}
await responseBody.CopyToAsync(originalBodyStream);
}
}
private async Task<string> ReadRequestBody(HttpRequest request)
{
HttpRequestRewindExtensions.EnableBuffering(request);
var body = request.Body;
var buffer = new byte[Convert.ToInt32(request.ContentLength)];
await request.Body.ReadAsync(buffer, 0, buffer.Length);
string requestBody = Encoding.UTF8.GetString(buffer);
body.Seek(0, SeekOrigin.Begin);
request.Body = body;
return $"{requestBody}";
}
private async Task<string> ReadResponseBody(HttpResponse response)
{
response.Body.Seek(0, SeekOrigin.Begin);
string responseBody = await new StreamReader(response.Body).ReadToEndAsync();
response.Body.Seek(0, SeekOrigin.Begin);
return $"{responseBody}";
}
正如我提到的,"RequestBody" 已正确记录到文件中,但 "ResponseBody" 没有任何记录(不是甚至添加为 属性)
感谢任何帮助。
从几个帖子中收集信息并根据我的需要对其进行自定义后,我找到了一种将请求和响应正文数据记录为 serilog 日志结构属性的方法。
我没有找到只在一个地方记录请求和响应正文的方法(在中间件的 Invoke
方法中),但我找到了解决方法。由于请求处理管道的性质,这是我必须做的:
Startup.cs
中的代码:
app.UseMiddleware<RequestResponseLoggingMiddleware>();
app.UseSerilogRequestLogging(opts => opts.EnrichDiagnosticContext = LogHelper.EnrichFromRequest);
我已经使用 LogHelper
class 来丰富请求属性,正如 Andrew Locks post.
中所述
当请求处理命中中间件时,在中间件的 Invoke
方法中,我正在读取 only request body data,并将此值设置为我添加到 LogHelper
class 的静态字符串 属性。通过这种方式,我读取并存储了请求正文数据作为字符串,并且可以在 LogHelper.EnrichFromRequest
方法被调用时将其添加为 enricher
读取请求体数据后,我正在复制一个指向原始响应体流的指针
await _next(context);
接下来被调用,context.Response
被填充,请求处理从中间件的 Invoke
方法退出,并转到 LogHelper.EnrichFromRequest
此时LogHelper.EnrichFromRequest
正在执行,正在读取响应体数据,并将其设置为enricher,以及之前存储的请求体数据和一些附加属性
处理returns到中间件Invoke
方法(在await _next(context);
之后),并将新内存流的内容(包含响应)复制到原始流,
以下是上述 LogHelper.cs
和 RequestResponseLoggingMiddleware.cs
classes 中描述的代码:
LogHelper.cs:
public static class LogHelper
{
public static string RequestPayload = "";
public static async void EnrichFromRequest(IDiagnosticContext diagnosticContext, HttpContext httpContext)
{
var request = httpContext.Request;
diagnosticContext.Set("RequestBody", RequestPayload);
string responseBodyPayload = await ReadResponseBody(httpContext.Response);
diagnosticContext.Set("ResponseBody", responseBodyPayload);
// Set all the common properties available for every request
diagnosticContext.Set("Host", request.Host);
diagnosticContext.Set("Protocol", request.Protocol);
diagnosticContext.Set("Scheme", request.Scheme);
// Only set it if available. You're not sending sensitive data in a querystring right?!
if (request.QueryString.HasValue)
{
diagnosticContext.Set("QueryString", request.QueryString.Value);
}
// Set the content-type of the Response at this point
diagnosticContext.Set("ContentType", httpContext.Response.ContentType);
// Retrieve the IEndpointFeature selected for the request
var endpoint = httpContext.GetEndpoint();
if (endpoint is object) // endpoint != null
{
diagnosticContext.Set("EndpointName", endpoint.DisplayName);
}
}
private static async Task<string> ReadResponseBody(HttpResponse response)
{
response.Body.Seek(0, SeekOrigin.Begin);
string responseBody = await new StreamReader(response.Body).ReadToEndAsync();
response.Body.Seek(0, SeekOrigin.Begin);
return $"{responseBody}";
}
}
RequestResponseLoggingMiddleware.cs:
public class RequestResponseLoggingMiddleware
{
private readonly RequestDelegate _next;
public RequestResponseLoggingMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
// Read and log request body data
string requestBodyPayload = await ReadRequestBody(context.Request);
LogHelper.RequestPayload = requestBodyPayload;
// Read and log response body data
// Copy a pointer to the original response body stream
var originalResponseBodyStream = context.Response.Body;
// Create a new 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);
// 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(originalResponseBodyStream);
}
}
private async Task<string> ReadRequestBody(HttpRequest request)
{
HttpRequestRewindExtensions.EnableBuffering(request);
var body = request.Body;
var buffer = new byte[Convert.ToInt32(request.ContentLength)];
await request.Body.ReadAsync(buffer, 0, buffer.Length);
string requestBody = Encoding.UTF8.GetString(buffer);
body.Seek(0, SeekOrigin.Begin);
request.Body = body;
return $"{requestBody}";
}
}
接受的答案不是线程安全的。
LogHelper.RequestPayload = requestBodyPayload;
当有多个并发请求时,此分配可能会导致意外的日志记录结果。
我没有使用静态变量,而是直接将请求主体推送到 Serilog 的 LogContext 属性 中。
如果登录文件,我们可以在.net core 5.0中添加以下代码和答案。
在Program.cs
中添加UseSerilog
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
.UseSerilog((hostingContext, services, loggerConfig) =>
loggerConfig.ReadFrom.Configuration(hostingContext.Configuration)
.WriteTo.Logger(lc => lc.Filter.ByIncludingOnly(Matching.FromSource("Serilog.AspNetCore.RequestLoggingMiddleware")).WriteTo.File(path: "Logs/WebHookLog_.log",
outputTemplate: "{Timestamp:o}-{RequestBody}-{ResponseBody}-{Host}-{ContentType}-{EndpointName} {NewLine}", rollingInterval: RollingInterval.Day))
);
要添加的字段 appsettings.json:
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"WriteTo": [
{ "Name": "Console" },
{
"Name": "File",
"Args": {
"path": "Logs/applog_.log",
"outputTemplate": "{Timestamp:o} [{Level:u3}] ({SourceContext}) {Message}{NewLine}{Exception}",
"rollingInterval": "Day",
"retainedFileCountLimit": 7
}
}
],
"Enrich": [ "FromLogContext", "WithMachineName" ],
"Properties": {
"Application": "AspNetCoreSerilogDemo"
}
},
我一直在努力使用 serilog 从中间件记录响应主体有效负载数据。 我正在开发 WEB API Core 应用程序,将 swagger 添加到端点,我的目标是记录每个端点调用带有 serilog 的 .json 文件(请求和响应数据)。
对于 GET 请求,应记录响应主体(作为 属性 添加到 serilog 上下文),对于 POST 请求,两者应记录请求和响应的正文。 我已经创建了中间件并设法从请求和响应流中正确检索数据,并将其作为字符串获取,但只有 "RequestBody" 被正确记录。
调试时,我可以看到读取 request/response 主体工作正常。
以下是 Program->Main 方法的代码摘录:
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.Enrich.FromLogContext()
.CreateLogger();
中间件中的代码:
public async Task Invoke(HttpContext context)
{
// Read and log request body data
string requestBodyPayload = await ReadRequestBody(context.Request);
LogContext.PushProperty("RequestBody", requestBodyPayload);
// Read and log response body data
var originalBodyStream = context.Response.Body;
using (var responseBody = new MemoryStream())
{
context.Response.Body = responseBody;
await _next(context);
string responseBodyPayload = await ReadResponseBody(context.Response);
if (!context.Request.Path.ToString().EndsWith("swagger.json") && !context.Request.Path.ToString().EndsWith("index.html"))
{
LogContext.PushProperty("ResponseBody", responseBodyPayload);
}
await responseBody.CopyToAsync(originalBodyStream);
}
}
private async Task<string> ReadRequestBody(HttpRequest request)
{
HttpRequestRewindExtensions.EnableBuffering(request);
var body = request.Body;
var buffer = new byte[Convert.ToInt32(request.ContentLength)];
await request.Body.ReadAsync(buffer, 0, buffer.Length);
string requestBody = Encoding.UTF8.GetString(buffer);
body.Seek(0, SeekOrigin.Begin);
request.Body = body;
return $"{requestBody}";
}
private async Task<string> ReadResponseBody(HttpResponse response)
{
response.Body.Seek(0, SeekOrigin.Begin);
string responseBody = await new StreamReader(response.Body).ReadToEndAsync();
response.Body.Seek(0, SeekOrigin.Begin);
return $"{responseBody}";
}
正如我提到的,"RequestBody" 已正确记录到文件中,但 "ResponseBody" 没有任何记录(不是甚至添加为 属性) 感谢任何帮助。
从几个帖子中收集信息并根据我的需要对其进行自定义后,我找到了一种将请求和响应正文数据记录为 serilog 日志结构属性的方法。
我没有找到只在一个地方记录请求和响应正文的方法(在中间件的 Invoke
方法中),但我找到了解决方法。由于请求处理管道的性质,这是我必须做的:
Startup.cs
中的代码:
app.UseMiddleware<RequestResponseLoggingMiddleware>();
app.UseSerilogRequestLogging(opts => opts.EnrichDiagnosticContext = LogHelper.EnrichFromRequest);
我已经使用
LogHelper
class 来丰富请求属性,正如 Andrew Locks post. 中所述
当请求处理命中中间件时,在中间件的
Invoke
方法中,我正在读取 only request body data,并将此值设置为我添加到LogHelper
class 的静态字符串 属性。通过这种方式,我读取并存储了请求正文数据作为字符串,并且可以在LogHelper.EnrichFromRequest
方法被调用时将其添加为 enricher读取请求体数据后,我正在复制一个指向原始响应体流的指针
await _next(context);
接下来被调用,context.Response
被填充,请求处理从中间件的Invoke
方法退出,并转到LogHelper.EnrichFromRequest
此时
LogHelper.EnrichFromRequest
正在执行,正在读取响应体数据,并将其设置为enricher,以及之前存储的请求体数据和一些附加属性处理returns到中间件
Invoke
方法(在await _next(context);
之后),并将新内存流的内容(包含响应)复制到原始流,
以下是上述 LogHelper.cs
和 RequestResponseLoggingMiddleware.cs
classes 中描述的代码:
LogHelper.cs:
public static class LogHelper
{
public static string RequestPayload = "";
public static async void EnrichFromRequest(IDiagnosticContext diagnosticContext, HttpContext httpContext)
{
var request = httpContext.Request;
diagnosticContext.Set("RequestBody", RequestPayload);
string responseBodyPayload = await ReadResponseBody(httpContext.Response);
diagnosticContext.Set("ResponseBody", responseBodyPayload);
// Set all the common properties available for every request
diagnosticContext.Set("Host", request.Host);
diagnosticContext.Set("Protocol", request.Protocol);
diagnosticContext.Set("Scheme", request.Scheme);
// Only set it if available. You're not sending sensitive data in a querystring right?!
if (request.QueryString.HasValue)
{
diagnosticContext.Set("QueryString", request.QueryString.Value);
}
// Set the content-type of the Response at this point
diagnosticContext.Set("ContentType", httpContext.Response.ContentType);
// Retrieve the IEndpointFeature selected for the request
var endpoint = httpContext.GetEndpoint();
if (endpoint is object) // endpoint != null
{
diagnosticContext.Set("EndpointName", endpoint.DisplayName);
}
}
private static async Task<string> ReadResponseBody(HttpResponse response)
{
response.Body.Seek(0, SeekOrigin.Begin);
string responseBody = await new StreamReader(response.Body).ReadToEndAsync();
response.Body.Seek(0, SeekOrigin.Begin);
return $"{responseBody}";
}
}
RequestResponseLoggingMiddleware.cs:
public class RequestResponseLoggingMiddleware
{
private readonly RequestDelegate _next;
public RequestResponseLoggingMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
// Read and log request body data
string requestBodyPayload = await ReadRequestBody(context.Request);
LogHelper.RequestPayload = requestBodyPayload;
// Read and log response body data
// Copy a pointer to the original response body stream
var originalResponseBodyStream = context.Response.Body;
// Create a new 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);
// 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(originalResponseBodyStream);
}
}
private async Task<string> ReadRequestBody(HttpRequest request)
{
HttpRequestRewindExtensions.EnableBuffering(request);
var body = request.Body;
var buffer = new byte[Convert.ToInt32(request.ContentLength)];
await request.Body.ReadAsync(buffer, 0, buffer.Length);
string requestBody = Encoding.UTF8.GetString(buffer);
body.Seek(0, SeekOrigin.Begin);
request.Body = body;
return $"{requestBody}";
}
}
接受的答案不是线程安全的。
LogHelper.RequestPayload = requestBodyPayload;
当有多个并发请求时,此分配可能会导致意外的日志记录结果。
我没有使用静态变量,而是直接将请求主体推送到 Serilog 的 LogContext 属性 中。
如果登录文件,我们可以在.net core 5.0
在Program.cs
中添加UseSerilogpublic static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
.UseSerilog((hostingContext, services, loggerConfig) =>
loggerConfig.ReadFrom.Configuration(hostingContext.Configuration)
.WriteTo.Logger(lc => lc.Filter.ByIncludingOnly(Matching.FromSource("Serilog.AspNetCore.RequestLoggingMiddleware")).WriteTo.File(path: "Logs/WebHookLog_.log",
outputTemplate: "{Timestamp:o}-{RequestBody}-{ResponseBody}-{Host}-{ContentType}-{EndpointName} {NewLine}", rollingInterval: RollingInterval.Day))
);
要添加的字段 appsettings.json:
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"WriteTo": [
{ "Name": "Console" },
{
"Name": "File",
"Args": {
"path": "Logs/applog_.log",
"outputTemplate": "{Timestamp:o} [{Level:u3}] ({SourceContext}) {Message}{NewLine}{Exception}",
"rollingInterval": "Day",
"retainedFileCountLimit": 7
}
}
],
"Enrich": [ "FromLogContext", "WithMachineName" ],
"Properties": {
"Application": "AspNetCoreSerilogDemo"
}
},