What is the best way of using NLog with MEF?
Asked Answered
F

4

5

I am wondering what is the best way to use NLog with Managed Extensibility Framework (MEF)?

I have an application that support plugins using MEF architecture (Import and Exports etc) I want to add logging capability to my application. As a logging component I want to use NLog.

What would you recommend? 1. Create a wrapper for NLog, i.e. additional plugin that configures NLog and exports functions like void Log(string level, string message) that other plugins importing 2. Every plugin should have it is own instance of NLog configured and used. (They all would write to the same file actually).

Fogg answered 13/9, 2010 at 23:54 Comment(0)
S
6

This is an interesting approach, however, it seems to suffer from the drawback that all loggers that are injected (or the one singleton that is injected) will be the same instance (or will have the same name, the name being the name of the NLogLoggingService class. That means that you cannot very easily control the granularity of logging (i.e. turn logging to "Info" level in one class and "Warn" in another class). Also, if you opt to use the call site formatting tokens, you will always get the call site of the call the the NLog logger rather than the call site in your application code.

Here is an abbreviated version of the logger that was linked:

  [Export(Services.Logging.LoggingService, typeof(ILoggingService))] 
  class NLogLoggingService : ILoggingService 
  { 
    Logger log; public NLogLoggingService() 
    { 
      log = LogManager.GetCurrentClassLogger(); 
    } 

    public void Debug(object message) 
    {
      log.Debug(message); 
    }
    public void DebugWithFormat(string format, params object[] args) 
    { 
      if (args.Length == 0) 
      { 
        log.Debug(format); 
      } 
      else
      { 
        Debug(string.Format(format, args)); 
      }
    } 
    public bool IsDebugEnabled 
    { 
      get 
      { 
        return log.IsDebugEnabled; 
      } 
    } 
  }

In the constructor LogManager.GetCurrentClassLogger() is used to get the NLog logger. GetCurrentClassLogger will return a NLog logger that is "named" based on the "current" type, which, in this case, is NLogLoggingService. So, to configure NLog in the app.config file, you will configure based on the that the logger is named "SoapBox.Core.NLogLoggingService". Commonly, in code that uses NLog (or log4net) directly, each class gets its own uniquely named logger like this:

namespace MyNamespace
{
  public class MyClass1
  {
    private static readonly Logger logger LogManager.GetCurrentClassLogger();

    public void DoSomeWork()
    {
      logger.Info("Logging from inside MyClass1.DoSomeWork");
    }
  }

  public class MyClass2
  {
    private static readonly Logger logger LogManager.GetCurrentClassLogger();

    public void DoSomeWork()
    {
      logger.Info("Logging from inside MyClass2.DoSomeWork");
    }
  }
}

Now the logging for MyClass1 and MyClass2 is individually controllable. You can configure different levels for each class, send them to different targets, or turn one or both off altogether. Alternatively, due to the concept of logger hierarchies in both log4net and NLog, you could control the logging in both class simultaneously by configuring a "logger" for the namespace (MyNamespace in this case), or any "ancestor" namespace. If there is not a logger configured for the fully qualified typename, then the logging framework essentially moves up the hierarchy by considering the name a dot delimited string and removing the last chunk and checking to see if that logger is configured. So, in this case, we are requesting loggers for MyNamespace.MyClass1 and MyNamespace.MyClass2. I could configure the app.config file to have MyNamespace log at the "info" and write to a file target (appender in log4net-speak). If I did that, then both loggers that I requested via their fully qualified names would inherit the MyNamespace configuration.

With the suggested way of injecting NLog via MEF, you will only have one logger instance, so you cannot configure each class to log differently. Also, as I mentioned earlier, if you opt to log call site information, you will always get "SoapBox.Core.NLogLoggingService" for the class and "Debug" (or DebugWithFormat, or Info, or InfoWithFormat, etc) for the method.

This seems to be an issue with successfully injecting loggers from log4net and NLog. You can see the question that I asked about this very issue a couple of months ago.

Ultimately I was able to figure out how some dependency injection frameworks can successfully inject log4net and NLog loggers that are specific to the class being created (i.e. if the DI framework is instantiating MyClass, which in turn depends on an ILogger interface, then MyClass will get a logger that is essentially equivalent to what would have happened if MyClass requested the logger itself via the LogManager.GetCurrentClassLogger api). Generally "resolvers" in DI/IoC frameworks are given the current context (containing, among other information, the type of the object currently being created). With that type available, it becomes a simple matter of having a logging framework-specific resolver receive that type and pass it along to the logging framework to create a logger appropriate for that type.

In order to get the most out of NLog's (and log4net's) capabilities you would really like to be able to tell MEF that your class is dependendent on "ILogger", but also that the instance of "ILogger" that gets injected into your class should depend on the Type of your class.

