77

我有兴趣了解更多关于人们如何使用依赖注入平台注入日志的信息。尽管下面的链接和我的示例引用了 log4net 和 Unity,但我不一定会使用其中任何一个。对于依赖注入/IOC,我可能会使用 MEF,因为这是项目的其余部分(大型)正在制定的标准。

我对依赖注入/ioc 很陌生,对 C# 和 .NET 也很陌生(在过去 10 年左右的 VC6 和 VB6 之后,我在 C#/.NET 中编写的生产代码很少)。我对现有的各种日志记录解决方案进行了大量调查,因此我认为我对它们的功能集有一个不错的处理。我只是对注入一个依赖项的实际机制不够熟悉(或者,也许更“正确”,注入一个依赖项的抽象版本)。

我看过其他与日志和/或依赖注入相关的帖子,例如: 依赖注入和日志接口

记录最佳实践

Log4Net Wrapper 类会是什么样子?

再次关于 log4net 和 Unity IOC 配置

我的问题与“如何使用 ioc 工具 yyy 注入日志记录平台 xxx?”没有特别的关系。相反,我对人们如何处理日志记录平台(通常但并不总是推荐)和配置(即 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,我可能想实现一个基于接口的记录器(甚至可能使用“通用”ILogger/ILog 接口),可能基于 TraceSource,以便我可以注入它。您是否会实现接口,比如通过 TraceSource,并按原样使用 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!")

转到第 2 点...如何解析特定的命名记录器实例?我是否应该只利用我选择的日志记录平台的 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 实例,并按名称将它们添加到(a?)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,因为它似乎不是一个有明确答案的问题。如果有人觉得不一样,请随时更改。

谢谢你的帮助!

4

5 回答 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 容器。

于 2011-06-26T22:19:13.100 回答
16

也可以使用Common.Logging Facade 或Simple Logging Facade

这两个都使用服务定位器样式模式来检索 ILogger。

坦率地说,日志记录是我认为自动注入几乎没有价值的依赖项之一。

我的大多数需要日志服务的类如下所示:

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

}

通过使用简单的日志记录外观,我将一个项目从 log4net 切换到 NLog,并且除了我的应用程序使用 NLog 的日志记录之外,我还添加了来自使用 log4net 的第三方库的日志记录。也就是说,门面很好地为我们服务。

一个难以避免的警告是丢失了特定于一个日志框架或另一个的功能,其中最常见的例子可能是自定义日志级别。

于 2010-08-19T20:07:25.777 回答
13

当您要注入的记录器提供了诸如 log4net 或 NLog 之类的记录平台时,这对任何试图弄清楚如何注入记录器依赖项的人都是有益的。我的问题是,当我知道特定 ILogger 的分辨率将取决于知道依赖于 ILogger 的类的类型时,我无法理解如何使类(例如 MyClass)依赖于 ILogger 类型的接口(例如我的班级)。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% 确定 log4net 和 NLog 的语法,但你明白了)。

虽然 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 如何或是否可以实现这样的事情。我可以让一个对象依赖于一个接口,然后在 MEF 创建该对象之后并在解决接口/依赖关系时执行我的代码吗?所以,假设我有这样的课程:

public class MyClass
{
  [Import(ILogger)]
  public ILogger logger;

  public MyClass()
  {
  }

  public void DoSomething()
  {
    logger.Info("Hello World!");
  }
}

当 MEF 解析 MyClass 的导入时,我可以有一些我自己的代码(通过属性,通过 ILogger 实现的额外接口,其他地方???)执行和解析 ILogger 导入基于它是当前处于上下文中的 MyClass 并返回一个(可能)与为 YourClass 检索到的 ILogger 实例不同的实例?我是否实施某种 MEF 提供程序?

在这一点上,我仍然不了解MEF。

于 2010-08-19T19:25:16.887 回答
5

我看到你想出了你自己的答案 :) 但是,对于未来有关于如何不将自己绑定到特定日志记录框架的问题的人来说,这个库:Common.Logging有助于解决这种情况。

于 2010-08-19T19:38:37.810 回答
0

我通过提供者制作了我的自定义ServiceExportProvider,我注册了 Log4Net 记录器以供 MEF 进行依赖注入。因此,您可以将记录器用于不同类型的注入。

注射示例:

[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));
        }
    }
}
于 2016-11-21T10:58:37.020 回答