复合应用程序的最佳日志记录方法是什么?

16

我正在创建一个复合WPF(Prism)应用程序,其中包括多个不同的项目(Shell、模块等)。我准备使用Log4Net实现日志记录。看起来有两种设置日志的方式:

  • 让Shell项目进行所有实际的日志记录。它获取对Log4Net的引用,并且其他项目通过组合事件发出信号,让Shell知道需要记录某些东西。这些项目仅在Shell的app.config文件中打开了日志记录级别(DEBUG、ERROR等),以避免降低性能。

  • 给每个项目,包括模块,提供Log4Net引用,并让项目自己记录到一个共同的日志文件中,而不是向Shell发送消息进行日志记录。

哪种方法更好?或者,是否有其他方法我应该考虑?谢谢你的帮助。

4个回答

19

在Prism中最简单的记录日志的方式是覆盖Bootstrapper中的LoggerFacade属性。通过覆盖LoggerFacade,只要Logger实现了ILoggerFacade接口,就可以传入任何需要的Logger实例和配置信息。

我发现以下方法在记录日志方面效果很好(我使用的是Enterprise Libary Logging块,但应用于Log4Net应该也很容易):

在你的Shell中创建一个Bootstrapper:

-My Project
  -Shell Module (add a reference to the Infrastructure project)
    -Bootstrapper.cs
在您的基础设施项目中创建一个日志适配器,例如:
-My Project
  -Infrastructure Module
    -Adapters
      -Logging
        -MyCustomLoggerAdapter.cs
        -MyCustomLoggerAdapterExtendedAdapter.cs
        -IFormalLogger.cs

MyCustomLoggerAdapter 类将用于覆盖 Bootstrapper 中的“LoggerFacade”属性。它应该有一个默认的构造函数,以创建实例。

注意:通过覆盖 Bootstrapper 中的 LoggerFacade 属性,您为 Prism 提供了记录其自身内部消息的日志机制。您可以在整个应用程序中使用此记录器,或者您可以扩展记录器以获得更完整的功能。 (请参阅 MyCustomLoggerAdapterExtendedAdapter/IFormalLogger)

public class MyCustomLoggerAdapter : ILoggerFacade
{

    #region ILoggerFacade Members

    /// <summary>
    /// Logs an entry using the Enterprise Library logging. 
    /// For logging a Category.Exception type, it is preferred to use
    /// the EnterpriseLibraryLoggerAdapter.Exception methods."
    /// </summary>
    public void Log( string message, Category category, Priority priority )
    {
        if( category == Category.Exception )
        {
            Exception( new Exception( message ), ExceptionPolicies.Default );
            return;
        }

        Logger.Write( message, category.ToString(), ( int )priority );
    }

    #endregion


    /// <summary>
    /// Logs an entry using the Enterprise Library Logging.
    /// </summary>
    /// <param name="entry">the LogEntry object used to log the 
    /// entry with Enterprise Library.</param>
    public void Log( LogEntry entry )
    {
        Logger.Write( entry );
    }

    // Other methods if needed, i.e., a default Exception logger.
    public void Exception ( Exception ex ) { // do stuff }
}

MyCustomLoggerAdapterExtendedAdapter是从MyCustomLoggerAdapter派生的,可以提供额外的构造函数以实现更全面的记录器。

public class MyCustomLoggerAdapterExtendedAdapter : MyCustomLoggerAdapter, IFormalLogger
{

    private readonly ILoggingPolicySection _config;
    private LogEntry _infoPolicy;
    private LogEntry _debugPolicy;
    private LogEntry _warnPolicy;
    private LogEntry _errorPolicy;

    private LogEntry InfoLog
    {
        get
        {
            if( _infoPolicy == null )
            {
                LogEntry log = GetLogEntryByPolicyName( LogPolicies.Info );
                _infoPolicy = log;
            }
            return _infoPolicy;
        }
    }

    // removed backing code for brevity
    private LogEntry DebugLog... WarnLog... ErrorLog


    // ILoggingPolicySection is passed via constructor injection in the bootstrapper
    // and is used to configure various logging policies.
    public MyCustomLoggerAdapterExtendedAdapter ( ILoggingPolicySection loggingPolicySection )
    {
        _config = loggingPolicySection;
    }


    #region IFormalLogger Members

