如何在 serilog 的输出 .net 核心中添加“请求正文”?

Ogu*_*cin 7 c# serilog asp.net-core

我有一个基于 .net core 3.1 的 Web API。

我使用 SeriLog 库作为记录器。

这是我的 SeriLog 配置。Serilog 已从“appsettings.json”配置。

在此处输入图片说明

如果存在,我需要将“请求正文”参数添加到日志的输出中。有什么办法可以配置这个。另外,我分享了我的日志输出。

在此处输入图片说明

小智 10

options.EnrichDiagnosticContext您可以通过从ReadBodyFromRequest方法或从 matthewd98FormatRequest的方法实现逻辑来记录正文 ,但是您还需要将正文添加到模板消息中,因为 Serilog 中的默认模板消息是 并且它不包含任何占位符


HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms
Body

app.UseSerilogRequestLogging(options =>
{
   options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
   {
     // string body = your logic to get body from httpContext.Request.Body
        diagnosticContext.Set("Body", body);
   };
   options.MessageTemplate = "HTTP {RequestMethod} {RequestPath} {Body} responded {StatusCode} in {Elapsed:0.0000}";
});
Run Code Online (Sandbox Code Playgroud)

  • “你获得身体的逻辑”这是最难的部分,这没有回答任何问题 (3认同)
  • 该属性不一定在 MessageTemplate 中可用。这取决于水槽。an 将始终通过其属性可用。 (2认同)

小智 8

我编写了一个自定义中间件来捕获 HTTP 请求和响应。它与 ASP.NET Core 3.X 兼容,也应该适用于 2.X 和 .NET 5.0,尽管我还没有用这些框架版本测试过它。

这是我的 git repo 的链接:https : //github.com/matthew-daddario/AspNetCoreRequestResponseLogger

相关代码是这样的:

    public class RequestResponseLoggerMiddleware
{
    private readonly RequestDelegate _next;
    private readonly bool _isRequestResponseLoggingEnabled;

    public RequestResponseLoggerMiddleware(RequestDelegate next, IConfiguration config)
    {
        _next = next;
        _isRequestResponseLoggingEnabled = config.GetValue<bool>("EnableRequestResponseLogging", false);
    }

    public async Task InvokeAsync(HttpContext httpContext)
    {
        // Middleware is enabled only when the EnableRequestResponseLogging config value is set.
        if (_isRequestResponseLoggingEnabled)
        {
            Console.WriteLine($"HTTP request information:\n" +
                $"\tMethod: {httpContext.Request.Method}\n" +
                $"\tPath: {httpContext.Request.Path}\n" +
                $"\tQueryString: {httpContext.Request.QueryString}\n" +
                $"\tHeaders: {FormatHeaders(httpContext.Request.Headers)}\n" +
                $"\tSchema: {httpContext.Request.Scheme}\n" +
                $"\tHost: {httpContext.Request.Host}\n" +
                $"\tBody: {await ReadBodyFromRequest(httpContext.Request)}");

            // Temporarily replace the HttpResponseStream, which is a write-only stream, with a MemoryStream to capture it's value in-flight.
            var originalResponseBody = httpContext.Response.Body;
            using var newResponseBody = new MemoryStream();
            httpContext.Response.Body = newResponseBody;

            // Call the next middleware in the pipeline
            await _next(httpContext);

            newResponseBody.Seek(0, SeekOrigin.Begin);
            var responseBodyText = await new StreamReader(httpContext.Response.Body).ReadToEndAsync();

            Console.WriteLine($"HTTP request information:\n" +
                $"\tStatusCode: {httpContext.Response.StatusCode}\n" +
                $"\tContentType: {httpContext.Response.ContentType}\n" +
                $"\tHeaders: {FormatHeaders(httpContext.Response.Headers)}\n" +
                $"\tBody: {responseBodyText}");

            newResponseBody.Seek(0, SeekOrigin.Begin);
            await newResponseBody.CopyToAsync(originalResponseBody);
        }
        else
        {
            await _next(httpContext);
        }
    }

    private static string FormatHeaders(IHeaderDictionary headers) => string.Join(", ", headers.Select(kvp => $"{{{kvp.Key}: {string.Join(", ", kvp.Value)}}}"));

