依赖注入和命名日志记录器

79

我对学习如何在依赖注入平台中注入日志感兴趣。虽然下面的链接和我的示例是关于log4net和Unity的,但我不一定会使用它们。对于依赖注入/IOC,我可能会使用MEF,因为这是项目(大型)正在采用的标准。

我非常新于依赖注入/IOC,并且对C#和.NET也非常陌生(过去10年左右都是VC6和VB6)。我已经调查了许多logging解决方案,所以我认为我对它们的特性有一定的了解。我只是不太熟悉获取一个依赖项注入(或者更正确地说,获取抽象版本的依赖项注入)的实际机制。

我看到了其他与logging和/或依赖注入相关的帖子,比如:dependency injection and logging interfaces

Logging best practices

What would a Log4Net Wrapper class look like?

again about log4net and Unity IOC config

我的问题与“如何使用IOC工具注入日志平台xxx?”无关。相反,我对人们如何处理包装logging平台(通常但并不总是建议)和配置(即app.config)感兴趣。例如,以log4net为例,我可以在app.config中配置多个记录器,然后以标准方式获取这些记录器(而无需依赖注入),方法如下:

private static readonly ILog logger = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

或者,如果我的记录器不是针对一个类命名的,而是针对一个功能区域命名的,我可以这样做:

private static readonly ILog logger = LogManager.GetLogger("Login");
private static readonly ILog logger = LogManager.GetLogger("Query");
private static readonly ILog logger = LogManager.GetLogger("Report");

所以,我猜我的“要求”大概是这样的:

  1. 我希望我的产品源码不直接依赖于日志平台。

  2. 我希望能够通过某种形式的依赖注入(可能是MEF),直接或间接地解析特定命名的记录器实例(可能在所有请求者之间共享相同的实例)。

  3. 我不知道是否应该把这个作为强制要求,但我希望能够根据需要获得一个指定名称的记录器(而不是类记录器)。例如,我可以基于类名为我的类创建一个记录器,但其中一个方法需要特别重的诊断,我希望单独控制。换句话说,我可能希望一个类“依赖”于两个独立的记录器实例。

让我们从第1点开始。我阅读了一些文章,主要在stackoverflow上,关于是否将日志平台包装起来。请参见上面的“最佳实践”链接,并转到jeffrey hantin的评论,了解他对为什么包装log4net是不好的看法。如果您确实包装了(而且如果您能够有效地包装),您会严格为注入/删除直接依赖而包装吗?或者您还会尝试抽象出一些或全部的log4net app.config信息?

假设我想使用System.Diagnostics,我可能希望基于TraceSource实现一个基于接口的记录器(甚至可以使用“通用”的ILogger / ILog接口),以便进行注入。你会实现这个接口,并只使用System.Diagnostics app.config信息吗?

类似于这样:

public class MyLogger : ILogger
{
  private TraceSource ts;
  public MyLogger(string name)
  {
    ts = new TraceSource(name);
  }

  public void ILogger.Log(string msg)
  {
    ts.TraceEvent(msg);
  }
}

使用方法如下:

private static readonly ILogger logger = new MyLogger("stackoverflow");
logger.Info("Hello world!")

接下来是第二个问题...如何解决特定的命名记录器实例?我应该只使用我选择的日志平台的app.config信息(即根据app.config中的命名方案解析记录器)吗?那么,在log4net的情况下,我是否可以优先“注入”LogManager(请注意,我知道这不可能,因为它是一个静态对象)?我可以封装LogManager(称之为MyLogManager),给它一个ILogManager接口,然后解析MyLogManager.ILogManager接口。我的其他对象可能依赖于ILogManager(在MEF术语中导入),在实现它的程序集中导出。现在我可以拥有这样的对象:

public class MyClass
{
  private ILogger logger;
  public MyClass([Import(typeof(ILogManager))] logManager)
  {
    logger = logManager.GetLogger("MyClass");
  }
}
任何时候调用ILogManager,它都直接委托给log4net的LogManager。或者,包装后的LogManager是否可以按名称将其基于app.config收到的ILogger实例添加到MEF容器中呢?稍后,当请求相同名称的记录器时,将查询封装的LogManager以获取该名称。如果有ILogger,则以这种方式解析。如果使用MEF可能做到这一点,那么这样做是否有任何好处呢?
在这种情况下,真正注入的只是ILogManager,并且它可以以log4net通常的方式分发ILogger实例。这种类型的注入(本质上是工厂)与注入命名记录器实例相比如何?这确实可以更轻松地利用log4net(或其他日志记录平台)的app.config文件。
我知道可以这样从MEF容器中获取命名实例:
var container = new CompositionContainer(<catalogs and other stuff>);
ILogger logger = container.GetExportedValue<ILogger>("ThisLogger");

