日志代码应该抛出异常吗?

Should logging code throw an Exception?

我正在设计一个小型库,它应该有助于登录到不同的目的地(文件、数据库等)。但是,我不确定如果出现问题(例如无法写入文件)是否应该抛出异常?

如果我抛出异常(与日志记录相关),使用我的库的应用程序开发人员可以清楚地看到日志记录没有按预期发生。另一方面,它可能是问题的另一个来源。

如果我抑制异常并且只是安静地不记录,开发人员可能会错过重要信息,例如无法访问数据库。

是否有关于如何处理此问题的任何建议和一般准则(如果 a 那么 b 等)?


据我目前所读:

Microsofts Guidelines for Exceptions 状态:

✗ DO NOT have public members that can either throw or not based on some option.

因此,是否抛出异常的选项将违反这些准则,但如果不确定异常的可能性有多大,它们不会给出关于抛出异常的声明。

我还在 this question/answer 中读到我不应该捕获异常,因为我不能对它们做任何有意义的事情,但是这个问题针对的是企业级系统,而我的问题是在更一般的层面上提出的。

有趣的是,就在本周,我向我的一个应用程序添加了一个新的 NLog 目标,但它没有记录,但也没有抱怨。所以令我震惊的是,NLog 团队针对失败的日志目标做出了这个决定。我可以告诉你,NLog 静默无法登录目标。

但是——不完全是。他们确实有一个内部记录器,您可以使用它来调试记录器:https://github.com/NLog/NLog/wiki/Internal-Logging 您可以这样配置:

<nlog internalLogFile="c:\log.txt" internalLogLevel="Trace">
   <targets>
      <!-- target configuration here -->
   </targets>
   <rules>
      <!-- log routing rules -->
   </rules>
</nlog>

由于 NLog(连同 Log4Net)可能是 .NET 的最大日志记录库,您可能对此感兴趣。

我看到的另一件事是一些库引用了 https://www.nuget.org/packages/Common.Logging/,我觉得它非常优雅。这是一个非常轻量级的包,带有一组可用于登录的接口。然后,调用程序集通过使用 NLog 或 Log4Net 的适配器来控制输出发生的情况,并继续使用预先存在的目标。

不要教条地遵循任何准则。查阅指南,然后按照您认为最适合解决手头问题的方式进行操作。

记录器是一种特殊的软件:它有特殊的需求和注意事项。因此,Microsoft 发布的准则不适用于您的记录器,就像它们适用于通用软件一样。

在我的书中,根据 "Is-This-A-Development-Environment" 标志改变记录器的行为是完全没问题的,如果 true 则抛出异常,或者抑制这些异常(可能将它们记录在一些更少的地方) - 容易出错的介质)否则。

如评论所述,这是一个设计决定,因此没有技术意义上正确的答案。

我通常是这样做的:

  • 如果出现问题,那么我会在第一次尝试时抛出异常。
  • 我在日志框架本身中捕获了这个异常并将其写入系统的事件日志(应该总是成功)。
  • 如果由于某种原因也失败了,我会默默地吞下错误。
  • 您可以选择在调试版本中重新抛出异常或将其写入调试输出。

我建议在很多情况下应忽略引用的 Microsoft 声明。我建议作为日志记录 class 的模式是有一个方法,其中 returns 一个 "log suppression" 令牌,然后抑制在日志记录期间发生的异常,但随后有一个方法它取消了日志抑制令牌的效果,并且根据传入的参数,returns 已被抑制的异常列表,或者当列表非空时,抛出包含的复合异常名单。我会进一步建议日志记录请求要求以后进先出的方式使用令牌,并且在取消早期创建的效果之前取消一个效果的失败应该被认为是一个例外使用错误。

尽管这违背了微软的建议,但基于参数的方法抛出或不抛出有两个优点,否则无法获得:

  1. 在许多情况下,支持 try/do 模式的操作会调用支持相同模式的方法。如果publictry/do方法链接到一个使用参数区分"try"和"do"的方法,并且要调用的嵌套方法也支持这样的参数,那么它是可以让 "try" 和 "do" 方法共享相同的实现。如果所有 public 方法都是 "try-only" 或 "do-only","do" 希望内部方法中的问题显示为它们抛出的异常,而 "try" 不会如果不想嵌套 "do" 方法抛出必须捕获的异常,那么即使调用者尝试在内部使用共享代码,他们最终也不得不:

    if (throwOnException)
    {
      thing.ReadData(whatever);
      success = true;
    }
    else
    {
      success = thing.TryReadData(whatever);
    }
    

    一个足够大的代码片段,它应该是它自己的方法,但它在语义上更属于 thing 而不是客户端。

  2. A try/finally 块应该只在 tryfinally 部分都成功时退出而不抛出异常。但是,如果在 try 中发生异常,传播该异常通常比让它被在 finally 中抛出的异常覆盖更好。通常,无论 try 块是否成功,finally 代码的行为都是相同的, 除了 try 时发生的异常成功的区块应该被允许传播,而当 try 区块失败时发生的那些应该被扼杀。

C# 和 VB 都没有提供让 finally 块知道 try 是否成功的好方法,但这是可能的。如果 finally 块有时会希望事物抛出异常,有时会扼杀它们,但在其他方面表现相同,那么为 select 问题报告行为设置一个参数将比必须使用单独的代码来处理待处理的问题更清晰-异常和无未决异常情况。