【问题标题】:Serilog logging web-api methods, adding context properties inside middlewareSerilog 记录 web-api 方法,在中间件中添加上下文属性
【发布时间】:2020-05-21 10:25:10
【问题描述】:

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

对于 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" 没有任何内容(甚至没有添加为属性) 感谢任何帮助。

【问题讨论】:

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


    【解决方案1】:

    从几个帖子中收集信息并根据我的需要对其进行自定义后,我找到了一种将请求和响应正文数据记录为 serilog 日志结构属性的方法。

    我没有找到一种方法将请求和响应正文都记录在一个地方(在中间件的Invoke 方法中),但我找到了一种解决方法。由于请求处理管道的性质,我必须这样做:

    Startup.cs中的代码:

    app.UseMiddleware<RequestResponseLoggingMiddleware>();
    app.UseSerilogRequestLogging(opts => opts.EnrichDiagnosticContext = LogHelper.EnrichFromRequest);
    
    • 我使用LogHelper 类来丰富请求属性,正如Andrew Locks post 中所述。

    • 当请求处理到达中间件时,在中间件的Invoke 方法中,我正在读取仅请求正文数据,并将此值设置为我添加到的静态字符串属性LogHelper 班级。这样,我已经将请求正文数据读取并存储为字符串,并且可以在调用 LogHelper.EnrichFromRequest 方法时将其添加为丰富器

    • 读取请求正文数据后,我正在复制指向原始响应正文流的指针

    • 接下来调用await _next(context);,填充context.Response,请求处理从中间件的Invoke方法退出,转到LogHelper.EnrichFromRequest

    • 此时 LogHelper.EnrichFromRequest 正在执行,正在读取响应正文数据,并将其设置为丰富器,以及之前存储的请求正文数据和一些附加属性

    • 处理返回到中间件Invoke方法(就在await _next(context);之后),并将新内存流(包含响应)的内容复制到原始流中,

    以下是上述LogHelper.csRequestResponseLoggingMiddleware.cs 类中的代码:

    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}";
        }
    }
    

    【讨论】:

    • 嗨,什么是 GetEndpoint() ?它似乎在 Core 3.1 中不可用(在使用 IHttpContextAccessor 时也不可用)。
    • @Yovav 嗨。 GetEndpoint() 是 HttpContext 对象的扩展方法。它来自 NuGet 包 Microsoft.AspNetCore.Http.Extensions 2.2.0
    • 见我的评论添加扩展应用程序uses.net core 3.0 或更高版本stackoverflow.com/questions/61847821/…
    • 任何人都可以帮助知道日志将存储在哪里,我已经尝试过这段代码。我正在使用 serilog 将日志保存在文本文件中。但是这里的日志不存储在文本文件中。我必须在此代码中添加任何单独的内容吗?
    • @aakash 据我记得,这应该在你项目的 appsettings.json 中配置,see here
    【解决方案2】:

    接受的答案不是线程安全的。

    LogHelper.RequestPayload = requestBodyPayload;

    当有多个并发请求时,此分配可能会导致意外的日志记录结果。
    我没有使用静态变量,而是将请求正文直接推送到 Serilog 的 LogContext 属性中。

    【讨论】:

      【解决方案3】:

      如果登录到该文件,我们可以在 .net core 5.0 中添加以下代码以及来自@Vladimir 的答案。

      在 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"
          }
        },
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多