A new logging API in EPiServer Framework

November 24, 2014

For as long as I can remember, the logging framework of choice for EPiServer has been log4net, but this is about to change now! Well, I say that, in reality we are only changing the way we are using the logging framework so that it is possible to change framework going forward. The main reason for this change being that it allows us to upgrade log4net to the latest version, something that was not previously possible due to an earlier change of the log4net strong name key without causing major problems.

The new Logging API that is shipping with EPiServer.Framework since version 7.17.0 is not meant to compete or be a replacement for existing logging frameworks such as log4net, but merely a simple abstraction for writing messages to a logging framework. To manage the configuration and output of the logger, simply use the API of the implementing framework of your choice.

EPiServer will be using it going forward and we will also be recommending it to be used by all add-on developers that previously has used log4net. And as an EPiServer site developer you are also free to use it on your sites should it suit your needs. The API is currently in a pre-release stage which means that classes and interfaces might change between minor version updates. So for the time being, even though we don’t expect any major changes, use with pre-caution and please provide feedback on any parts that you think need improvements.

Using the API

We tried to construct the Logging API to provide both a familiar and easy to use experience and at the same time make it really simple to move away from log4net.

LogManager

The LogManager class provides a simple way to create new logger instances. Simply call the static GetLogger() method to receive a logger for the class where it is called. This method is guaranteed to always return a new instance regardless if any implementation has been found, so you can safely use the logger without performing any null checks.

private static readonly ILogger Logger = LogManager.GetLogger();

Note that this method uses some reflection to resolve the caller class, so avoid calling it repeatedly if performance is critical or alternatively pass in the type explicitly as an argument. It should however be noted that the performance impact is negligible in most cases such as the one demonstrated above.

If you instantiate your object instances using the StructureMap container maintained by EPiServer, you also have the option of simply adding an ILogger dependency to your constructor.

private class MyClass
{
    private readonly ILogger _logger;

    public MyClass(ILogger logger)
    {
        _logger = logger;
    }
}

ILogger with extension methods

The ILogger interface itself only contains two methods. Instead the easy to use experience is provided by a set of extension methods.

In contrast to log4net, there is no need to change between for example Debug and DebugFormat or need to call IsDebugEnabled before starting any costly message serialization. The following overloads are provided for each different logging level or alternatively you can call Log and pass the log level as a parameter.

// Log debug message
logger.Debug("Some message");
// Log debug message and exception
logger.Debug("Some message", exception);
// Log debug formatted message with arguments
logger.Debug("Some format {0},{1}", arg0, arg1);
// Log debug message formatted by lambda expression
logger.Debug(someObject, s => "someObject is currently: " + s.SomeCostlyMethod());
// Log debug message and exception formatted by lambda expression
logger.Debug(someObject, someException, (s, ex) => "someObject: " + s.SomeCostlyMethod() + " threw an exception: " + ex);

The final two overloads below provides you with the option of passing in a lambda expression that is only evaluated once it has been confirmed that the logger is enabled for that level. It is therefore no need for any explicit calls to check if a level is enabled before calling unless you really need to cream out every last millisecond.

All extension methods will use the Invariant culture for string formatting, so if you want to format your values using a specific culture, we recommend that you call string.Format explicitly with your formatter of choice.

Backwards compatibility with log4net

As mentioned earlier, we will continue to make use of log4net as the default setup and we will ship a log4net implementation as a separate package called EPiServer.Logging.Log4Net. By creating it as a separate package, we will also be able to release an update that uses the latest version of log4net without breaking existing sites and add-ons. This package continues to define episerverlog.config as the log4net configuration file so there should be no need for any change on your site when you upgrade to maintain the same logging.

If you want to start using the new API in an existing project, but can’t stand the thought of having to update every single log4net call, do not despair. To ease your (and possibly our own) pain, we have added a dedicated namespace called EPiServer.Logging.Compatibility that will help with the migration. In this you can find a LogManager class and an ILog interface that matches their log4net equivalents so that you in most cases simply should be able to do a search and replace between ‘log4net’ and ‘EPiServer.Logging.Compatibility’ in your projects and recompile.

Implementing the API

Another objective that we wanted to achieve with the API was to make it really easy to create new implementations for other logging frameworks. There are only two interfaces with a minimal footprint that needs to be implemented. ILogger is responsible for writing a message of a specified level to the logging framework of choice and ILoggerFactory for creating new ILogger instances.

Implementation are registrered using an assembly attribute such as:

[assembly: LoggerFactory(typeof(MyLoggerFactory))]

Note that the logging API currently only supports one logger type and will use the first factory type it can find.

Advertisements
%d bloggers like this: