Serilog 记录 web-api 方法,在中间件中添加上下文属性

Posted

技术标签:

【中文标题】Serilog 记录 web-api 方法,在中间件中添加上下文属性【英文标题】:Serilog logging web-api methods, adding context properties inside middleware 【发布时间】: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" 没有任何内容(甚至没有添加为属性) 感谢任何帮助。

【问题讨论】:

【参考方案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 或更高版本***.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) MessageNewLineException",
          "rollingInterval": "Day",
          "retainedFileCountLimit": 7
        
      
    ],
    "Enrich": [ "FromLogContext", "WithMachineName" ],
    "Properties": 
      "Application": "AspNetCoreSerilogDemo"
    
  ,

【讨论】:

以上是关于Serilog 记录 web-api 方法,在中间件中添加上下文属性的主要内容,如果未能解决你的问题,请参考以下文章

记录:Serilog的最简配置方法

Serilog:记录到不同的文件

Serilog高级玩法之用Serilog记录所选终结点附加属性

Serilog 中的异常解构

Serilog高级玩法之用Serilog记录所选终结点附加属性

Serilog 记录日志