LOG4NET has been around for years… literally years! I remember using log4net for the first time some 5-6 years ago in my first job. Now almost 6 years later, I’m still finding log4net to be a reliable logging utility.

In this article, I will provide details on my implementation of log4net into a WCF Service for use with my development framework..

Firstly.. download the log4net library from Apache @ http://logging.apache.org/log4net/ Afterwards, extract and add the library reference into your project.

The goal of my logging service is to provide a clean logging wrapper for my consumers. Log4net itself is pretty straight forward to use, however to minimise the duplication of code and minimise the redundant customisation of the log4net library, it seemed fitting to control all configurations and my logging logic into one self-contained WCF service.

So.. lets begin..

The Constructor

To start logging messages you first need an instance of a log object. Log4net provides two ways of instantiating a logger.  The first is by its name, which is a simple string name.  The second is the object type.

So for the constructor, I’ve created the following to cater for both. My implementation is contained in a class file called LoggingUtility.cs:


        private readonly ILog _log;

        public LoggingUtility(string name)
        {
            _log = LogManager.GetLogger(name);
        }

        public LoggingUtility(Type type)
        {
            _log = LogManager.GetLogger(type);
        }

Logging Levels

Log4net provides 5 different levels of logging: Debug, Info, Warn, Error and Fatal

  • DEBUG level specifies that these messages are used for debugging purposes only, and shouldn’t be taken into consideration as an actual error.
  • INFO level specifies that these messages are to provide additional information to the developer/user
  • WARN level specifies that these messages are non-vital error, but should be investigated to ensure everything is correct.
  • ERROR level specifies that something unforeseen has occurred when it should not have happened. And should be looked into as soon as possible.
  • FATAL level specifies that something drastic has occurred and could potentially lead to data corruption and/or incorrect behaviour of the system.

Each level simply defines the criticality of the log messages. Because of this minute difference, each level category is very similar in the implementation. The difference is calling the equivalent method in the log4net library.

Implementation methods

In my LoggingUtility.cs implementation, I have coded the most commonly used function that I required in my framework. Ofcourse, this is just the tip of the iceberg. Additional functionality can be added in the future.

Below is a snippet of the method signatures for the DEBUG level:


        #region Debug

        //This logs a class object/message directly to the log output
        public void Debug(object message) {...}

        //This logs a class object and an Exception object together to the log output
        public void Debug(object message, Exception exception) {...}

        //This method allows the logging of messages with arguments passed in as a collection
        public void DebugFormat(string format, params object[] args) {...}

        //This method allows the logging of messages with one argument only
        public void DebugFormat(string format, object arg0) {...}

        //This method allows the logging of messages with a specific output format using the Provider passed in
        public void DebugFormat(IFormatProvider provider, string format, params object[] args) {...}

        #endregion

These provide some basic and extensible functionality for the implementation.
Below is the complete code snippet for all the DEBUG level methods:


        #region Debug

        //This logs a class object/message directly to the log output
        public void Debug(object message)
        {
            _log.Debug(message);
        }

        //This logs a class object and an Exception object together to the log output
        public void Debug(object message, Exception exception)
        {
            _log.Debug(message, exception);
        }

        //This method allows the logging of messages with arguments passed in as a collection
        public void DebugFormat(string format, params object[] args)
        {
            var message = string.Format(format, args);
            Debug(message);
        }

        //This method allows the logging of messages with one argument only
        public void DebugFormat(string format, object arg0)
        {
            DebugFormat(format, new[] { arg0 });
        }

        //This method allows the logging of messages with a specific output format using the Provider passed in
        public void DebugFormat(IFormatProvider provider, string format, params object[] args)
        {
            var message = string.Format(provider, format, args);
            Debug(message);
        }

        #endregion

The other levels are near duplicates of the above methods. The only difference should be the call to the corresponding log4net log-level function. For example, instead of calling Debug(message), one should call Fatal(message), or Info(message).

That’s pretty much it.. I know it doesn’t look like much. But using a custom implementation as a wrapper to the log4net library will make your code alot simply on the consumer side of things. It removes the need to hardcode the Logger Name/Object type, it removes the need to duplicate customisation of your loggers (when used in multiple applications), it removes the need to duplicate any custom logic you want to share across applications.

Customisation

Log4net is configured using the web.config (or eqiv app.config) of your project. Below I will describe how to setup two appenders to use with our implementation.

An ‘appender’ is a component that accepts log4net message. Examples of appenders could be the use of a log file in your file system, or the application event log on your machine, it could be emails, or the output of a console system etc. Think of an appender as the recipient of your log messages. Which are configured to handle the log messages as it sees fit. The list of pre-built appenders can be found under the namespace log4net.Appender.

