asp.net-core - Serilog 日志记录 web-api 方法,在中间件中添加上下文属性

标签 asp.net-core asp.net-core-webapi serilog asp.net-core-middleware

我一直在努力使用 serilog 记录响应正文有效负载数据,从中间件记录。
我正在开发 WEB API Core 应用程序,将 swagger 添加到端点,我的目标是使用 serilog(请求和响应数据)将每个端点调用记录到 .json 文件。

对于 GET 请求,应记录响应正文(作为属性添加到 serilog 上下文),对于 POST 请求,应记录请求正文和响应正文。
我创建了中间件并设法从请求和响应流中正确检索数据,并将其作为字符串获取,但只有“RequestBody”被正确记录。

调试时,我可以看到读取请求/响应正文工作正常。

以下是摘自 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用于丰富请求属性的类,正如 Andrew Locks post 中所述.
  • 当请求处理命中中间件时,在中间件的 Invoke 中我正在阅读的方法 仅请求正文数据 ,并将此值设置为我已添加到 LogHelper 的静态字符串属性类(class)。通过这种方式,我将请求正文数据读取并存储为字符串,并且可以在 LogHelper.EnrichFromRequest 时将其添加为丰富器。方法被调用
  • 读取请求正文数据后,我正在复制指向原始响应正文流的指针
  • await _next(context);接下来被调用,context.Response填充,请求处理退出中间件的 Invoke方法,然后转到 LogHelper.EnrichFromRequest
  • 此时此刻 LogHelper.EnrichFromRequest正在执行,读取响应正文数据 现在,并将其设置为丰富器,以及之前存储的请求正文数据和一些附加属性
  • 处理返回中间件Invoke方法(紧跟在 await _next(context); 之后),并将新内存流(包含响应)的内容复制到原始流

  • 以下是上述 LogHelper.cs 中描述的代码和 RequestResponseLoggingMiddleware.cs类:

    日志助手.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}";
        }
    }
    

    关于asp.net-core - Serilog 日志记录 web-api 方法,在中间件中添加上下文属性,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60076922/

    相关文章:

    asp.net-core - Serilog 中是否有一个选项可以在运行时更改日志文件参数,就像更改 LogLevel 一样?

    c# - 未从 appsettings 文件应用 ASP.Net Core 2 中的 Serilog JsonFormatter

    c# - 如何访问 DbContext 类中的配置?

    c# - SupportedUICultures 不显示所有文化

    asp.net - 我可以坚持使用 ASP.Net 并从 EF6 迁移到 EF Core 吗?

    c# - 最佳实践的全局结果-Asp.Net Core 3和Angular 9中的错误处理

    c# - 在多个项目中使用 Asp.Net Core 2 注入(inject) Serilog

    azure - ASP.NET 5 (RC1) 错误网关 : The specified CGI application encountered an error and the server terminated the process

    unit-testing - 使用 HttpRequestMessage 对 Azure Functions 进行单元测试

    c# - 如何将图像从 React 上传到 ASP.NET Core Web API?