    /// <summary>
    /// Info: informational statements concerning program state, 
    /// representing program events or behavior tracking.
    /// </summary>
    /// <param name="message"></param>
    public void Info( string message )
    {
        InfoLog.Message = message;
        InfoLog.ExtendedProperties.Clear();
        base.Log( InfoLog );
    }

    /// <summary>
    /// Debug: fine-grained statements concerning program state, 
    /// typically used for debugging.
    /// </summary>
    /// <param name="message"></param>
    public void Debug( string message )
    {
        DebugLog.Message = message;
        DebugLog.ExtendedProperties.Clear();
        base.Log( DebugLog );
    }

    /// <summary>
    /// Warn: statements that describe potentially harmful 
    /// events or states in the program.
    /// </summary>
    /// <param name="message"></param>
    public void Warn( string message )
    {
        WarnLog.Message = message;
        WarnLog.ExtendedProperties.Clear();
        base.Log( WarnLog );
    }

    /// <summary>
    /// Error: statements that describe non-fatal errors in the application; 
    /// sometimes used for handled exceptions. For more defined Exception
    /// logging, use the Exception method in this class.
    /// </summary>
    /// <param name="message"></param>
    public void Error( string message )
    {
        ErrorLog.Message = message;
        ErrorLog.ExtendedProperties.Clear();
        base.Log( ErrorLog );
    }

    /// <summary>
    /// Logs an Exception using the Default EntLib Exception policy
    /// as defined in the Exceptions.config file.
    /// </summary>
    /// <param name="ex"></param>
    public void Exception( Exception ex )
    {
        base.Exception( ex, ExceptionPolicies.Default );
    }

    #endregion


    /// <summary>
    /// Creates a LogEntry object based on the policy name as 
    /// defined in the logging config file.
    /// </summary>
    /// <param name="policyName">name of the policy to get.</param>
    /// <returns>a new LogEntry object.</returns>
    private LogEntry GetLogEntryByPolicyName( string policyName )
    {
        if( !_config.Policies.Contains( policyName ) )
        {
            throw new ArgumentException( string.Format(
              "The policy '{0}' does not exist in the LoggingPoliciesCollection", 
              policyName ) );
        }

        ILoggingPolicyElement policy = _config.Policies[policyName];

        var log = new LogEntry();
        log.Categories.Add( policy.Category );
        log.Title = policy.Title;
        log.EventId = policy.EventId;
        log.Severity = policy.Severity;
        log.Priority = ( int )policy.Priority;
        log.ExtendedProperties.Clear();

        return log;
    }

}


public interface IFormalLogger
{

    void Info( string message );

    void Debug( string message );

    void Warn( string message );

    void Error( string message );

    void Exception( Exception ex );

}

Bootstrapper 中:

public class MyProjectBootstrapper : UnityBootstrapper
{

  protected override void ConfigureContainer()
  {
    // ... arbitrary stuff

    // create constructor injection for the MyCustomLoggerAdapterExtendedAdapter
      var logPolicyConfigSection = ConfigurationManager.GetSection( LogPolicies.CorporateLoggingConfiguration );
      var injectedLogPolicy = new InjectionConstructor( logPolicyConfigSection as LoggingPolicySection );

      // register the MyCustomLoggerAdapterExtendedAdapter
      Container.RegisterType<IFormalLogger, MyCustomLoggerAdapterExtendedAdapter>(
              new ContainerControlledLifetimeManager(), injectedLogPolicy );

  }

    private readonly MyCustomLoggerAdapter _logger = new MyCustomLoggerAdapter();
    protected override ILoggerFacade LoggerFacade
    {
        get
        {
            return _logger;
        }
    }

}

最后,要使用任意一种记录器,你只需要将相应的接口添加到你的类构造函数中,UnityContainer会为你注入记录器:

public partial class Shell : Window, IShellView
{
    private readonly IFormalLogger _logger;
    private readonly ILoggerFacade _loggerFacade;

    public Shell( IFormalLogger logger, ILoggerFacade loggerFacade )
    {
        _logger = logger;
        _loggerFacade = loggerFacade

        _logger.Debug( "Shell: Instantiating the .ctor." );
        _loggerFacade.Log( "My Message", Category.Debug, Priority.None );

        InitializeComponent();
    }


