It is much more configurable. As it provides pretty comprehensive log4net.config section.
It has good number of appenders that come with the framework it self.
It is extensible.
I found much more easier to add custom appenders, configuration properties etc.
It has a very good online documentation.
There are many other frameworks which give capability to integrate log4net logging with it. For example
- Castle has provided an excellent plug-in to integrate it with WCF service. Since the application was exposed as a web service, we wanted to log every request and response to and from the service. With windsor container, we were able to intercept and request in and response going out from the service. At this interceptor we called did our logging.
- NHibernate also allows us to log all SQL-queries fired through it. Here is the link to know how to do this:
http://blogs.hibernatingrhinos.com/nhibernate/archive/2008/07/01/how-to-configure-log4net-for-use-with-nhibernate.aspx
Here is how I started with a simple implementation of loggers.
I created a class with a dictionary of loggers with key as logging level and value as the logger it self. I wanted my application to call simple methods like debug, error and let Logging wrapper take care of choosing the logger itself. So I had static methods to the class which selected the appropriate logger and logged. In the static constructor I also had to configure my loggers once i had created them. I did that using
log4net.Config.XmlConfigurator.ConfigureAndWatch(new FileInfo("log4net.config"))
This loads the log4net.config from the running directory and configures the logger with the appenders etc.
[NonSerialized] private static readonly IDictionary loggers = new Dictionary();
static Logger()
{
loggers.Add(LoggingLevel.DEBUG, LogManager.GetLogger(LoggingLevel.DEBUG));
loggers.Add(LoggingLevel.INFO, LogManager.GetLogger(LoggingLevel.INFO));
loggers.Add(LoggingLevel.ERROR, LogManager.GetLogger(LoggingLevel.ERROR));
loggers.Add(LoggingLevel.FATAL, LogManager.GetLogger(LoggingLevel.FATAL));
log4net.Config.XmlConfigurator.ConfigureAndWatch(new FileInfo("log4net.config"));
}
public static string FormatErrorMessage(Exception ex)
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.AppendFormat("Error message: {0} \n Stack trace: {1}", CompleteExceptionMessage(ex),
CompleteStackTrace(ex));
return stringBuilder.ToString();
}
public static void Debug(string message)
{
loggers[LoggingLevel.DEBUG].Debug(message);
}
I had a slightly weird requirement of logging the entire request and response message through the wcf service. Infact I feel this is a common requirement and we should provide a appender within log4net library to do this.
Initially when we used the logging framework as it is, we realized that our messages were getting serialized even though logging was turned off. This was because, we use to serialize the message and then pass the message as string to logger for logging.
To overcome this, we had to extend the log4net implementation where we override the Debug(object message), and Info(object message) method. We also had to write our own appender. This appender would in turn serialize the object passed in and log it. It was simple, we just derive the existing rollingfileAppender and override the existing append method.
The reason why we had to override the methods was because log4net internally converts everything to string before constructing LogEventData object. So even though we were passing in the object, we use to get object.ToString(). We ended up creating a custom logger which was basically extending the existing one. In this we, hid the existing logger methods by ours and created the loggingEvent object the way we wanted. It was then picked up by our appender from log4net.config file.
In the class diagram, you will notice that I had write a CommonLogManager as well. Since I have written a custom logger around the existing logger. I need to wrap the logger that the log4net logmanger return by the one that I have written. That's exactly what WrapperMap allows me to do. This is how it does.
public static class CommonLogManager
{
private static readonly WrapperMap s_wrapperMap =
new WrapperMap(WrapperCreationHandler);
public static CommonLogImpl GetLogger(string name)
{
return WrapLogger(LoggerManager.GetLogger(Assembly.GetCallingAssembly(), name));
}
private static CommonLogImpl WrapLogger(ILogger logger)
{
return (CommonLogImpl) s_wrapperMap.GetWrapper(logger);
}
private static ILoggerWrapper WrapperCreationHandler(ILogger logger)
{
return new CommonLogImpl(logger);
}
}
If you look at my implementation of logger class -- where I create different loggers into a dictionary. It turns out that it is not testable. I would like to write test around logging that I am doing. For that -- I change small bits in the class.
public class Logger
{
[NonSerialized] private static IDictionary loggers = new Dictionary();
static Logger()
{
loggers.Add(LoggingLevel.DEBUG, CommonLogManager.GetLogger(LoggingLevel.DEBUG));
loggers.Add(LoggingLevel.INFO, CommonLogManager.GetLogger(LoggingLevel.INFO));
loggers.Add(LoggingLevel.ERROR, CommonLogManager.GetLogger(LoggingLevel.ERROR));
loggers.Add(LoggingLevel.FATAL, CommonLogManager.GetLogger(LoggingLevel.FATAL));
XmlConfigurator.ConfigureAndWatch(new FileInfo("log4net.config"));
}
protected static IDictionary Loggers
{
get { return loggers; }
set { loggers = value; }
}
And here is how my test looked like.
[TestFixture]
public class DemoLoggingTest
{
#region Setup/Teardown
[SetUp]
public void Setup()
{
request = new request();
request.operand1 = 10;
request.operand2 = 0;
request.secretpassword = "password";
demoLogging = new DemoLibrary();
mockRepository = new MockRepository();
}
#endregion
private request request;
private DemoLibrary demoLogging;
private MockRepository mockRepository;
private void CreatemockLogger(ILog mockCommonLogger)
{
var loggerStub = new LoggerStub();
var logs = new Dictionary();
logs.Add("DEBUG", mockCommonLogger);
logs.Add("ERROR", mockCommonLogger);
loggerStub.injectLoggers(logs);
}
[Test]
[ExpectedException(typeof (DivideByZeroException))]
public void ShouldLogDebugLogs()
{
var mockCommonLogger = (ILog) mockRepository.CreateMock(typeof (ILog));
CreatemockLogger(mockCommonLogger);
mockCommonLogger.Debug(request);
mockCommonLogger.Error(null);
LastCall.On(mockCommonLogger).IgnoreArguments();
mockRepository.ReplayAll();
demoLogging.Divide(request);
mockRepository.VerifyAll();
}
[TearDown]
public void TearDown()
{
request = null;
demoLogging = null;
mockRepository = null;
}
}
internal class LoggerStub : Logger
{
public void injectLoggers(IDictionary dictionary)
{
Loggers = dictionary;
}
}
For writing my appenders, -- it was a piece of cake. Here is an example.
public class SerializingRollingFileAppenderAppender : RollingFileAppender
{
protected override void Append(LoggingEvent loggingEvent)
{
LoggingEventData data = loggingEvent.GetLoggingEventData();
data.Message = LogMessageHelper.FormatMessageBeforeLogging(loggingEvent);
base.Append(new LoggingEvent(data));
}
}
public class LogMessageHelper
{
private static string SerializeMessage(object message)
{
if (message is string) return (string) message;
using (var writer = new StringWriter())
{
SerializeMessage(message, writer);
return writer.ToString();
}
}
private static void SerializeMessage(object message, TextWriter writer)
{
new XmlSerializer(message.GetType()).Serialize(writer, message);
}
public static string GetThreadId()
{
return "Thread Id: " + Thread.CurrentThread.ManagedThreadId;
}
public static string FormatMessageBeforeLogging(LoggingEvent loggingEvent)
{
var sb = new StringBuilder();
sb.Append(" ");
sb.Append(GetThreadId());
string data = SerializeMessage(loggingEvent.MessageObject);
sb.Append(data);
sb.Append(Environment.NewLine);
return sb.ToString();
}
}
Since our request/response messages had confidential information which we did not want to log. We had to make our custom parameters take in a set of regex's. This regex's were applied on the serialized message and the matches were hashed. In order to do this, you basically need to add a property which by convention is going to be used in config file as well. This is how my appender looked like.
public class SerializingRollingFileAppenderAppender : RollingFileAppender
{
private readonly List RegexFilterPatterns = new List();
public string RegexFilterPattern
{
set { RegexFilterPatterns.Add(value); }
}
protected override void Append(LoggingEvent loggingEvent)
{
LoggingEventData data = loggingEvent.GetLoggingEventData();
data.Message = LogMessageHelper.FormatMessageBeforeLogging(loggingEvent,RegexFilterPatterns);
base.Append(new LoggingEvent(data));
}
}
Hope this is all that one may need for logging.
However -- I did miss making asynchronous call to logging in log4net.