记录器包装最佳实践


91

我想在应用程序中使用nlogger,也许将来我将需要更改日志记录系统。所以我想使用伐木外观。

您是否知道现有示例的任何建议,如何编写这些建议?或者只是给我链接到该领域的一些最佳实践。




Answers:


207

我曾经使用诸如Common.Logging之类的日志记录外观(甚至隐藏了我自己的CuttingEdge.Logging库),但是如今,我使用了Dependency Injection模式,这使我可以将记录器隐藏在我自己的(简单的)抽象背后,该抽象遵循这两个Dependency反转原理接口隔离原理(ISP),因为它只有一个成员并且接口是由我的应用程序定义的;不是外部库。使您的应用程序核心部分对外部库的存在的了解降至最低;即使您无意替换日志记录库。对外部库的严格依赖使测试代码变得更加困难,并且使应用程序复杂化,而该API从未专门为您的应用程序设计。

这是我的应用程序中通常看起来像的样子:

public interface ILogger
{
    void Log(LogEntry entry);
}

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

// Immutable DTO that contains the log information.
public class LogEntry 
{
    public readonly LoggingEventType Severity;
    public readonly string Message;
    public readonly Exception Exception;

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

        this.Severity = severity;
        this.Message = message;
        this.Exception = exception;
    }
}

可选地,可以使用一些简单的扩展方法来扩展此抽象(允许接口保持狭窄并继续遵守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 exception) {
        logger.Log(new LogEntry(LoggingEventType.Error, exception.Message, exception));
    }

    // More methods here.
}

由于该接口仅包含一个方法,因此您可以轻松创建一个ILogger实现来替代log4netSerilogMicrosoft.Extensions.Logging,NLog或任何其他日志记录库的实现,并将您的DI容器配置为将其注入到它们的类ILogger中构造函数。

请注意,使用单个方法在接口顶部使用静态扩展方法与使用具有多个成员的接口完全不同。扩展方法只是创建LogEntry消息并通过ILogger接口上唯一方法传递消息的助手方法。扩展方法成为使用者代码的一部分;不是抽象的一部分。这不仅使扩展方法得以发展而无需更改抽象,扩展方法和功能。LogEntry构造函数总是在使用记录器抽象时执行,即使该记录器是存根/模拟的。在测试套件中运行时,这可以更加确定调用记录器的正确性。一员界面也使测试变得更加容易。由于具有许多成员的抽象,因此很难创建实现(例如,模拟,适配器和装饰器)。

当您执行此操作时,几乎不需要日志外观(或任何其他库)可能提供的某些静态抽象。


4
@GabrielEspinoza:这完全取决于将扩展方法放在其中的命名空间。如果yu将其放置在与接口相同的命名空间中,或者放置在项目的根命名空间中,则问题将不存在。
2014年

2
@ user1829319这只是一个例子。我确定您可以根据此答案提出适合您特定需求的实现。
史蒂文

2
我仍然不明白...将5个Logger方法作为ILogger的扩展而不是ILogger的成员有何优势?
伊丽莎白2015年

3
@Elisabeth的好处是,只需实现一个函数:“ ILogger :: Log”,就可以使Facade接口适应任何日志框架。扩展方法确保无论您决定使用哪种框架,我们都可以访问“便捷” API(例如“ LogError”,“ LogWarning”等)。尽管使用C#接口,但这是添加通用“基类”功能的一种a回方式。
BTownTKD

2
我需要再次强调一下这很棒的原因。将您的代码从DotNetFramework上转换为DotNetCore。在执行此操作的项目中,我只需要编写一个新的混凝土即可。我没有的地方...... gaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!我很高兴我发现了这个“回头路”。
granadaCoder


8

到目前为止,最好的选择是使用Microsoft.Extensions.Logging包(由Julian指出)。大多数日志记录框架都可以与此一起使用。