    #region IShellView Members

    public void ShowView()
    {
        _logger.Debug( "Shell: Showing the Shell (ShowView)." );
         _loggerFacade.Log( "Shell: Showing the Shell (ShowView).", Category.Debug, Priority.None );
       this.Show();
    }

    #endregion

}

我认为您不需要单独的日志记录模块。通过将日志记录策略添加到基础架构模块中,所有其他模块都将获得所需的引用(假设您将基础架构模块添加为其他模块的引用)。并且通过在Boostrapper中添加记录器,您可以让UnityContainer根据需要注入日志记录策略。

在CodePlex的CompositeWPF contrib项目上,还有一个使用Log4Net的简单示例(链接)

希望对你有所帮助。


1
在这个解决方案中,我缺少log4net的分层日志记录。我认为在棱镜中也是如此。 - Philip Stuyck

4
我最终回到了这个问题,结果发现答案非常简单。在Shell项目中,将Log4Net配置为自定义记录器。Prism文档(2009年2月)在第287页解释了如何做到这一点。Shell项目是唯一需要引用Log4Net的项目。要访问记录器(假设所有模块都传递了对Prism IOC容器的引用),只需在IOC容器中解析ILoggerFacade即可获得对自定义记录器的引用。以正常方式向此记录器传递消息即可。
因此,无需将任何事件返回到Shell,也无需模块具有Log4Net引用。天哪,我真喜欢IOC容器!

4

上面建议使用的LoggerFacade的问题在于,应用程序中非prism部分不知道它。我认为Logger需要更低级别和更普遍地访问,而不仅仅是在Composite框架中。

我的建议是,为什么不依赖于标准的Debug/Trace并实现自己的TraceListener呢?这样它将适用于Prism/nonPrism部分,您可以通过此实现所需的灵活性。


阿门。我正在开发一个应用程序,开发人员一直在一些非常低层的代码中引用ILogger外观,这让我感觉很不好。 - Gusdor
2
是的,但log4net是一种分层记录器,比简单的调试跟踪日志记录要好得多。大型日志很难挖掘,log4net是可配置和可扩展的,即使对于低级例程也是如此。在这一点上,我不明白为什么甚至低级模块也不应该依赖于log4net。这是一个横切关注点。如果真的需要,创建一个接口记录器程序集,并对其进行依赖,并使用一个实现库。 - Philip Stuyck

1
每个模块都有单独的日志记录器配置可能会在部署时出现问题。请记住,高级用户或管理员可能会完全更改您的日志记录目标,将其重定向到数据库或集中存储库聚合日志记录服务(例如我的公司)。如果所有单独的模块都有单独的配置,则高级用户/管理员必须为每个模块重复配置(在每个.config文件中或在主app.config中每个模块的部分中),并且每次位置/格式更改时都要重复此操作。此外,由于附加程序是从配置中运行时添加的,并且可能存在您目前不知道的附加程序,因此某些人可能会使用锁定文件的附加程序,导致应用程序模块之间发生冲突。拥有一个单一的log4.net配置简化了管理。
仍然可以根据每个模块的需求单独配置各个模块(例如,DB层的INFO,UI层的ERROR)。每个模块都将通过请求自己的类型来获取记录器:LogManager.GetLogger(typeof(MyModule);但只有Shell将配置记录器(例如调用XmlConfigurator.Configure),使用自己的app.config。

1
即使每个项目都有自己对Log4Net的引用,它们也不会有单独的配置文件--这将是难以管理的。在解决方案中,每个项目都使用自己的日志记录(具有自己的Log4Net引用)时,实践似乎是在主应用程序中使用单个配置文件。请参见http://nirajrules.wordpress.com/2008/06/14/blogging-best-practicies/。 - David Veeneman
这个问题仍然没有解决。我已经了解到Prism有自己的日志记录功能,并且可以与Log4Net等记录器进行接口。但是我仍然没有找到最佳的日志记录策略。 - David Veeneman
我最终找到了答案--请看下面。我不会更改Metro Smurf的回答中的“正确”答案。他确实给出了将Log4Net与Prism接口的很好的示例。但是,我下面的回答真正解决了如何获取您设置的记录器的核心问题。 - David Veeneman
Metro Smurfs的回答比你的更符合“最佳实践”。 - Kris

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