对ASP .NET Core 中的请求进行响应日志记录

作者:微信公众号:【架构师老卢】
6-8 10:25
86

概述:记录 ASP .NET Core http 请求和响应是几乎每个 .NET 开发人员迟早都会面临的常见任务。在很长一段时间里,开发团队选择的最流行的方法似乎是编写自定义中间件。但是由于我们内置了一个库。所以,亲爱的 .NET 海狸,让我们看看Microsoft给我们的日志!设置日志记录我们将从 .NET 提供给我们的最小设置开始,通过运行以下控制台命令dotnet new web检查 http 请求的每个部分的日志记录非常重要,例如请求正文、查询字符串、错误和路径参数。因此,默认终结点将不起作用。让我们创建一个带有二十一点查询和路由参数的派对:app.MapPost(/parties/{par

记录 ASP .NET Core http 请求和响应是几乎每个 .NET 开发人员迟早都会面临的常见任务。在很长一段时间里,开发团队选择的最流行的方法似乎是编写自定义中间件。但是由于我们内置了一个库。所以,亲爱的 .NET 海狸,让我们看看Microsoft给我们的日志!

设置日志记录

我们将从 .NET 提供给我们的最小设置开始,通过运行以下控制台命令

dotnet new web

检查 http 请求的每个部分的日志记录非常重要,例如请求正文、查询字符串、错误和路径参数。因此,默认终结点将不起作用。让我们创建一个带有二十一点查询和路由参数的派对:

app.MapPost("/parties/{partyId}/guests", (string partyId, [FromQuery] bool? loungeAccess, Guest visitor) => {  
    if (loungeAccess == true && !visitor.Vip)   
        throw new NotEnoughLevelException();  
  
    return new Ticket(  
        PartyId: partyId,  
        Receiver: visitor.Name,  
        LoungeAccess: loungeAccess ?? false,  
        Code: Guid.NewGuid().ToString()  
    );  
});  
  
app.Run();  
  
public record Guest(string Name, bool Vip);  
public record Ticket(string PartyId, string Receiver, bool LoungeAccess, string Code);  
public class NotEnoughLevelException : Exception;

默认情况下,对于 ASP,.NET Core 将大致中断请求并返回 。让我们返回一个错误对象。实现它的最简单方法可能是使用 nuget 包。要安装它,我们将使用以下命令:

dotnet add package Nist.Errors

并将我们的异常映射到相应的错误:

app.UseErrorBody<Error>(ex => ex switch {  
    NotEnoughLevelException _ => new (HttpStatusCode.BadRequest, "NotEnoughLevel"),  
    _ => new (HttpStatusCode.InternalServerError, "Unknown")  
}, showException: false);

现在,让我们来看看请求和响应日志记录。启用它的最简约方法包括 4 个步骤:

1. 注册 http 日志服务

builder.Services.AddHttpLogging(o => {});

2. 附加 http 日志中间件

app.UseHttpLogging();

3. 删除以最大程度地减少我们的配置开销appsettings.Development.json

4. 由于默认情况下,日志级别为 http 日志中间件指定专用日志级别:

"Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"

在所有这些更改之后,我们的完整内容将如下所示:Program.cs

using Microsoft.AspNetCore.Mvc;  
using Nist.Errors;  
using System.Net;  
  
var builder = WebApplication.CreateBuilder(args);  
  
builder.Services.AddHttpLogging(o => {});  
  
var app = builder.Build();  
  
app.UseHttpLogging();  
  
app.UseErrorBody<Error>(ex => ex switch {  
    NotEnoughLevelException _ => new (HttpStatusCode.BadRequest, "NotEnoughLevel"),  
    _ => new (HttpStatusCode.InternalServerError, "Unknown")  
}, showException: false);  
  
app.MapPost("/parties/{partyId}/guests", (string partyId, \[FromQuery\] bool? loungeAccess, Guest visitor) => {  
    if (loungeAccess == true && !visitor.Vip)   
        throw new NotEnoughLevelException();  
  
    return new Ticket(  
        PartyId: partyId,  
        Receiver: visitor.Name,  
        LoungeAccess: loungeAccess ?? false,  
        Code: Guid.NewGuid().ToString()  
    );  
});  
  
app.Run();  
  
public record Guest(string Name, bool Vip);  
public record Ticket(string PartyId, string Receiver, bool LoungeAccess, string Code);  
public class NotEnoughLevelException : Exception;

让我们测试一下,从一个普通的请求开始:

POST http://localhost:5244/parties/new-year/guests?loungeAccess=true  
  
{  
    "name": "Paul",  
    "vip" : true  
}

以下是我们得到的日志:

现在让我们转向一个“坏”请求。

POST http://localhost:5244/parties/halloween/guests?loungeAccess=true  
  
{  
    "name": "John",  
    "vip" : false  
}

我们将得到:

配置日志记录

正如你所看到的,虽然我们记录了很多东西,但我们不能真正说太多关于请求的信息。我们可能会弄清楚命中了哪个端点以及我们的响应是否成功(如果我们使用 http 状态代码),但仅此而已。此外,在高负载环境中,匹配请求和响应可能是一个挑战,因为它们是单独记录的。因此,让我们看看我们可以记录什么。代码如下:CombineLogs

builder.Services.AddHttpLogging(o => {  
    o.CombineLogs = true;  
  
    o.LoggingFields = HttpLoggingFields.All | HttpLoggingFields.RequestQuery;  
});

HttpLoggingFields.All是一种谎言,它在日志中指定:“HttpRequest.QueryString 不包含在此标志中,因为它可能包含私人信息”。因此,我们需要手动附加 RequestQuery

这就是我们现在得到的:

这要好得多,现在我们可以看到到底收到了什么,响应了什么,以及处理花费了多少时间。然而,日志仍然让人感到不知所措——我们记录了很多标头,这似乎没有任何价值。让我们只留下我们需要的东西:

builder.Services.AddHttpLogging(o => {  
    o.CombineLogs = true;  
  
    o.LoggingFields = HttpLoggingFields.RequestQuery  
        | HttpLoggingFields.RequestMethod  
        | HttpLoggingFields.RequestPath  
        | HttpLoggingFields.RequestBody  
        | HttpLoggingFields.ResponseStatusCode  
        | HttpLoggingFields.ResponseBody  
        | HttpLoggingFields.Duration;  
});

现在,我们将以更紧凑的格式获得几乎相同数量的有用信息:

让它变得更好

但是,我们所拥有的仍然存在一些问题:

  1. 请注意,由于我们具有 path 变量,因此单个端点的字段值不相同。这将阻止我们通过特定端点获取分析。partyIdpath
  2. 请求-响应日志与发生的异常的日志之间没有连接。这可能会使查找不成功响应的确切异常变得复杂。特别是当异常无法识别时。
  3. 由于多种原因,日志仍然占用大量空间。首先,我们有无用的不可删除字段 (, )。其次,我们有一个非常“宽”的格式,总共使用 4 行作为方法、路径和查询字符串。RequestBodyStatusPathBase

幸运的是,我们有一个 nuget 包,它涵盖了内置库中的盲点。让我们来看看。首先,我们需要安装它:

dotnet add package Nist.Logging

然后只更改 3 行:

using Nist.Logs;  
  
// remove builder.Services.AddHttpLogging  
  
app.UseHttpIOLogging(); // instead of app.UseHttpLogging();

我们得到了最少但功能齐全的日志:

虽然, 仍然记录发生的异常,但我们不再需要依赖它,因为我们在日志记录中间件中记录了异常。您甚至可以 将 with 静音ExceptionHandlerMiddlewareExceptionHandlerMiddleware"Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware": "None"appsettings.json

回顾

总而言之,让我们看一下我们能够获得的最佳请求-响应日志记录:

尽管由于 .NET 6 提供了开箱即用的广泛 http 日志记录功能,但仍缺少一些功能。因此,我们改用了 nuget 包。下表总结了库的功能列表:Microsoft.AspNetCoreNist.Logs

| Feature            | Microsoft.AspNetCore.HttpLogging     | Nist.Logs |  
|--------------------|--------------------------------------|-----------|  
| Full Uri           | ❌ - Only Path and Query Separately   | ✅         |  
| Minimalism         | ❌ - Few unremovable redundant fields | ✅         |  
| Exception attached | ❌                                    | ✅         |  
| Endpoint id        | ❌                                    | ✅         |  
| Single Line        | ✅                                    | ✅         |  
| Http Method        | ✅                                    | ✅         |  
| Request Body       | ✅                                    | ✅         |  
| Response Body      | ✅                                    | ✅         |  
| Response Code      | ✅                                    | ✅         |  
| Request Duration   | ✅                                    | ✅         |

为了快速参考,这是最后一张:Program.cs

using Microsoft.AspNetCore.Mvc;  
using Nist.Logs;  
using Nist.Errors;  
using System.Net;  
  
var builder = WebApplication.CreateBuilder(args);  
var app = builder.Build();  
app.UseHttpIOLogging();  
app.UseErrorBody\<Error>(ex => ex switch {  
    NotEnoughLevelException _ => new (HttpStatusCode.BadRequest, "NotEnoughLevel"),  
    _ => new (HttpStatusCode.InternalServerError, "Unknown")  
}, showException: false);  
app.MapPost("/parties/{partyId}/guests", (string partyId, [FromQuery] bool? loungeAccess, Guest visitor) => {  
    if (loungeAccess == true && !visitor.Vip)   
        throw new NotEnoughLevelException();  
    return new Ticket(  
        PartyId: partyId,  
        Receiver: visitor.Name,  
        LoungeAccess: loungeAccess ?? false,  
        Code: Guid.NewGuid().ToString()  
    );  
});  
app.Run();  
public record Guest(string Name, bool Vip);  
public record Ticket(string PartyId, string Receiver, bool LoungeAccess, string Code);  
public class NotEnoughLevelException : Exception;
阅读排行