Using NLog with EPiServer and log4net

In my current EPiServer project we decided to use NLog for our logging needs rather than going with log4net. Since EPiServer has a dependency on the latter we wanted a way of bridging EPiServer’s log messages to our own NLog setup. This was accomplished by creating a new appender for log4net, redirecting all relevant log messages to it and converting them into something that NLog would like. The source code in it’s full, and some sample config, is available at GitHub.

Creating a bridge appender from log4net to NLog

The first thing that we need in a bridge class is a way of converting log levels as well as the log messages themselves. The former would have been an excellent switch statement had we had more constant values to compare with. All it really does is trying to figure out the log level of the log4net message mapping it to the corresponding NLog version.

NLogBridgeAppender.cs

private static LogLevel ToNLogLevel(Level level)
{
  LogLevel nLevel;

  if (level == Level.Fatal) nLevel = LogLevel.Fatal;
  else if (level == Level.Error) nLevel = LogLevel.Error;
  else if (level == Level.Warn) nLevel = LogLevel.Warn;
  else if (level == Level.Debug) nLevel = LogLevel.Debug;
  else if (level == Level.Info) nLevel = LogLevel.Info;
  else if (level == Level.Trace) nLevel = LogLevel.Trace;
  else if (level == Level.Off) nLevel = LogLevel.Off;
  else
  {
    var message = string.Format("Unsupported log level: {0}.", level);
    throw new NotSupportedException(message);
  }

  return nLevel;
}

We use the log level mapper method while creating the log message mapper itself. It takes a LoggingEvent sent out by log4net and returns NLog’s LogEventInfo.

private static LogEventInfo ToNLog(LoggingEvent loggingEvent)
{
  return new LogEventInfo
    {
      TimeStamp = loggingEvent.TimeStamp,
      LoggerName = loggingEvent.LoggerName,
      Message = Convert.ToString(loggingEvent.MessageObject),
      Level = ToNLogLevel(loggingEvent.Level),
      Exception = loggingEvent.ExceptionObject,
      FormatProvider = null
    };
}

Our NLogBridgeAppender class will need to extend AppenderSkeleton in order to become a log4net appender.

namespace MyWeb.Core.Infrastructure.Logging
{
  public class NLogBridgeAppender : AppenderSkeleton
  {

It will then give us a possibility to override the Append method for logging events as below. The method takes a log4net LoggingEvent which we feed to our NLog-mapper method turning it into a LogEventInfo object. After this we will fetch the proper logger and pass the new log object to it.

protected override void Append(LoggingEvent loggingEvent)
{
  var nLogEvent = ToNLog(loggingEvent);
  var nLogger = GetLogger(loggingEvent);
  nLogger.Log(nLogEvent);
}

In the GetLogger method we check to see if we already have a logger with the event’s LoggerName, and if we don’t, we use NLog’s LogManager to get it. The logger is then stored for later use before returned.

lock (_lockObject)
{
  if (_loggers.TryGetValue(loggingEvent.LoggerName, out nLogger))
  {
    return nLogger;
  }
  nLogger = LogManager.GetLogger(loggingEvent.LoggerName);
  _loggers = new Dictionary<string, Logger>(_loggers)
    {
      { loggingEvent.LoggerName, nLogger }
    };
}
return nLogger;

Configuring EPiServerLog.config to use the NLog appender

In our project we use the configuration file transform functionality that ships with EPiServer 7.5 to manage our config files. Below are selected parts from the log config common to all of our different environments. Please see the files at GitHub for the full transform file.

<?xml version="1.0" encoding="utf-8" ?>
<log4net xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">

  <appender xdt:Transform="Remove"
            xdt:Locator="Match(name)"
            name="errorFileLogAppender" />
  <appender xdt:Transform="Remove"
            xdt:Locator="Match(name)"
            name="outputDebugStringAppender" />

  <appender xdt:Transform="Insert"
            xdt:Locator="Match(name)"
            name="NLogBridgeAppender"
            type="MyWeb.Core.Infrastructure.Logging .NLogBridgeAppender, MyWeb.Core">
    <encoding value="utf-8" />
    <filter type="log4net.Filter.LoggerMatchFilter">
      <loggerToMatch value="MyWeb" />
      <acceptOnMatch value="false" />
    </filter>
  </appender>

  <root xdt:Transform="Replace">
    <level value="Warn" />
    <appender-ref ref="NLogBridgeAppender" />
  </root>

We remove EPiServer’s own errorFileLogAppender and outputDebugStringAppender as we don’t have any need for them. Then we add our own; the NLogBridgeAppender. As you can see, we set the appender type to the class namespace (MyWeb.Core.Infrastructure.Logging.NLogBridgeAppender) followed by the containing assembly (MyWeb.Core) in order to tell which class is holding the appender logic.

The log4net LoggerMatchFilter is in place to prevent duplicate log messages from our own assemblies; if we didn’t exclude log messages originating from the MyWeb namespace, they would be passed along to NLog by log4net and entered twice into our log storage.

The root element is replaced to use our new appender rather than EPiServer’s.

Configuring NLog

Configuring NLog is the easiest part, there is alot of good information out there. Below is a Common configuration transform for setting up one log file storing all messages originating from the MyWeb namespace, and one file for all log messages (including everything from EPiServer and third party assemblies).

NLog.Common.config

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <targets>
    <target xdt:Transform="Insert"
            xsi:type="File"
            name="AllFile"
            fileName="${basedir}/../Logs/All.${shortdate}.log"
            layout="[${longdate}][${uppercase:${level}}][${logger}] ${message} ${exception:format=ToString,StackTrace}" />
    <target xdt:Transform="Insert"
            xsi:type="File"
            name="MyWebFile"
            fileName="${basedir}/../Logs/MyWeb.${shortdate}.log"
            layout="[${longdate}][${uppercase:${level}}][${logger}] ${message} ${exception:format=ToString,StackTrace}" />
  </targets>
  <rules>
    <logger xdt:Transform="Insert"
            name="*"
            minlevel="Warn"
            writeTo="AllFile" />
    <logger xdt:Transform="Insert"
            name="MyWeb"
            minLevel="Warn"
            writeTo="MyWebFile" />
  </rules>
</nlog>

4 Comments

  1. Henrik Fransas August 22, 2014
    • Mathias Kunto August 23, 2014
  2. Jen Trinanes September 10, 2014
    • Mathias Kunto September 11, 2014