    private static async Task<string> ReadBodyFromRequest(HttpRequest request)
    {
        // Ensure the request's body can be read multiple times (for the next middlewares in the pipeline).
        request.EnableBuffering();

        using var streamReader = new StreamReader(request.Body, leaveOpen: true);
        var requestBody = await streamReader.ReadToEndAsync();

        // Reset the request's body stream position for next middleware in the pipeline.
        request.Body.Position = 0;
        return requestBody;
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 这是如何被接受的答案?它甚至没有提到 Serilog。 (20认同)

Sim*_*ell 8

如果您可以升级到 dotnet core 6,则无需编写自己的请求/响应主体日志记录中间件。微软在aspnet框架中有一个。

这可以设置为与serilog一起使用,尽管我发现serilog文档是错误的,因为启动代码需要Microsoft的UseHttpLogging和Serilogs UseSerilogRequestLogging(Serilog doco说只使用他们的...没有写主体) Serilog文档 Microsoft的新aspnet dotnet 6 日志记录文档

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseHsts();
    }

    app
        .UseHttpsRedirection()
        .UseRouting()
        .UseHttpLogging()
        .UseSerilogRequestLogging()
        .UseSwagger()
        .UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Some.api"))
        .UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
}     
Run Code Online (Sandbox Code Playgroud)

要完成此操作(根据微软日志记录中的 doco),您可以设置诸如

        services.AddHttpLogging(logging =>
        {
            logging.LoggingFields = HttpLoggingFields.All;
            logging.RequestHeaders.Add(HeaderNames.Accept);
            logging.RequestHeaders.Add(HeaderNames.ContentType);
            logging.RequestHeaders.Add(HeaderNames.ContentDisposition);
            logging.RequestHeaders.Add(HeaderNames.ContentEncoding);
            logging.RequestHeaders.Add(HeaderNames.ContentLength);
            
            logging.MediaTypeOptions.AddText("application/json");
            logging.MediaTypeOptions.AddText("multipart/form-data");
            
            logging.RequestBodyLogLimit = 1024;
            logging.ResponseBodyLogLimit = 1024;
        });
Run Code Online (Sandbox Code Playgroud)

并且不要忘记用于 Microsoft 日志记录和 Serilog 的 appsettings.json。例如:

  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Information",
      "Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
    }
  },
  "Serilog": {
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Information",
        "Microsoft.Hosting.Lifetime": "Information",
        "Microsoft.AspNetCore": "Information",
        "Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
      }
    },
    "WriteTo": [
      {
        "Name": "File",
        "Args": { "path":  "c:/data/logs/aspnetcore-log-.txt", "rollingInterval": "Day" }
      }
    ]
  },
Run Code Online (Sandbox Code Playgroud)

并且需要调用UseSerilog扩展,如:

public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .UseSerilog((context, services, configuration) => configuration
                .ReadFrom.Configuration(context.Configuration)
                .ReadFrom.Services(services)
                .Enrich.FromLogContext()
                .WriteTo.Console())
            .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
Run Code Online (Sandbox Code Playgroud)

  • 这会添加默认的 Microsoft 聊天记录器。是的,您确实获得了请求/响应主体,但您还获得了许多其他日志行。SerilogRequestLogging 是针对每个日志没有多行的确切用例而创建的。所以我错误地赞成了这个答案,在我看来这不是一个好的答案。综上所述; 它添加了另一个记录相同信息的记录器,因此除了请求/响应主体之外,您最终还会得到重复的日志信息和爆炸的日志。 (3认同)

Hak*_*tık 7

@Alexander 的上述回答确实做得很好,但它没有解释如何获取身体,这是很难正确完成的。
这是完整的答案

首先,您需要一个新的中间件

public class ResetTheBodyStreamMiddleware
{
    private readonly RequestDelegate _next;

    public ResetTheBodyStreamMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // Still enable buffering before anything reads
        context.Request.EnableBuffering();

        // Call the next delegate/middleware in the pipeline
        await _next(context);

        // Reset the request body stream position to the start so we can read it
        context.Request.Body.Position = 0;
    }
}
Run Code Online (Sandbox Code Playgroud)

然后您需要注册中间件然后注册该UseSerilogRequestLogging方法。

app.UseMiddleware<ResetTheBodyStreamMiddleware>();

app.UseSerilogRequestLogging(options =>
    options.EnrichDiagnosticContext = async (diagnosticContext, context) =>
    {
        // Reset the request body stream position to the start so we can read it
        context.Request.Body.Position = 0;

        // Leave the body open so the next middleware can read it.
        using StreamReader reader = new(
            context.Request.Body,
            encoding: Encoding.UTF8,
            detectEncodingFromByteOrderMarks: false);

        string body = await reader.ReadToEndAsync();

        if (body.Length is 0)
            return;

        object? obj = JsonSerializer.Deserialize<object>(body);
        if (obj is null)
            return;

        diagnosticContext.Set("Body", obj);
        options.MessageTemplate = "HTTP {RequestMethod} {RequestPath} {Body} responded {StatusCode} in {Elapsed:0.0000}";
    }
);
Run Code Online (Sandbox Code Playgroud)