依乐祝

依乐祝

授之以鱼,不如授之以渔!右侧微信扫描二维码关注我们“DotNetCore实战”吧。

如何从Serilog请求日志记录中排除健康检查终结点

这是在ASP.NET Core 3.X中使用Serilog.AspNetCore系列文章的第四篇文章:。

  1. 第1部分-使用Serilog RequestLogging减少日志详细程度
  2. 第2部分-使用Serilog记录所选的终结点属性
  3. 第3部分-使用Serilog.AspNetCore记录MVC属性
  4. 第4部分-从Serilog请求日志记录中排除健康检查端点(本文)

作者:依乐祝

译文地址:https://www.cnblogs.com/yilezhu/p/12253361.html

原文地址:https://andrewlock.net/using-serilog-aspnetcore-in-asp-net-core-3-excluding-health-check-endpoints-from-serilog-request-logging/

在本系列的前几篇文章中,我描述了如何配置Serilog的RequestLogging中间件以向Serilog的请求日志摘要中添加附加属性,例如请求主机名或选定的端点名称。我还展示了如何使用过滤器将MVC或RazorPage特定的属性添加到摘要日志。

在本文中,我将展示如何过滤掉某个特定请求的摘要日志消息。当您有一个访问比较频繁的端点时,这非常有用,因为为每个请求都进行记录几乎没有什么价值。

健康检查访问较频繁

这篇文章的动机来自我们在Kubernetes中运行应用程序时看到的行为。Kubernetes使用两种类型的“健康检查”(或“探针”)来检查应用程序是否正常运行:liveness probes和readiness probes。您可以将探测配置为向应用程序发出HTTP请求,作为应用程序正常运行的指示器。

从Kubernetes 1.16版开始,存在第三种探针,即startup probe

在ASP.NET Core 2.2+中提供的健康检查终结点非常适合这些探针。您可以设置一个简单,没有任何返回值的健康检查,该健康检查对每个请求返回200 OK的响应,以使Kubernetes知道您的应用程序没有崩溃。

在ASP.NET Core 3.x中,可以使用终结点路由来配置健康检查。您必须在Startup.cs中ConfigureServices中通过调用AddHealthChecks()来添加必须的服务,并在Configure中使用MapHealthChecks()来添加健康检查终结点:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // ..other service configuration

        services.AddHealthChecks(); // Add health check services
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            // .. other middleware
            app.UseRouting();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapHealthChecks("/health"); //Add health check endpoint
                endpoints.MapControllers();
            });
        }
}

在上面的示例中,向/healthz发送请求将调用健康检查终结点。由于我没有配置任何运行状况检查200,因此只要应用程序正在运行,端点将始终返回响应:

在上面的示例中,向/healthz发送请求将调用运行状况检查终结点。由于我没有配置任何运行的健康检查,因此只要应用程序正在运行,端点将始终返回200响应:

image-20200201214448978

这里存在的唯一的问题是Kubernetes将非常频繁的调用这个终结点。当然,确切的频率由您决定,但每10秒检查一次应该是很常见的。但是如果你想让Kubernetes可以快速重启有故障的Pod的话,您就需要一个相对较高的频率了。

这本身不是问题;Kestrel每秒可以处理数百万个请求,因此这不是性能问题。这里令人比较烦恼的问题是每个请求都会生成一定数量的日志。虽然它没有MVC基础架构的请求所示的那么多-每个请求10个日志,但是即使每个请求只有1个日志(就像我们从Serilog.AspNetCore获得的那样)都可能会令人不快。

这里的主要问题是成功进行健康检查请求的日志实际上并未告诉我们任何有用的信息。它们与任何业务活动都不相关,它们纯粹是基础设施。这里如果能够跳过这些请求的Serilog请求摘要日志会很好。在下一部分中,我将介绍我所想出的方法,该方法依赖于本系列前面几篇文章的内容,并在其基础上做出更改。

定制用于Serilog请求日志的日志级别

上一篇文章中,我展示了如何在Serilog请求日志中包括所选终结点。我的方法是在注册Serilog中间件时为RequestLoggingOptions.EnrichDiagnosticContext属性提供一个自定义函数

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // ... Other middleware

    app.UseSerilogRequestLogging(opts
        // EnrichFromRequest helper function is shown in the previous post
        => opts.EnrichDiagnosticContext = LogHelper.EnrichFromRequest); 

    app.UseRouting();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapHealthChecks("/healthz"); //Add health check endpoint
        endpoints.MapControllers();
    });
}

RequestLoggingOptions具有另一个属性,GetLevel该属性的Func<>被用于确定应用于给定请求日志的日志记录级别。默认情况下,它设置为以下功能

public static class SerilogApplicationBuilderExtensions
{
    static LogEventLevel DefaultGetLevel(HttpContext ctx, double _, Exception ex) =>
        ex != null
            ? LogEventLevel.Error 
            : ctx.Response.StatusCode > 499 
                ? LogEventLevel.Error 
                : LogEventLevel.Information;
}

此函数检查是否为请求引发了异常,或者响应代码是否为5xx错误。如果是这样,它将创建一个Error级别的摘要日志,否则将创建一个Information级别日志。

假设您希望将摘要日志记录为Debug而不是Information。首先,您将创建一个具有以下所需逻辑的辅助函数,如下所示:

public static class LogHelper
{
    public static LogEventLevel CustomGetLevel(HttpContext ctx, double _, Exception ex) =>
        ex != null
            ? LogEventLevel.Error 
            : ctx.Response.StatusCode > 499 
                ? LogEventLevel.Error 
                : LogEventLevel.Debug; //Debug instead of Information
}

