Serilog文档翻译系列(八) - 记录器的生命周期、可靠性

01、记录器的生命周期

Serilog 大多数情况下“只需使用”,并且在创建和处理日志记录器时不需要过多考虑。然而,由于以下原因:

某些接收器(sink)涉及后台进程,特别是那些使用网络的接收器;

一些接收器(尤其是文件和滚动文件接收器)所持有的资源;

因此,某些使用模式在效果和可靠性上表现得更好。

1、在所有应用程序中

使用 Serilog 最简单的方法是通过全局 Log 类:

Log.Logger = new LoggerConfiguration()
    .WriteTo.File(@"myapp\log.txt")
    .CreateLogger();
Log.Information("Hello!");
// Your application runs, then:
Log.CloseAndFlush();

如果这样做,只需配置一次日志记录器,然后在应用程序的整个生命周期中使用它。

要创建更专业的日志记录器:

调用 Log.ForContext(...) 来接收一个附加了额外属性的 ILogger;这不需要任何特殊的关闭/刷新逻辑,因为这将由父日志记录器处理。

在少数情况下,可以使用单独的 LoggerConfiguration 创建一个额外的 ILogger,并使用 WriteTo.Logger(Log.Logger) 将事件传递到根日志记录器;在这种情况下,必须遵循下面的处置逻辑。

2、不使用 Log

如果您不希望使用静态 Log 类,可以使用 LoggerConfiguration 创建一个 ILogger。

using (var log = new LoggerConfiguration()
        .WriteTo.File(@"myapp\log.txt")
        .CreateLogger())
{
    log.Information("Hello again!");
    // Your app runs, then disposal of `log` flushes any buffers
}

在这种情况下,不使用 Log.CloseAndFlush()。相反,当应用程序不再需要日志记录器时,会进行处置。

只有通过 LoggerConfiguration 创建的根日志记录器需要以这种方式处理。从 ForContext() 和类似方法返回的 ILoggers 不需要任何特殊处理。

3、使用 IoC 容器

请参见 Autofac 集成示例,了解如何与 Autofac IoC 容器一起使用可注入的 ILoggers。如果您希望更新此页面以提供其他容器的说明,请提出问题。

02、可靠性

Serilog 认为,综合考虑,日志记录的优先级低于其他应用程序代码,绝不应在可避免的情况下影响正在运行的应用程序的操作。

在实践中,这主要转化为一种处理 Serilog 中异常的策略。在这个过程中会对可用性做出一些妥协。本文档解释了作为 Serilog 库用户您可以期待的内容,以及如果您正在扩展 Serilog,如何编写与其余代码库良好配合的代码。

1、配置

在配置时,即调用 LoggerConfiguration 方法时,错误分为两类。

运行时配置错误

如果由于主机机器的运行时状态无法配置接收器(sink),Serilog 将捕获任何导致的异常并将其写入 SelfLog(参见调试和诊断)。

// X: does not exist, but this is a runtime condition
// so Serilog will not fail here.
Log.Logger = new LoggerConfiguration()
    .WriteTo.File("X:\\log.txt")
    .CreateLogger();

这种策略可以防止部署环境中的暂时性问题导致原本有效的应用程序失败。

开发时不变性违规

在配置时,对于永远无法有效执行的代码(例如,违反 API 不变性的情况)会做出一定的容忍:

// Null is never acceptable as a filename, so
// Seriog will throw ArgumentNullException here.
Log.Logger = new LoggerConfiguration()
    .WriteTo.File(null)
    .CreateLogger();

这一决定基于两个原因:

这些错误不太可能通过开发者的工作站或测试环境,因为日志记录配置发生在启动时,并且应该总是以相同的方式失败。

允许无效值或静默忽略它们会导致意外行为,这很难调试,并使库的正确配置更加困难。

如果您愿意,可以将日志记录配置代码包装在 try/catch 结构中,以避免异常传播,但不推荐这样做。

接收器作者: 实现这一点的责任在于接收器的实现本身,因此需要明确考虑/处理。

2、写入日志事件