正如史蒂文的答案中所解释的那样,定义您自己的界面对于简单的情况是可以的,但它遗漏了一些我认为很重要的事情:

  • 结构化的日志记录和解构对象(Serilog和NLog中的@表示法)
  • 延迟的字符串构造/格式化:因为它需要一个字符串,所以它必须在调用时评估/格式化所有内容,即使最后由于该事件低于阈值也不会记录该事件(性能成本,请参阅上一点)
  • IsEnabled(LogLevel)出于性能原因,可能需要进行条件检查(如您所愿)

您可能可以在自己的抽象中实现所有这些功能,但是到那时,您将重新发明轮子。


4

通常我更喜欢创建一个像

public interface ILogger
{
 void LogInformation(string msg);
 void LogError(string error);
}

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


11
并且不要忘记LogWarningLogCritical方法及其所有重载。这样做会违反接口隔离原则。首选ILogger使用单个Log方法定义接口。
2013年

2
我真的很抱歉,这不是我的意图。无需羞愧。我之所以经常看到这种设计,是因为许多开发人员都使用流行的log4net(使用这种确切的设计)作为示例。不幸的是,这种设计并不是很好。
史蒂文

2
我更喜欢@Steven的答案。他引入了对的依赖LogEntry,从而导致了对的依赖LoggingEventType。但是,ILogger实现LoggingEventTypes可能必须处理这些case/switch,这是代码的味道。为什么要隐藏LoggingEventTypes依赖项?实现必须无论如何都要处理日志记录级别,因此最好明确实现应该做什么,而不是将其隐藏在带有通用参数的单个方法之后。
DharmaTurtle

1
举一个极端的例子,想象一下一个ICommand带有一个的Handle,其中带有一个objectcase/switch为了满足接口的约定,实现必须在可能的类型上进行。这不理想。没有隐藏任何必须处理的依赖的抽象。取而代之的是一个接口,该接口清楚地说明了预期的情况:“我希望所有记录器都处理警告,错误,致命事件等”。这比“我希望所有记录器都处理包括警告,错误,致命消息等的消息”更为可取。
DharmaTurtle

我有点同意@Steven和@DharmaTurtle。除类型外,LoggingEventType应将LoggingEventLevel类型称为类,并应在OOP中对其进行编码。对我来说,不使用接口方法与不使用相应enum值之间没有区别。而是使用ErrorLoggger : ILoggerInformationLogger : ILogger每个记录器都定义自己的级别。然后,DI需要通过按键(枚举)注入所需的记录器,但是该按键不再是界面的一部分。(您现在是SOLID)。
Wouter

4

LibLog项目的形式已经找到了解决这个问题的好方法。

LibLog是一个日志记录抽象,内置支持主要记录器,包括Serilog,NLog,Log4net和Enterprise记录器。它通过NuGet程序包管理器作为源(.cs)文件(而不是.dll引用)安装到目标库中。该方法允许包括日志记录抽象,而无需强制库承担外部依赖关系。它还允许库作者包括日志记录,而无需强制使用方应用程序显式提供库的记录器。LibLog使用反射来找出正在使用的具体记录器,并将其连接起来,而无需在库项目中使用任何明确的接线代码。

因此,LibLog是在库项目中进行日志记录的绝佳解决方案。只需在您的主应用程序或服务中引用并配置一个具体的记录器(Serilog即可),然后将LibLog添加到您的库中即可!


我用它来解决log4net重大更改问题(讨厌)(wiktorzychla.com/2012/03/pathetic-breaking-change-between.html)如果您是从nuget那里获取的,它实际上会创建一个.cs文件在您的代码中,而不是添加对预编译dll的引用。.cs文件已命名为您的项目的名称空间。因此,如果您具有不同的层(csprojs),则将具有多个版本,或者需要合并为共享的csproj。当您尝试使用它时,您会发现这一点。但是就像我说的那样,这是log4net突破性更改问题的救星。
granadaCoder


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.