如何将命名实例放入容器中?我知道属性模型可以使用不同的ILogger实现,每个都有一个名称(通过MEF属性指定),但这并没有真正帮助我。是否有一种方法可以创建类似于app.config(或其中的部分)的东西,以列出具有相同实现的记录器,并且MEF可以读取它们的名称?应该有一个集中的“管理器”(例如MyLogManager),它通过底层的app.config解决了命名的日志记录器,然后将解析的记录器插入MEF容器中吗? 这样,它将对拥有访问同一MEF容器的其他人可用(尽管没有MyLogManager如何使用log4net的app.config信息的知识,似乎容器将无法直接解析任何命名的记录器)。

这已经变得相当长了。我希望它是连贯的。请随时分享有关如何将日志平台(我们最有可能考虑log4net、NLog或基于System.Diagnostics的一些东西(希望很薄))注入到应用程序中的任何特定信息。

您是否注入了“管理器”并使其返回记录器实例?

您是否在自己的配置部分或DI平台的配置部分中添加了一些自己的配置信息,以使直接注入记录器实例变得更加容易/可能(即使你的依赖关系是ILogger而不是ILogManager)。

那么,有一个静态或全局容器,其中包含ILogManager接口或其中的一组命名ILogger实例。因此,与传统意义上的注入不同(通过构造函数、属性或成员数据),日志记录依赖性是明确解决的。这是一个好的还是坏的依赖注入方式。

我将其标记为社区wiki,因为它似乎不是一个具有明确答案的问题。如果有人有不同看法,请随时更改。

谢谢任何帮助!

5个回答

39

我正在使用 Ninject 来解析当前类名称以便用于日志记录实例,代码如下:

kernel.Bind<ILogger>().To<NLogLogger>()
  .WithConstructorArgument("currentClassName", x => x.Request.ParentContext.Request.Service.FullName);

NLog 实现的构造函数可能如下所示:

public NLogLogger(string currentClassName)
{
  _logger = LogManager.GetLogger(currentClassName);
}

我想这种方法对其他IOC容器也适用。


6
我使用了 x.Request.ParentContext.Plan.Type.FullName 来获取具体的类名,而不是接口名。 - Chadwick
我总是得到空的ParentContext。我有一个继承自Loggerbase的NLogLogger,它实现了ILogger。 - Marshal
如果基类在不同的项目中,则确保再次初始化记录器,如下所示: [assembly: log4net.Config.XmlConfigurator(Watch = true)] - Orhan

17

还可以使用Common.Logging门面或Simple Logging Facade

这两种方式都采用了服务定位器模式来检索ILogger。

坦白地说,日志记录是那些自动注入中我看不到任何价值的依赖之一。

需要记录日志的大多数类看起来都像这样:

public class MyClassThatLogs {
    private readonly ILogger log = Slf.LoggerService.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.FullName);

}

通过使用简单日志门面(Simple Logging Facade),我将一个项目从log4net切换到了NLog,并且我还使用NLog添加了第三方库的日志记录,该库之前使用的是log4net。也就是说,这个门面对我们非常有用。

但是,很难避免的一个注意点是丢失特定于某个日志框架的功能,其中最常见的例子可能是自定义日志级别。


谢谢你提供关于SLF和Common.Logging的提示。我最近实际上尝试过这些库,它们都非常酷。最终,我认为我们的项目可能不会使用DI来获取日志记录,所以我们可能会使用SLF或Common.Logging。 - wageoghe