I don't know how easy it will be to achieve that with MEF. Alternatively, you could wrap NLog's static LogManager in a ILogManager and inject that. That would deviate from the normal "inject ILogger" paradigm.

To summarize: If you inject NLog via MEF this way, you will indeed be able to log with NLog, but you will only ever have one named logger (SoapBox.Core.NLogLoggingService). This means that you will not be able control with any degree of granularity - either for levels/on/off or for output (NLog Target/log4net Appender)

I don't have a good answer for what to do as far as injecting NLog via MEF AND keeping the granularity/flexibility that "raw" NLog gives you.

I can say that we have decided to use Common.Logging for .NET to abstract the logging framework but we decided NOT to inject logging. Instead, we will just use a static LogManager (as provided by Common.Logging) to hand out loggers.

Slurry answered 6/10, 2010 at 17:23 Comment(0)
C
1

I think Option 1 is better.

You can take a look at how the open source framework SoapBox Core imports a reference to an ILoggingService using MEF. It also provides a default implementation of the logging service based on NLog, but you could easily swap it out for log4Net, for example.

For reference:

SoapBox Core is LGPL'd, so you might be able to use (this part) in your application.

Chest answered 22/9, 2010 at 16:43 Comment(1)
That is a prettty cool implementation of a MEF-able NLog logger, but it does suffer from a couple of drawbacks: 1. All MEF-ed loggers for an application will actually be the same logger, so you don't have much control over your logging. 2. Call site information is lost as the call site of the logging class will be used. Off the top of my head don't have a better suggestion for MEF, but I do think it is worth noting those shortcomings. See my answer for a longer version.Slurry
S
1

I have been fighting with this problem a while now.

Really improtant was the Callsite (FullyQualified Namespace) within the logfiles.

First, i tryed to get the right logger out of the Stacktrace:

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static NLog.Logger GetLogger()
    {
        var stackTrace = new StackTrace(false);
        StackFrame[] frames = stackTrace.GetFrames();
        if (null == frames) throw new ArgumentException("Stack frame array is null.");
        StackFrame stackFrame;
        switch (frames.Length)
        {
            case 0:
                throw new ArgumentException("Length of stack frames is 0.");
            case 1:
            case 2:
                stackFrame = frames[frames.Length - 1];
                break;
            default:
                stackFrame = stackTrace.GetFrame(2);
                break;
        }

        Type declaringType = stackFrame.GetMethod()
                                       .DeclaringType;

        return declaringType == null ? LogManager.GetCurrentClassLogger() :                 LogManager.GetLogger(declaringType.FullName);
    }

But sadly, the Stacktrace with MEF is very long and i cannot clearly identify the correct caller for the Requester of the ILogger.

So, instead of injecting the ILogger Interface via Constructor Injection, i have created a ILogFactory Interface, that can get injected via Constructor Injection and call then the Create Method on the Factory

    public interface ILogFactory
    {
        #region Public Methods and Operators

        /// <summary>
        ///     Creates a logger with the Callsite of the given Type
        /// </summary>
        /// <example>
        ///     factory.Create(GetType());
        /// </example>
        /// <param name="type">The type.</param>
        /// <returns></returns>
        ILogger Create(Type type);

        #endregion
    }

And implemented it:

    using System;
    using System.ComponentModel.Composition;

    [Export(typeof(ILogFactory))]
    [PartCreationPolicy(CreationPolicy.Shared)]
    public class LogFactory : ILogFactory
    {
        #region Public Methods and Operators

        public ILogger Create(Type type)
        {
            var logger = new Logger().CreateLogger(type);
            return logger;
        }

        #endregion
    }

With the ILogger:

    public interface ILogger
    {
        #region Public Properties

        bool IsDebugEnabled { get; }

        bool IsErrorEnabled { get; }

        bool IsFatalEnabled { get; }

        bool IsInfoEnabled { get; }

        bool IsTraceEnabled { get; }

        bool IsWarnEnabled { get; }

        #endregion

        #region Public Methods and Operators

        void Debug(Exception exception);
        void Debug(string format, params object[] args);
        void Debug(Exception exception, string format, params object[] args);
        void Error(Exception exception);
        void Error(string format, params object[] args);
        void Error(Exception exception, string format, params object[] args);
        void Fatal(Exception exception);
        void Fatal(string format, params object[] args);
        void Fatal(Exception exception, string format, params object[] args);
        void Info(Exception exception);
        void Info(string format, params object[] args);
        void Info(Exception exception, string format, params object[] args);
        void Trace(Exception exception);
        void Trace(string format, params object[] args);
        void Trace(Exception exception, string format, params object[] args);
        void Warn(Exception exception);
        void Warn(string format, params object[] args);
        void Warn(Exception exception, string format, params object[] args);

        #endregion
    }

