views:

282

answers:

3

I am creating a Composite WPF (Prism) app with several different projects (Shell, modules, and so on). I am getting ready to implement logging, using Log4Net. It seems there are two ways to set up the logging:

  • Let the Shell project do all of the actual logging. It gets the reference to Log4Net, and other projects fire composite events to let the Shell know that it needs to log something. Those projects fire the events only for levels where logging is turned on in the Shell's app.config file (DEBUG, ERROR, etc), so as not to degrade performance.

  • Give each project, including modules, a Log4Net reference, and let the project do its own logging to a common log file, instead of sending messages to the Shell for logging.

Which is the better approach? Or, is there another approach that I should consider? Thanks for your help.

A: 

Having separate logger configurations for each module might turn into problems at deployment. Remember that a power user or administrator may completely change the target of your logging, redirecting to a database or to a central repository aggregated logging service (like my company's one). If all separate modules have separate configurations, the power user/admin has to repeat the configuration for each module (in each .config file, or in each module's section in the main app.config), and repeat this every time a change in location/formatting occurs. And besides, given that the appenders are added at run time from configuration and there may be appenders you don't know anything about at the moment, someone may use an appender that locks the file and result in conflict between the app modules. Hsving one single log4.net config simplifies administration.

Individual modules can still be configure as for the needs of each one, separately (eg. INFO for DB layer, ERROR for UI layer). Each module would get the logger by asking for its own type: LogManager.GetLogger(typeof(MyModule); but only the Shell will configure the logger (eg. call XmlConfigurator.Configure), using its own app.config.

Remus Rusanu
Even if each project has its own reference to Log4Net, they wouldn't have separate configuration files--that would be unmanageable. Where eachproject in a solution does its own logging (with its own Log4Net reference) the practive seems to be to use a single configuration file in the main app. See http://nirajrules.wordpress.com/2008/06/14/blogging-best-practicies/
David Veeneman
This question is still open. I have learned that Prism does its own logging, and that it can be interface to a logger such as Log4Net. But I still haven't found a best practice for logging strategy.
David Veeneman
I did finally find my answer--see below. I'm not going to change the 'correct' answer from Metro Smurf's response. He does give a good example of interfacing Log4Net to Prism. But my response below really addresses the core issue of how to get the logger you set up.
David Veeneman
A: 

The simplist approach to logging in Prism is to override the LoggerFacade property in your Bootstrapper. By overridding the LoggerFacade, you can pass in an instance of any Logger you want with any configuration needed as long as the logger implements the ILoggerFacade interface.

I've found the following to work quite well for logging (I'm using the Enterprise Libary Logging block, but applying something similar for Log4Net should be straight forward):

Create a Boostrapper in your Shell:

-My Project
  -Shell Module (add a reference to the Infrastructure project)
    -Bootstrapper.cs

Create a Logging Adapter in your Infrastructure project, i.e.,:

-My Project
  -Infrastructure Module
    -Adapters
      -Logging
        -MyCustomLoggerAdapter.cs
        -MyCustomLoggerAdapterExtendedAdapter.cs
        -IFormalLogger.cs

The MyCustomLoggerAdapter class will be used to override the LoggerFacade property in the Boostrapper. It should have a default contstructor that news everything up.

Note: by overriding the LoggerFacade property in the Bootstrapper, you are providing a logging mechanisim for Prism to use to log its own internal messages. You can use this logger throughout your application, or you can extend the logger for a more fully featured logger. (see 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 }
}

The MyCustomLoggerAdapterExtendedAdapter is dervied from the MyCustomLoggerAdapter and can provide additional constructors for a more full-fledged logger.

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 );

}

In the Boostrapper:

public class MyProjectBootstrapper : UnityBootstrapper
{

  protected override void ConfigureContainer()
  {
    // ... 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;
        }
    }

}

Finally, to use either logger, all you need to do is add the appropriate interface to your class' constructor and the UnityContainer will inject the logger for you:

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

}

I don't think you need a separate module for the logging policy. By adding the logging policies to your infrastructure module, all other modules will get the required references (assuming you add the infrastructure module as a reference to your other modules). And by adding the logger to your Boostrapper, you can let the UnityContainer inject the logging policy as needed.

There is a simple example of uisng Log4Net on the CompositeWPF contrib project on CodePlex as well.

HTH's

Metro Smurf
David Veeneman
A: 

I finally got back to this one, and it turns out the answer is really pretty simple. In the Shell project, configure Log4Net as a custom logger. The Prism Documentation (Feb. 2009) explains how to do that at p. 287). The Shell project is the only project that needs a reference to Log4Net. To access the logger (assuming all modules are passed a reference to the Prism IOC container), simply resolve ILoggerFacade in the IOC container, which will give you a reference to your custom logger. Pass a message to this logger in the normal manner.

So, there is no need for any eventing back to the Shell, and no need for modules to have Log4Net references. Holy mackerel, I love IOC containers!

David Veeneman