我该如何在应用程序中设计日志系统架构?

6

我对此进行了大量的研究,但没有找到我需要的答案。我希望 StackOverflow 的聪明人能够帮助我。

我在不同的情况下遇到了这个问题。比如我有一个 C# 应用程序,我想记录重要的事情。

public class MyClass
{
    ... 

    public void ImportantMethod()
    {
        DoInterestingThing();

        var result = SomethingElseImportant();
        if (result == null)
        {
            logger.Log("I wasn't expecting that. No biggie.");
            return;
        }

        MoreInterestingStuff(); 
}

我感兴趣的是,我应该从哪里获取logger
我有几个选择:
1. 在构造函数中将其注入到MyClass中。 2. 使用全局可用的服务定位器检索它。 3. 使用方法装饰器和AOP让日志记录自动完成。
这些选项都不是很好。#3似乎不可能,因为我正在记录我的业务逻辑,而不仅仅是跟踪我的方法调用、输入参数和/或抛出的异常。#2虽然简单,但似乎很难进行单元测试。当然,我希望对所有内容进行单元测试。#1虽然可以正常工作,但会在我的业务逻辑中添加日志对象,与业务对象本身无关
还有其他想法,或者对上述选项有什么看法吗?非常感谢!
编辑:为了明确起见,我已经知道如何进行DI(我使用Unity),并且我已经知道一个好的日志记录框架(我使用log4net)。只是想知道如何在整个应用程序中以最智能的方式使用日志记录。

* 编辑 *

我将Mark Seeman的答案标记为解决方案。我检查了我的应用程序,发现大多数日志记录调用都可以使用装饰器完成相同的工作。即记录方法的输入、抛出的任何异常和退出返回值。

有些情况下我仍然需要直接在方法内部记录日志。一个例子是我想在不返回任何内容但不抛出异常的方法中快速失败。在这些情况下,我有一个单例,它持有一个LogProvider的引用,后者将检索一个命名的日志实例。代码看起来类似于这样:

private ILog logger = LogProviderFactory.Instance.GetLogger(typeof(Foo));

LogProviderFactory有一个名为SetProvider的方法,允许您替换单例。因此,在单元测试中,我可以执行以下操作:

// LogProviderFactory.Instance now is our mock
LogProviderFactory.SetProvider(MockLogProvider);

日志装饰器使用与单例相同的LogProvider(通过注入获得),因此整个系统中的日志记录是统一的。

所以最终解决方案主要是选项#3,以及混合使用选项#2(它是服务定位器模式,但服务被“注入”到定位器中)。

AOP

就“面向方面编程”而言,我对语言的限制感到有些失望。希望在未来的版本中AOP将被视为一级公民。

  • 我尝试过PostSharp,但无法在我的计算机上正确运行它。此外,一个很大的限制是你必须在系统上安装PostSharp才能使用它(而不是只调用随解决方案一起提供的dll或类似的东西)。
  • 我使用了LinFu,并且能够部分地使其工作。然而,在某些情况下它会崩溃。新的2.0版本几乎没有文档,这是一个障碍。
  • 然而,使用Unity进行接口拦截似乎可以直接开箱即用。我很幸运,因为我想要记录的大多数内容都在实现接口的类中。

1
始终仔细地查看是否需要记录某些内容,或者通过抛出异常来快速失败。快速退出通常比您预期的要更有效。 - Steven
好观点,史蒂文!在我最近的应用程序中,我需要添加一些日志记录,这些记录不一定与异常/错误相关。例如“收到新消息”或“添加用户”等内容。并非调试语句,而是关于应用程序执行情况的信息记录。 - JonH
3个回答

5

2
@Onisemus:这就是拦截的作用。 - Steven
2
+1 @Onisemus:我不相信你已经仔细阅读了链接的文章,建议重新阅读以获取拦截点。 - Ruben Bartelink
1
如果您需要在方法中间记录日志,则该方法执行了太多的操作 :) - Mark Seemann
2
如果有必要,注入日志依赖项是唯一的方法,但我认为很少有情况需要从方法内部记录日志。不过,永远不要说永远 :) - Mark Seemann
在方法内部记录日志是完全合理的,这取决于情景和原因。 - Adrian K
显示剩余3条评论

2

两个要点:

(1) - 预构建的日志框架。

有些人喜欢Log4Net,但我是EntLibs的粉丝。这实际上完成了记录日志的重活,像EntLibs这样的工具可以让您记录到不同类型的日志存储库(数据库、消息队列、滚动文本文件等)。它们还可以根据类别等不同实例进行记录。通常它们是高度可配置的。

(2) - 包装日志框架的自定义类。

所以,“logger”是您编写的内容,它调用日志框架来实现实际的记录日志。