13
这篇文章是为了帮助那些想要注入一个日志记录器依赖项,但又需要在提供日志平台的情况下进行注入(例如log4net或NLog)。 我的问题在于我无法理解如何使一个类(例如MyClass)依赖于ILogger类型的接口,当我知道特定ILogger的解析取决于依赖于ILogger的类的类型(例如MyClass)。 DI/IoC容器如何获取正确的ILogger呢?
我研究了Castle和NInject的源代码,并且还查看了AutoFac和StructureMap。Castle和NInject都提供了日志记录实现。两者都支持log4net和NLog。Castle还支持System.Diagnostics。 在这两种情况下,当平台为给定对象解析依赖项(例如当平台正在创建MyClass并且MyClass依赖于ILogger时),它会将依赖项(ILogger)的创建委托给ILogger“提供程序”("解析器"可能是更常见的术语)。然后,ILogger提供程序的实现负责实际实例化ILogger的实例并将其返回,以便注入到依赖类(例如MyClass)中。在这两种情况下,提供程序/解析器都知道依赖类(例如MyClass)的类型。 因此,在创建MyClass并正在解析其依赖关系时,ILogger“解析器”知道该类是MyClass。在使用Castle或NInject提供的日志记录解决方案时,这意味着日志记录解决方案(作为log4net或NLog的包装器实现)获取了类型(MyClass),因此它可以委托到log4net.LogManager.GetLogger()或NLog.LogManager.GetLogger()(语法不是100%确定,但你知道我的意思)。
虽然AutoFac和StructureMap不提供日志记录功能(至少我通过查看未发现),但它们似乎提供了实现自定义解析器的能力。因此,如果要编写自己的日志记录抽象层,则可以编写相应的自定义解析器。因此,当容器想要解析ILogger时,将使用您的解析器来获取正确的ILogger,并且它将访问当前上下文(即当前正在满足哪个对象的依赖关系-哪个对象依赖于ILogger)。获得对象的类型,然后准备好将ILogger的创建委托给当前配置的日志记录平台(您可能已经在接口后面封装了一个界面并编写了解析器)。
因此,我怀疑需要的一些关键点是:
1.最终DI容器必须以某种方式知道要使用什么日志记录平台。通常,这是通过指定“ILogger”由特定于日志记录平台的“解析器”(因此,Castle具有log4net、NLog和System.Diagnostics“解析器”(其中之一))来解决的。可以通过配置文件或编程方式指定要使用哪个解析器。
2.解析器需要了解解析依赖关系(ILogger)的上下文。也就是说,如果已创建MyClass并且它依赖于ILogger,则当解析器尝试创建正确的ILogger时(解析器),它必须知道当前类型(MyClass)。这样,解析器就可以使用底层日志记录实现(log4net、NLog等)来获取正确的记录器。
这些点对于那些DI/IoC用户来说可能很明显,但我现在才开始接触它,所以我花了一段时间才理解它。
我还没有弄清楚像MEF这样的东
public class MyClass
{
  [Import(ILogger)]
  public ILogger logger;

  public MyClass()
  {
  }

  public void DoSomething()
  {
    logger.Info("Hello World!");
  }
}
当MEF正在解决MyClass的导入时,我能否通过一个属性、ILogger实现的附加接口或其他方式来执行一些自己的代码,并根据当前上下文是MyClass来解析ILogger导入并返回潜在不同于YourClass检索到的ILogger实例?我需要实现一种MEF提供程序吗?
此时,我仍然不知道MEF是什么。

看起来MEF和Unity(MS模式与实践组中的IoC)就像是在比较苹果和橙子。也许这个特定问题需要一个真正的IoC容器,它具有可扩展性点以添加自定义依赖关系解析。https://dev59.com/pXVC5IYBdhLWcg3wcgqd - Norman H
哇... 有些人确实把简单的事情搞得比必要的复杂。当你只是想向文件写入一些文本时,我想知道所有这些是否真的有用... - Ed S.

5

我看到你已经找到了答案 :) 但是,对于将来有这个问题的人,想要避免与特定的日志框架绑定,这个库:Common.Logging 可以帮助解决这个问题。


和qstarin一样的评论。感谢您指向Common.Logging。 - wageoghe

0

我创建了自定义的ServiceExportProvider,通过该提供程序,我使用MEF注册Log4Net记录器以进行依赖注入。结果是您可以将记录器用于不同类型的注入。

注入示例:

[Export]
public class Part
{
    [ImportingConstructor]
    public Part(ILog log)
    {
        Log = log;
    }

    public ILog Log { get; }
}

[Export(typeof(AnotherPart))]
public class AnotherPart
{
    [Import]
    public ILog Log { get; set; }
}

使用示例:

class Program
{
    static CompositionContainer CreateContainer()
    {
        var logFactoryProvider = new ServiceExportProvider<ILog>(LogManager.GetLogger);
        var catalog = new AssemblyCatalog(typeof(Program).Assembly);
        return new CompositionContainer(catalog, logFactoryProvider);
    }

    static void Main(string[] args)
    {
        log4net.Config.XmlConfigurator.Configure();
        var container = CreateContainer();
        var part = container.GetExport<Part>().Value;
        part.Log.Info("Hello, world! - 1");
        var anotherPart = container.GetExport<AnotherPart>().Value;
        anotherPart.Log.Fatal("Hello, world! - 2");
    }
}

控制台返回的结果:

2016-11-21 13:55:16,152 INFO  Log4Mef.Part - Hello, world! - 1
2016-11-21 13:55:16,572 FATAL Log4Mef.AnotherPart - Hello, world! - 2

ServiceExportProvider 实现:

public class ServiceExportProvider<TContract> : ExportProvider
{
    private readonly Func<string, TContract> _factoryMethod;

    public ServiceExportProvider(Func<string, TContract> factoryMethod)
    {
        _factoryMethod = factoryMethod;
    }

    protected override IEnumerable<Export> GetExportsCore(ImportDefinition definition, AtomicComposition atomicComposition)
    {
        var cb = definition as ContractBasedImportDefinition;
        if (cb?.RequiredTypeIdentity == typeof(TContract).FullName)
        {
            var ce = definition as ICompositionElement;
            var displayName = ce?.Origin?.DisplayName;
            yield return new Export(definition.ContractName, () => _factoryMethod(displayName));
        }
    }
}

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