日志包装器最佳实践

94

我希望在我的应用程序中使用nlogger,也许将来我需要更改日志记录系统。 因此,我想使用一个日志门面。

您知道有关如何编写这些内容的现有示例的任何建议吗? 或者只需给我提供一些最佳实践的链接。


(Note: I have retained the HTML tags as requested and provided a concise translation.)

4
已存在:http://netcommon.sourceforge.net/ - R. Martinho Fernandes
你有没有看过 Codeplex 上的简单日志门面项目(The Simple Logging Facade project on Codeplex)? - JefClaes
相关:https://dev59.com/8mkw5IYBdhLWcg3wbqGZ - Steven
7个回答

221
我曾经使用像Common.Logging这样的记录门面(甚至隐藏了我的CuttingEdge.Logging库),但现在我使用依赖注入模式。这使我能够将日志记录器隐藏在应用程序定义的抽象后面,该抽象符合依赖反转原则接口隔离原则(ISP),因为它只有一个成员,并且接口由我的应用程序定义;而不是外部库。
尽量减少核心部分对外部库存在的认知,即使您没有打算替换日志记录库。对外部库的硬依赖会使测试代码更加困难,并且会为您的应用程序增加一个从未专门为您的应用程序设计的API。
这就是我应用程序中抽象的常见形式:
public interface ILogger
{
    void Log(LogEntry entry);
}

public sealed class ConsoleLogger : ILogger
{
    public void Log(LogEntry entry)
}

public enum LoggingEventType { Debug, Information, Warning, Error, Fatal };

// Immutable DTO that contains the log information.
public struct LogEntry
{
    public LoggingEventType Severity { get; }
    public string Message { get; }
    public Exception Exception { get; }

    public LogEntry(LoggingEventType severity, string msg, Exception ex = null)
    {
        if (msg is null) throw new ArgumentNullException("msg");
        if (msg == string.Empty) throw new ArgumentException("empty", "msg");

        this.Severity = severity;
        this.Message = msg;
        this.Exception = ex;
    }
}

可选地,这种抽象可以通过一些简单的扩展方法进行扩展(允许接口保持窄并继续遵守ISP)。这使得此接口的消费者的代码更加简单:

public static class LoggerExtensions
{
    public static void Log(this ILogger logger, string message) =>
        logger.Log(new LogEntry(LoggingEventType.Information, message));

    public static void Log(this ILogger logger, Exception ex) =>
        logger.Log(new LogEntry(LoggingEventType.Error, ex.Message, ex));

    // More methods here.
}

因为接口只包含一个方法,所以很容易创建一个ILogger实现,代理到log4net到SerilogMicrosoft.Extensions.Logging,NLog或任何其他记录库,并配置您的DI容器将其注入到具有ILogger构造函数的类中。还可以轻松创建一个写入控制台的实现,或者用于单元测试的虚拟实现,如下面的清单所示:
public class ConsoleLogger : ILogger
{
    public void Log(LogEntry entry) => Console.WriteLine(
      $"[{entry.Severity}] {DateTime.Now} {entry.Message} {entry.Exception}");
}

public class FakeLogger : List<LogEntry>, ILogger
{
    public void Log(LogEntry entry) => this.Add(entry);
}

在具有单个方法的接口之上使用静态扩展方法与具有多个成员的接口完全不同。这些扩展方法只是帮助方法,它们创建一个LogEntry消息并将其通过ILogger接口上唯一的方法传递。这些扩展方法本身不包含任何自我Volatile Behavior,因此不会影响可测试性。如果您愿意,您可以轻松地对它们进行测试,并且它们成为使用者代码的一部分;而不是抽象的一部分。

这不仅允许扩展方法在不需要更改抽象的情况下发展,而且当存根/模式化记录器时,始终执行扩展方法和LogEntry构造函数。这在运行测试套件时对于调用记录器的正确性给予了更大的保障。我曾经多次以这种方式自掘坟墓,在我的单元测试中,对使用的第三方记录器抽象的调用成功了,但在生产中执行时仍然失败了。

单成员接口也使得测试更容易;拥有许多成员的抽象使得创建实现(如模拟、适配器和装饰器)变得困难。

这样做几乎没有任何需要使用日志门面(或任何其他库)提供的静态抽象的需要。

即使有了这个ILogger设计,仍然建议以这样的方式设计您的应用程序,只有少数类需要依赖于您的ILogger抽象。这个答案更详细地讨论了这个问题。


4
这完全取决于你放置扩展方法的命名空间。如果你将其放在与接口相同的命名空间中或项目的根命名空间中,问题就不会存在。 - Steven
2
@user1829319 这只是一个例子。我相信你可以根据这个答案提供一个适合你特定需求的实现。 - Steven
2
我还是不明白...将5个Logger方法作为ILogger的扩展而不是成员,有什么优势? - Elisabeth
3
好的,下面是翻译的结果:@Elisabeth 好处在于您可以通过实现一个单独的函数“ILogger::Log”来适应任何日志框架的外观界面。扩展方法确保我们可以访问“方便”的API(如“LogError”,“LogWarning”等),而不管您决定使用哪个框架。这是一种迂回的方式来添加共同的“基类”功能,尽管使用 C#接口。 - BTownTKD
2
我需要再次强调一个原因,为什么这很棒。将您的DotNetFramework代码升级到DotNetCore。在我完成这项工作的项目中,我只需要编写一个新的具体实现。而那些我没有升级的项目......噢呀呀呀呀呀呀呀呀!我很高兴我找到了这个“捷径”。 - granadaCoder
显示剩余20条评论