我喜欢这种方法,有以下几个原因:

  • 通过将自定义包装器放入单独的程序集中,您可以将日志框架(#1)与应用程序的其余部分解耦。
  • 通过编写自己的Logging API,您可以定义适合自己需求的方法签名,并在其上进行扩展。
  • 如果您正在与团队合作,您可以使方法签名非常易于使用,以便没有人有理由说使用日志太难。
  • 它保持了日志的一致性。它还使得查找“非法”代码(写入文件、控制台或事件日志的代码)变得容易,因为您的记录中不会有这些代码(它们全部在框架中)。
  • 通过为每个层编写特定的自定义类,您可以预填充许多数据,使实际应用程序代码的编写者更轻松。您可以设置严重性、优先级、默认事件ID、类别等。
  • 它在应用程序复杂性和增长方面具有很好的可扩展性;对于较小的应用程序来说,它可能看起来过于笨重,但如果随着时间的推移而增长,您将有足够的余地。

以下是我曾经参与的一个项目中信息记录类的示例。它有一堆易于调用的公共方法和一个调用框架的私有方法(ConcreteLogInformation)。

public static void LogInformation(string title, string message)

public static void LogInformation(string title, Dictionary<string, object> extendedProperties)

public static void LogInformation(string title, int eventId, Dictionary<string, object> extendedProperties)

public static void LogInformation(string title, string message, Dictionary<string, object> extendedProperties)

public static void LogInformation(string title, string message, int eventId)

public static void LogInformation(string title, string message, int eventId, Dictionary<string, object> extendedProperties)

public static void LogInformation(string title, string message, int eventId, string category)

public static void LogInformation(string title, string message, int eventId, Dictionary<string, object> extendedProperties, string category)

private static void ConcreteLogInformation(string title, string message, int eventId, Dictionary<string, object> extendedProperties, string category)

2
我同意你所说的一切,除了最后一部分你使用静态方法。使用静态方法会使测试变得更加困难。更好的方法是在需要记录器的类型的构造函数中注入一个 ILogger 接口。对我而言行之有效的方法是在该接口上有一个单独的 Log(LogEntry) 方法,并且有一堆扩展方法(例如 Log(string)Log(Exception)),这些扩展方法调用 Log(LogEntry) 接口方法。(仍然 +1 为你的答案)。 - Steven
@Steven - 嗯,听起来很酷。我必须承认,我从未有动力使用 DI 进行日志记录(如果您的 DI 失败了 - 您如何记录日志?),但考虑这个想法仍然是一个好主意。 - Adrian K
“如果你的 DI 失败” 究竟是什么意思? - Steven
使用 DI - 依赖反转(框架/子系统)。假设您进行了新的部署,它基于配置,但配置是错误的 - 您的日志记录将无法正常工作,并可能使诊断问题更加困难。另一件事是,您可以将使用 DI 视为使整个日志记录系统更加复杂 - 就日志记录子系统而言,我认为 KISS(保持简单愚蠢)原则很好 - 假设保持简单=鲁棒性。 - Adrian K
谢谢您的回复。如果我理解正确,您建议在某个静态类中使用静态方法记录应用程序内的日志?这似乎与选项#2相同。两种方式都是以静态方式访问日志功能,这使得单元测试非常困难(或根据设置可能不可能进行)。您可以使用普通DI,但我已经在描述中解释了我的犹豫。 - JonH
显示剩余2条评论

1
Ninject Contetual Binding docs在工厂或工厂方法中使用请求上下文进行上下文绑定部分,我有一个例子,可以通过以下方式(使用Ninjectese)利用您的容器为您的类注入适当的记录器:
Bind<ILog>().ToMethod( context => LogFactory.CreateLog( context.Request.Target.Type ) );

对于跟踪类型的东西,Mark的拦截文章描述了最佳方法。

我可以再次请求您在不仅仅是丢弃它们而没有点赞之前深入阅读@Mark Seemann引用的文章吗?


抱歉,没看到@Mark Seemann文章的第一个链接。但是它似乎并没有解决我的问题。AOP /拦截对于跟踪非常有效,但是示例方法正在执行更多操作。我在我的业务逻辑中调用日志记录函数-我对发生在方法调用之前和之后的事情不仅感兴趣。Ninject文章与log4net的做法相同,也与选项#2相同。我喜欢它……但很难进行单元测试。使用服务定位器实现时,如何在我的单元测试中指定模拟记录器? - JonH
@Onisemus:不知道你在说什么 SL。如果你真的读了,你会看到有ILogger接口的构造函数注入,这是可测试/可模拟的。重点是你的DI工具可以处理松散耦合,而你只需要一个绑定来记录20个类。 - Ruben Bartelink
哦,我看过了。有两个例子,一个是构造函数注入,另一个是工厂方法。我指的是工厂方法。构造函数注入非常有道理,我明白为什么它很受欢迎。只是像日志记录这样的横切关注点会使域变得混乱。我不太愿意使用它——可能我需要将ILog注入到域中一半的类中。 - JonH
@Onisemus:在我之前的回答中,将属性注入与单个绑定结合使用可以解决这个问题。但主要观点是,如果你将代码正确地拆分开来,你的代码就不需要做太多的日志记录 - 你可以通过AOP和/或DI容器的迷你AOP功能注入对外部调用的跟踪。然后对于剩下的那些应该很少见的情况,让CR/R#生成构造函数,以显示这个领域类真的认为它有重要的日志记录内容(而不是你到处都会散布的内容)。 - Ruben Bartelink
这是一个很好的观点 - 让AOP设施处理跟踪,然后在绝对必要的时候注入日志记录。就像你所说的,如果在跟踪之外记录很重要,那么在那个点注入日志记录依赖似乎是合理的。对于那些罕见的情况,你如何看待注入记录器与通过某种静态方法(服务位置,某种混合方法)获取记录器?静态方法可能有助于清理域...但另一方面,也许你可以考虑将日志记录作为一个特定的依赖项来进行注入。(并使该依赖项明显)。 - JonH
1
@Onisemus:我会把层次结构放在构造函数注入、属性注入、服务定位器、单例模式、其他静态方法/全局变量和全局变量中。但是构造函数注入要好得多。一旦您正确使用 DI 容器(不是 SL!),单例/静态的感知优势很快就会消失。但是,说了这么多,最好还是节省时间,选择构造函数注入,并寻找代表缺少抽象的依赖项群集。 - Ruben Bartelink

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