WCF Service

Exposing these functions via a WCF Service is the next step. The WCF Service can then be consumed by all your applications built using your framework – without needing to worry about the logging implementation. The steps required to build a WCF Service and to link it to our LoggingUtility.cs class is out of the scope for this article. However, if you have any questions and need some pointers please drop a message.

Appenders

So below we have the configuration of our appenders. These are to be inserted between the <log4net> …</log4net> sections in the configuration file:

    <!-- Define some output appenders -->
    <appender name="RollingFile" type="log4net.Appender.RollingFileAppender">
      <evaluator type="log4net.Core.LevelEvaluator">
        <threshold value="Verbose"/>
      </evaluator>
      <maximumFileSize value="500KB"/>
      <file value="c:\\Logs\\GhostFrameworkLog.txt"/>
      <appendToFile value="true"/>
      <lockingModel type="log4net.Appender.RollingFileAppender+MinimalLock"/>
      <rollingStyle value="Date"/>
      <datePattern value="_yyyy-MM-dd.\tx\t"/>
      <staticLogFileName value="true"/>
      <layout type="log4net.Layout.PatternLayout,log4net">
        <param name="ConversionPattern" value="%date [%thread] %level [%logger{2}] - %message%newline"/>
      </layout>
    </appender>

    <appender name="EventLog" type="log4net.Appender.EventLogAppender">
      <mapping>
        <level value="ERROR" />
        <eventLogEntryType value="Error" />
      </mapping>
      <mapping>
        <level value="DEBUG" />
        <eventLogEntryType value="Information" />
      </mapping>

      <layout type="log4net.Layout.PatternLayout">
        <!-- Pattern to output the caller's file name and line number -->
        <conversionPattern value="%5level [%thread] (%file:%line) - %message%newline" />
      </layout>
    </appender>

    <root>
      <level value="DEBUG" />
      <appender-ref ref="EventLog"/>
      <appender-ref ref="RollingFile"/>
    </root>


The first appender is our file System log file appender.

The file is limited to 500kb in size. So the older messages will be pushed out automatically by log4net.
The file is appeneded to, instead of being cleared each time it is written to.
The file today will be called “GhostFrameworkLog.txt”
The file for previous dates will be appended with a “_yyyy-MM-dd” suffix.
Each log record in the file will be formatted with [date] [thread id] [log-level] [log name] – [log message]


The second appender is the configuration for our Application Event Log.

Log messages with a Log-level of ERROR will be mapped to the Event Log’s ERROR message type.
Log messages with a Log-level of DEBUG will be mapped to the Event Log’s INFO message type.
The Event Log’s message type simply governs what icon and criticality to display the messages under the Event log window.

Lastly the ROOT node of the log4net configuration states which appender(s) to use for which log-level messages.

The configuration sections can be preset, however it is this ROOT node that defines which appenders are actively used when logging certain messages.

As above, we make use of the File Appender only. To log our messages into both appenders, simply update the config section as below. If you want ALL log-level type messages to use both appenders, replace with <level value=”DEBUG” /> with <level value=”ALL” />

    <root>
      <level value="DEBUG" />
      <appender-ref ref="EventLog"/>
      <appender-ref ref="RollingFile"/>
    </root>

Conclusion

So here’s the complete LoggingUtility.CS file for your reference. I have added some custom methods for my own purposes. But feel free to customise it as you require..! Hope this article helped 🙂


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using log4net;
using log4net.Core;
using log4net.Layout;
using log4net.Layout.Pattern;
using System.Diagnostics;

namespace GhostFramework.Utility
{
    public class LoggingUtility : ILog
    {
        public enum EntryType
        {
            Info,
            Warning,
            Error
        }

        #region Private fields

        private readonly ILog _log;

        #endregion

        public LoggingUtility(string name)
        {
            _log = LogManager.GetLogger(name);
        }

        public LoggingUtility(Type type)
        {
            _log = LogManager.GetLogger(type);
        }

        #region Implementation of ILoggerWrapper

        ILogger ILoggerWrapper.Logger
        {
            get { return _log.Logger; }
        }

        #endregion

        #region Implementation of ILog

        #region Debug

        public void Debug(object message)
        {
            _log.Debug(message);
        }

        public void Debug(object message, Exception exception)
        {
            _log.Debug(message, exception);
        }

        public void DebugFormat(string format, params object[] args)
        {
            var message = string.Format(format, args);
            Debug(message);
        }

        public void DebugFormat(string format, object arg0)
        {
            DebugFormat(format, new[] { arg0 });
        }

