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.csRequestResponseLoggingMiddleware.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"
    }
  },