事件分阶段写入日志记录管道。首先调用记录器,然后构造事件,接下来对其进行丰富,应用过滤器,最后将其传递(“发出”)到配置的接收器。

调用记录器

ILogger 和静态 Log 类上的方法会静默忽略无效参数:

// Safely does nothing
Log.Warning(null);

这样做是因为在执行频率较低的代码路径中的日志记录语句可能不会被测试,因此不应在运行时失败。

构造日志事件

当构造日志事件时,Serilog 可能会反射任何解构对象的属性。

如果这些属性抛出异常,Serilog 会捕获错误,写入 SelfLog,并在解构对象中包含错误信息而不是属性值。

关于类型加载的说明

如果一个应用程序在没有所需依赖项的情况下部署,加载器可能会无法找到/构造有效类型。这是一个主要的应用程序配置错误,可能在解构过程中或稍后,例如在 JIT 阶段显现出来。这种情况非常罕见。在这种情况下,Serilog 不会做任何事情,允许错误传播。

装饰器

装饰器向日志事件添加属性。装饰器可能会抛出异常:Serilog 会捕获这些异常并写入 SelfLog。

装饰器作者: Serilog 自身实现了这一策略,装饰器在意外失败时应抛出异常(尽管出于性能考虑,避免这种情况是明智的)。

过滤器

过滤器决定哪些事件会被传递到日志记录管道中。过滤器不会抛出异常,而是写入 SelfLog:

// No events will be carried through this pipeline, but no
// errors will be thrown.
Log.Logger = new LoggerConfiguration()
    .Filter.ByExcluding(e => { throw new Exception(); })
    .WriteTo.ColoredConsole()
    .CreateLogger();

过滤器作者: 如果过滤器意外抛出异常,这被视为一个错误——这对性能和功能的影响是显著的。

发出到接收器

Serilog 捕获并将接收器引发的任何异常写入 SelfLog。通常情况下,这些异常是正常使用 Serilog 时发生的绝大多数异常。

接收器作者: 接收器在失败时应抛出异常。Serilog 将一致地捕获和处理这些异常。

关于不可捕获的异常的说明

需要注意的是,仍然存在一类不可捕获的异常,Serilog 被迫传播这些异常,例如 StackOverflowException 等。

3、异步/批处理网络操作

许多 Serilog 接收器使用相同的基础 PeriodicBatchingSink 架构。这些接收器(例如批处理的 Azure 表存储接收器、CouchDB 接收器、RavenDB 接收器和 Seq 接收器(在非持久模式下))会缓存日志事件,从而减少将日志数据传输到远程主机所需的网络往返次数。

Log.Logger = new LoggerConfiguration()
    .WriteTo.CouchDB("api/missing")
    .CreateLogger()

这些接收器在写入事件时不会失败,但可能会在后台异步发送批次时失败。当批次发送失败时,详细信息将写入 SelfLog。

正在发送的批次将保留在内存中,并会以逐渐增加的时间间隔重试,时间间隔从 5 秒逐步增加到 10 分钟。增加的时间间隔可以保护接收器,在经历一段停机后重新上线时,避免连接洪水。

如果经过 4 次尝试仍无法发送批次,该批次将被丢弃并尝试新的批次。这可以防止接收器拒绝的“坏”事件堵塞日志记录器。后续的成功将允许其他批次正常传输。

如果再有两次尝试失败(总共 6 次失败,通常在 10 分钟左右),等待的日志事件的整个缓冲区将被丢弃。这可以防止在日志事件长时间无法送达时出现内存不足错误。

如果连接仍然中断,缓冲区将每 10 分钟刷新一次,直到重新建立连接。

接收器作者: 通过从 PeriodicBatchingSink 派生,可以默认提供此行为。如果需要不同的行为,则需要实现自定义的 ILogEventSink。

:相关源码都已经上传至代码库,有兴趣的可以看看。https://gitee.com/hugogoos/Planner

posted @ 2024-10-11 00:17  IT规划师  阅读(255)  评论(0编辑  收藏  举报