        public void DebugFormat(IFormatProvider provider, string format, params object[] args)
        {
            var message = string.Format(provider, format, args);
            Debug(message);
        }

        #endregion

        #region Info

        public void Info(object message)
        {
            _log.Info(message);
        }

        public void Info(object message, Exception exception)
        {
            _log.Info(message, exception);
        }

        public void InfoFormat(string format, params object[] args)
        {
            var message = string.Format(format, args);
            Info(message);
        }

        public void InfoFormat(string format, object arg0)
        {
            InfoFormat(format, new[] { arg0 });
        }

        public void InfoFormat(IFormatProvider provider, string format, params object[] args)
        {
            var message = string.Format(provider, format, args);
            Info(message);
        }

        #endregion

        #region Warn

        public void Warn(object message)
        {
            _log.Warn(message);
        }

        public void Warn(object message, Exception exception)
        {
            _log.Warn(message, exception);
        }

        public void WarnFormat(string format, params object[] args)
        {
            var message = string.Format(format, args);
            Warn(message);
        }

        public void WarnFormat(string format, object arg0)
        {
            WarnFormat(format, new[] { arg0 });
        }

        public void WarnFormat(IFormatProvider provider, string format, params object[] args)
        {
            var message = string.Format(provider, format, args);
            Warn(message);
        }

        #endregion

        #region Error

        public void Error(object message)
        {
            _log.Error(message);
        }

        public void Error(object message, Exception exception)
        {
            _log.Error(message, exception);
        }

        public void ErrorFormat(string format, params object[] args)
        {
            var message = string.Format(format, args);
            Error(message);
        }

        public void ErrorFormat(string format, object arg0)
        {
            ErrorFormat(format, new[] { arg0 });
        }

        public void ErrorFormat(IFormatProvider provider, string format, params object[] args)
        {
            var message = string.Format(provider, format, args);
            Error(message);
        }

        #endregion

        #region Fatal

        public void Fatal(object message)
        {
            _log.Fatal(message);
        }

        public void Fatal(object message, Exception exception)
        {
            _log.Fatal(message, exception);
        }

        public void FatalFormat(string format, params object[] args)
        {
            var message = string.Format(format, args);
            Fatal(message);
        }

        public void FatalFormat(string format, object arg0)
        {
            FatalFormat(format, new[] { arg0 });
        }

        public void FatalFormat(IFormatProvider provider, string format, params object[] args)
        {
            var message = string.Format(provider, format, args);
            Fatal(message);
        }

        #endregion

        #region Properties
        public bool IsDebugEnabled
        {
            get { return _log.IsDebugEnabled; }
        }

        public bool IsInfoEnabled
        {
            get { return _log.IsInfoEnabled; }
        }

        public bool IsWarnEnabled
        {
            get { return _log.IsWarnEnabled; }
        }

        public bool IsErrorEnabled
        {
            get { return _log.IsErrorEnabled; }
        }

        public bool IsFatalEnabled
        {
            get { return _log.IsFatalEnabled; }
        }
        #endregion

        #endregion

        #region Stopwatch
        public void LogElapsed(Stopwatch sw, string actionDescription, object details = null, EntryType entryType = EntryType.Info)
        {
            WriteToLog(entryType,
                "{0}, Elapsed {1}. {2}",
                actionDescription,
                sw.Elapsed,
                details);
        }

        public void LogElapsed(
            Stopwatch sw,
            string actionDescription,
            int thresholdMilliseconds,
            object details = null,
            EntryType entryType = EntryType.Warning)
        {
            LogElapsed(sw, actionDescription, TimeSpan.FromMilliseconds(thresholdMilliseconds), details, entryType);
        }

        public void LogElapsed(
            Stopwatch sw,
            string actionDescription,
            TimeSpan threshold,
            object details = null,
            EntryType entryType = EntryType.Warning)
        {
            var elapsed = sw.Elapsed;
            if (elapsed > threshold)
            {
                WriteToLog(entryType, "{0} -> {1} (Expected {2}). {3}",
                    actionDescription,
                    elapsed,
                    threshold,
                    details);
            }
        }

        protected virtual void WriteToLog(EntryType entryType, string formatString, params object[] args)
        {
            if (entryType == EntryType.Info)
            {
                InfoFormat(formatString, args);
            }
            else if (entryType == EntryType.Warning)
            {
                WarnFormat(formatString, args);
            }
            else if (entryType == EntryType.Error)
            {
                ErrorFormat(formatString, args);
            }
            else
            {
                throw new ArgumentOutOfRangeException();
            }
        }
        #endregion
    }
}