10
目前最好的选择是使用Microsoft.Extensions.Logging包(正如Julian所指出的)。 大多数日志框架都可以与此一起使用。
根据Steven的回答中所解释的,定义自己的接口对于简单情况来说是可以的,但它忽略了我认为重要的几个方面:
- 结构化日志和对象解构(Serilog和NLog中的@符号) - 延迟字符串构造/格式化:由于它需要一个字符串,在调用时必须评估/格式化所有内容,即使最终事件不会被记录因为它在阈值以下(性能成本,见上一点) - 条件检查,例如IsEnabled(LogLevel),出于性能原因您可能希望这样做
您可能可以在自己的抽象中实现所有这些,但此时您将会重新发明轮子。

10

11
NLog v4.0库中有一个ILogger接口。您不再需要使用此库。 - Jowen

5
一个很好的解决方案出现了,它以LibLog项目的形式呈现。
LibLog是一个日志抽象层,内置支持Serilog、NLog、Log4net和Enterprise logger等主要日志记录器。它通过NuGet包管理器安装到目标库中作为源(.cs)文件而不是.dll引用。这种方法允许在不强制库承担外部依赖的情况下包含日志抽象层。它还允许库作者包含日志记录,而不会强制使用应用程序向库提供记录器。LibLog使用反射来确定正在使用哪个具体的记录器,并连接到它而不需要在库项目中显式编写任何连线代码。
因此,LibLog是在库项目中记录日志的好方法。只需在您的主应用程序或服务中引用和配置一个具体的记录器(Serilog),并将LibLog添加到您的库中即可!

1
我曾使用这种方法来解决log4net的破坏性变更问题(呃)。 (https://www.wiktorzychla.com/2012/03/pathetic-breaking-change-between.html)如果你从NuGet获取,它实际上会在你的代码中创建一个.cs文件,而不是添加到预编译的dll引用。 .cs文件命名空间与您的项目相同。因此,如果您有不同的层(csprojs),则可能会有多个版本,或者您需要合并为共享的csproj。当您尝试使用它时,您会发现这一点。但就像我说的那样,这在解决log4net破坏性变化问题方面真的很有帮助。 - granadaCoder

4
通常我更喜欢创建像这样的接口:
public interface ILogger
{
 void LogInformation(string msg);
 void LogError(string error);
}

在运行时,我会注入一个从该接口实现的具体类。

2
我更喜欢这个答案,而不是@Steven的答案。他引入了对LogEntry的依赖,因此也依赖于LoggingEventTypeILogger实现必须处理这些LoggingEventTypes,可能通过case/switch,这是一种代码异味。为什么要隐藏LoggingEventTypes的依赖性?实现必须无论如何处理日志级别,因此最好明确说明实现应该做什么,而不是将其隐藏在具有通用参数的单个方法后面。 - DharmaTurtle
1
作为极端的例子,想象一下有一个 ICommand 接口,它有一个 Handle 方法,需要传入一个 object 对象。实现类必须通过 case/switch 来处理所有可能的类型,以满足接口的约定。这并不理想。不要隐藏那些必须被处理的依赖项。而是应该有一个明确表述期望的接口:“我希望所有记录器都能处理警告、错误、致命等级的日志信息”。与其期望所有的记录器都能够“包含”警告、错误、致命等级的日志信息,还不如声明期望的接口更为优选。 - DharmaTurtle
我有点同意@Steven和@DharmaTurtle的观点。除此之外,LoggingEventType 应该被称为 LoggingEventLevel,因为类型是类,在面向对象编程中应该这样编码。对我来说,不使用接口方法与不使用相应的 enum 值之间没有区别。相反,使用 ErrorLoggger : ILoggerInformationLogger : ILogger,其中每个记录器都定义了自己的级别。然后 DI 需要注入所需的记录器,可能通过一个键(枚举)进行,但是这个键不再是接口的一部分。(现在你遵循了 SOLID 原则)。 - Wouter
@Wouter 我不会否认SOLID的重要性,但在编程中并不存在普适真理,只有使用不同设计时需要权衡取舍。但是,在您的示例/设计中如何在运行时切换日志记录级别呢?这在例如Nlog、log4net等中非常常见。 - Jesper
@Jesper,loglevel是具体的ILogger类型,logger可以启用或禁用。日志记录器可以共享基于logLevelValue的启用或禁用配置。如果日志记录器共享一个公共配置模型,则可以根据名称和值启用或禁用日志记录器,类似于log4net。 - Wouter

3

2

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接