and Implementation of:

    using System;

      using NLog;
      using NLog.Config;

      /// <summary>
      ///     The logging service.
      /// </summary>
      public class Logger : NLog.Logger, ILogger
      {
          #region Fields

          private string _loggerName;

          #endregion

          #region Public Methods and Operators

          /// <summary>
          ///     The get logging service.
          /// </summary>
          /// <returns>
          ///     The <see cref="ILogger" />.
          /// </returns>
          public ILogger CreateLogger(Type type)
          {
              if (type == null) throw new ArgumentNullException("type");               

              _loggerName = type.FullName;

              var logger = (ILogger)LogManager.GetLogger(_loggerName, typeof(Logger));

              return logger;
          }

To use it... just inject the ILogFactory and calle the Create Method in a Mefed Importing Constructor:

      [ImportingConstructor]
      public MyConstructor(          
        ILogFactory logFactory)
       {
        _logger = logFactory.Create(GetType());
        }

hope this helps

Solfatara answered 18/7, 2013 at 9:3 Comment(0)
F
1

If you create a new ExportProvider and cast the ImportDefinition being passed in to a ICompositionElement. You can get the type that the logger is being injected into.

Here is the ExportProvider

public class LoggerExportProvider : ExportProvider
{
    private readonly ExportDefinition _loggerExportDefinition;

    private readonly Func<string, ILogger> _loggerFactory;

    /// <summary>
    /// Initializes a new instance of the <see cref="LoggerExportProvider"/> class.
    /// </summary>
    /// <param name="loggerFactory">The logger factory function.</param>
    public LoggerExportProvider(Func<string, ILogger> loggerFactory)
    {
        _loggerFactory = loggerFactory;
        _loggerExportDefinition = new ExportDefinition(typeof (ILogger).FullName, new Dictionary<string, object> {{"ExportTypeIdentity", typeof (ILogger).FullName}});
    }

    protected override IEnumerable<Export> GetExportsCore(ImportDefinition definition, AtomicComposition atomicComposition)
    {
        IList<Export> exports = new List<Export>();
        var compositionElement = definition as ICompositionElement;
        if (compositionElement == null || compositionElement.Origin == null)
            return exports;

        var constraint = definition.Constraint.Compile();
        if (constraint(_loggerExportDefinition))
            exports.Add(new Export(_loggerExportDefinition, () => _loggerFactory(compositionElement.Origin.DisplayName)));

        return exports;
    }
}

This is setup in such a way that it will work with any logging framework as you need to pass in a function that will return an ILogger (the Ilogger is our own, you'll have to create your own interface or just make it specific to Nlog). The string being passed to the function is the full class name that the type is being injected too. (compositionElement.Origin.DisplayName)

An example of bootstrapping MEF with this would look like this:

public class Example
{
    [Import]
    public ILogger Logger { get; set;}

    public Example()
    {
        var aggregatecatalogue = new AggregateCatalog();
        aggregatecatalogue.Catalogs.Add(new AssemblyCatalog(typeof (ILogger).Assembly));
        aggregatecatalogue.Catalogs.Add(new AssemblyCatalog(GetType().Assembly));
        var container = new CompositionContainer(aggregatecatalogue, new LoggerExportProvider(s => new MockLogger(s)));
        container.ComposeParts(this);
    }
}

The code above was copied from a unit test, so I'm just add specific assemblies instead of parsing a directory. The MockLogger is an implementation of the ILogger interface that takes the logging class name (or injecting type) as a parameter to it's constructor.

This doesn't require parsing any stack traces and pulls the information that is otherwise sitting there directly out of MEF.

Farflung answered 31/12, 2013 at 19:33 Comment(2)
I'm not sure that compositionElement would ever be null, but that was to make resharper happy, it passes the unit tests but isn't in production code yet.Farflung
This ended up in production code and works great btw! Although I'm no longer using MEF at my current gig so...Farflung

© 2022 - 2024 — McMap. All rights reserved.