然后,您可以在调用时设置级别功能UseSerilogRequestLogging()

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // ... Other middleware

    app.UseSerilogRequestLogging(opts => {
        opts.EnrichDiagnosticContext = LogHelper.EnrichFromRequest;
        opts.GetLevel = LogHelper.CustomGetLevel; // Use custom level function
    });

    //... other middleware
}

现在,您的请求摘要日志将全部记录为Debug,除非发生错误(Seq的屏幕截图):

显示调试请求日志的序列

但这如何解决我们的冗长日志的问题呢?

当你在配置Serilog时,你通常应该会定义一个最低请求级别。例如,以下简单配置将默认级别设置为Debug(),并将其写入控制台接收器:

Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Debug()
    .WriteTo.Console()
    .CreateLogger();

因此,过滤日志的最简单方法是使日志级别低于MinimumLevel记录器配置中指定的级别。一般而言,如果使用最低级别Verbose,它将几乎总是被过滤掉。

困难之处在于我们不想总是Verbose用作摘要日志的日志级别。如果这样做,我们将不会获得任何非错误的请求日志,而Serilog中间件将变得毫无意义!

相反,我们希望将日志级别设置为Verbose 针对运行健康检查端点的请求。在下一节中,我将展示如何在不影响其他请求的情况下识别这些请求。

将自定义日志级别用于健康检查终结点请求

我们需要的是能够在写入摘要日志时识别出健康检查的请求的能力。如前所示,该GetLevel()方法将当前HttpContext作为参数,因此理论上有一些可行性。对我来说,最明显的做法是:

  • HttpContext.Request路径与已知的健康检查路径列表进行比较
  • 当健康检查终结点被请求时,使用选定的端点元数据来进行标识

第一种选择是最明显的,但是它真的不值得尝试。一旦你陷入其中,你会发现你必须开始复制请求路径并处理各种边缘情况,因此在这里我将跳过该情况。

第二种方法使用了与我上一篇文章中使用的方法类似,在该方法中,我们获得了EndpointRoutingMiddleware为给定请求选择的IEndpointFeature。此功能(如果存在)提供了所选端点的显示名称和路由数据等详细信息。

如果我们假设健康检查是使用默认显示名称注册的,即"Health checks",则我们可以使用HttpContext来标识“健康检查”的请求,如下所示:

public static class LogHelper
{
    private static bool IsHealthCheckEndpoint(HttpContext ctx)
    {
        var endpoint = ctx.GetEndpoint();
        if (endpoint is object) // same as !(endpoint is null)
        {
            return string.Equals(
                endpoint.DisplayName, 
                "Health checks",
                StringComparison.Ordinal);
        }
        // No endpoint, so not a health check endpoint
        return false;
    }
}

我们可以将此功能与默认GetLevel功能的自定义版本结合使用,以确保运行健康检查请求的摘要日志使用Verbose级别,当发生错误时使用Error而其他请求则使用Information

public static class LogHelper
{
    public static LogEventLevel ExcludeHealthChecks(HttpContext ctx, double _, Exception ex) => 
        ex != null
            ? LogEventLevel.Error 
            : ctx.Response.StatusCode > 499 
                ? LogEventLevel.Error 
                : IsHealthCheckEndpoint(ctx) // Not an error, check if it was a health check
                    ? LogEventLevel.Verbose // Was a health check, use Verbose
                    : LogEventLevel.Information;
        }
}

这个嵌套的三目运算符有一个额外的逻辑-对于无错误,我们检查是否选择了显示名为“Health check”的端点,如果选择了,则使用级别Verbose,否则使用Information

您可以进一步推广此代码,以允许传入其他显示名称或其他自定义使用的日志级别。为了简单起见,我在这里没有这样做,但是GitHub上的相关示例代码显示了如何执行此操作

剩下的就是更新Serilog中间件RequestLoggingOptions以使用您的新功能:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // ... Other middleware

    app.UseSerilogRequestLogging(opts => {
        opts.EnrichDiagnosticContext = LogHelper.EnrichFromRequest;
        opts.GetLevel = LogHelper.ExcludeHealthChecks; // Use the custom level
    });

    //... other middleware
}

这时候当你运行应用程序后检查日志时,您会看到标准请求的普通请求日志,但没有健康检查的日志(除非发生错误!)。在下面的屏幕截图中,我将Serilog配置为也记录Verbose日志,以便您可以查看运行状况检查请求-通常会将它们过滤掉!

Seq屏幕截图显示了使用详细级别的运行状况检查请求

总结

在本文中,我展示了如何为Serilog中间件的RequestLoggingOptions提供一个自定义函数,该函数定义了要为给定请求的日志使用的LogEventLevel。例如,我展示了如何使用它将默认级别更改为Debug。如果您选择的级别低于最低级别,它将被完全过滤掉,并且不会被记录。

我还展示了您可以使用这种方法来过滤通过调用健康检查端点生成的公共(低级别的)请求日志。一般来说,这些请求只有在指出问题时才有意义,但它们通常也会在成功时生成请求日志。由于这些端点被频繁调用,因此它们可以显著增加写入的日志数量(无用)。

本文中的方法是检查选定的IEndpointFeature并检查它是否具有显示名称“Health checks”。如果是,请求日志将使用Verbose级别写入,这通常会被过滤掉。为了更灵活,您可以自定义在这个帖子中显示的日志来处理多个端点名称,或者任何其他的标准。

posted @ 2020-02-03 13:58  依乐祝  阅读(2556)  评论(3编辑  收藏  举报