Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Flexible Logging using log4net

3.57/5 (4 votes)
20 Aug 2016CPOL5 min read 29.8K   515  
This is a logging component enabling extended logging and multiple targets

Introduction

The intention of this component is to help in writing log information into multiple targets (file, database, service etc...) in a more flexible way and each log is stored as a JSON string which is easier to analyze later (we have tools to visualize json data like http://jsonviewer.stack.hu/).

Background

We were discussing to use log4net for logging but at the same time, wanted to give flexibility to the callers to pass their own data not as a string but an object and to control where to save the logs. I have already posted an article for logging but it was a simple logging into a text file and not flexible. That was when we decided to write code to meet this requirement.

Design

Dependencies

First, we need two libraries:

  1. log4net -https://logging.apache.org/log4net/
  2. Newtonsoft.json - This converts the C# object to a JSON string

These components are already part of the code if you download the source code from this post.

The Solution Structure

There are five projects in the solution. They are:

  1. Business - A class library wrapping up the log4net and exposes a service class Logger for the users, in this case the ConsoleApplication1. It may sound like the design is overly complex for this small example but in real project, you may have to have this kind of separation of things for easier maintenance.
  2. ConsoleApplication1 - Just a Console app used to test the logging
  3. Contracts - A class library holding all classes for logging and your custom log classes
  4. FlexibleLogging - The core library where we use the log4net to wire up the logging to work
  5. FlexibleLoggingAppenders - A class library having extended logging targets than the ones provided by log4net. Here, we intend to use an external service such as WCF for saving the log.

Using the Code

Before we look at the code, first make sure the log4net.config file is set to "Copy always" as shown below, otherwise logging will not work.

log4net.config

It is not necessary to have a separate config file, but it is convenient to have it in a separate file.

 <root>
  <!--
  1.OFF - nothing gets logged
  2.FATAL
  3.ERROR
  4.WARN
  5.INFO
  6.DEBUG
  7.ALL - everything gets logged
  -->
  <level value="ALL"/>
  <appender-ref ref="ServiceAppender"/>
  <appender-ref ref="AspNetTraceAppender"/>
  <appender-ref ref="RollingFile"/>
  <appender-ref ref="AdoNetAppender"/>
  <!--to debug log4net. check the output window of Visual Studio-->
  <appender-ref ref="DebugAppender"/>
</root>

In the root, we configured to write the log into five targets:

  1. ServiceAppender - This is our external service log not provided by the log4net itself
  2. AspNetTraceAppender - To view the log in the web page if Trace is enabled
  3. RollingFile - Log is written to a text file and a new file gets created once the size limit is reached
  4. AdoNetAppender - The log is written to a database
  5. DebugAppender - The log you can see it in Visual studio

log4net will support many targets than the one we used here, so you are free to add if you need it. Look here.

Just setting the targets alone will not write the log, we need to tell log4net how we intended to write the logs. So we look at how each one of them is configured.

ServiceAppender

HTML
<appender name="ServiceAppender" 
 type="FlexibleLoggingAppenders.ServiceAppender,FlexibleLoggingAppenders" >
    <filter type="log4net.Filter.LevelRangeFilter">
      <levelMin value="WARN" />
      <levelMax value="FATAL" />
    </filter>
    <layout type="log4net.Layout.PatternLayout">
      <conversionPattern value="%message%newline"/>
    </layout>
</appender> 
  1. If you look at the type attribute of the appender node, we inform the log4net to use the ServiceAppender class which is available in the FlexibleLoggingAppenders.dll (log4net will find it during the runtime from the bin folder of the calling application, in this case ConsoleApplication1 so we don't need to add reference to the FlexibleLogging project).
  2. Then, we specify a filter range between WARN and FATAL. This will make sure only the logs with that level will be calling this service.
  3. Then, we specify the log pattern. Here "%message%newline" means each message is written followed by a line break.

The only thing the ServiceAppender class should do is to derive from AppenderSkeleton which is a log4net class and overwrite the Append method to write your own logic. This sample code is just writing to the debug output.

C#
public class ServiceAppender : AppenderSkeleton
{
    protected override void Append(LoggingEvent piLoggingEvent)
    {
        string log = RenderLoggingEvent(piLoggingEvent);
        //write a code to pass this log elsewhere you like
        Debug.WriteLine(log);
    }
}

AspNetTraceAppender

C#
<appender name="AspNetTraceAppender" type="log4net.Appender.AspNetTraceAppender" >
  <layout type="log4net.Layout.PatternLayout">
    <conversionPattern value="%date [%thread] %-5level %logger [%property{NDC}] - %message%newline" />
  </layout>
</appender>

We have not added a filter here which means it logs everything and you can see it in trace log. See here what tracing in ASP.NET is.

RollingFile

C#
<appender name="RollingFile" type="log4net.Appender.RollingFileAppender">
  <!--
  The file location can be anywhere as long as the running application has read/write/delete access.
  The environment variable also can be set as the location.
  <file value="${TMP}\\Log4NetTest.log"/>
  or a physical path
  <file value="D:\Log4NetTest.log"/>
  -->
  <file value="${TMP}\\Log4NetTest.log"/>
  <appendToFile value="true"/>
  <rollingStyle value="Size" />
  <maxSizeRollBackups value="5" />
  <maximumFileSize value="5MB" />
  <!--Ensure the file name is unchanged-->
  <staticLogFileName value="true" />
  <lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
  <layout type="log4net.Layout.PatternLayout">
    <conversionPattern value="%message%newline"/>
  </layout>
 </appender>

The log is written to file and keeps maximum 5 backups once a file reaches 5MB in size.

I guess you got the point how each target is configured, so I will let you download the code to see the rest of the configuration yourself to help reduce the size of this article.

LogEntry

C#
public abstract class LogEntry
{
    public string Level { get; set; }
    public DateTime TimeStamp { get; private set; }
    public MethodInfo MethodInfo { get; private set; }
    public Type LogEntryType { get; set; }

    protected LogEntry(int piStackFrame)
    {
        TimeStamp = DateTime.Now;

        var callingMethod = new StackFrame(piStackFrame).GetMethod();

        MethodInfo = new MethodInfo()
        {
            Name = callingMethod.Name,
            Signature = callingMethod.ToString(),
        };
        if (callingMethod.DeclaringType == null) return;
        MethodInfo.ClassName = callingMethod.DeclaringType.ToString();
        LogEntryType = GetType();
    }

    protected LogEntry() : this(3)
    {

    }

    public override string ToString()
    {
        string json = this.toJson();
        return json;
    }
}

This is the base class for all logging and holds all basic stuff like the method and class from which the log is written. This leaves the callers can define their own log class by subclassing this and focus on defining their own properties. For example, a LoginSuccessLogEntry class can have properties like UserName, Role, etc.

In the attached solution, there are two pre-defined classes:

  1. MessageLogEntry - for information logs
  2. ErrorLogEntry - for any exception or error logs

Log4NetLoggingService

The class implementing the interface ILoggingService. The constructor can accept the optional targets (by default all) set by the users.

C#
    public class Log4NetLoggingService : ILoggingService
    {
        private readonly ILog _logger;

        static Log4NetLoggingService()
        {
            //Gets directory path of the calling application
            //RelativeSearchPath is null if the executing assembly i.e. calling assembly is a
            //stand alone exe file (Console, WinForm, etc). 
            //RelativeSearchPath is not null if the calling assembly is a web hosted application 
            //i.e. a web site
            var log4NetConfigDirectory = AppDomain.CurrentDomain.RelativeSearchPath ?? 
                                         AppDomain.CurrentDomain.BaseDirectory;
            var log4NetConfigFilePath = Path.Combine(log4NetConfigDirectory, 
                                        ConfigurationManager.AppSettings["log4netConfigFileName"]);

            XmlConfigurator.ConfigureAndWatch(new FileInfo(log4NetConfigFilePath));
        }

        public Log4NetLoggingService(LogTarget targets = LogTarget.All)
        {
            _logger = LogManager.GetLogger(new StackFrame(1).GetMethod().DeclaringType);
#if DEBUG
            var error = LogManager.GetRepository().ConfigurationMessages.Cast<LogLog>();
#endif

            if (targets.HasFlag(LogTarget.All))
                return;

            SwitchOffLogTargets(targets);
        }

        protected ILog logger { get { return _logger; } }

        public void Fatal(ErrorLogEntry logEntry)
        {
            logEntry.Level = Level.Fatal.ToString();
            if (_logger.IsFatalEnabled)
                _logger.Fatal(logEntry);
        }

        public void Error(ErrorLogEntry logEntry)
        {
            logEntry.Level = Level.Error.ToString();
            if (_logger.IsErrorEnabled)
                _logger.Error(logEntry);
        }

        public void Warn(LogEntry logEntry)
        {
            logEntry.Level = Level.Warn.ToString();
            if (_logger.IsWarnEnabled)
                _logger.Warn(logEntry);
        }

        public void Info(LogEntry logEntry)
        {
            logEntry.Level = Level.Info.ToString();
            if (_logger.IsInfoEnabled)
                _logger.Info(logEntry);
        }

        public void Debug(LogEntry logEntry)
        {
            logEntry.Level = Level.Debug.ToString();
            if (_logger.IsDebugEnabled)
                _logger.Debug(logEntry);
        }

        private void SwitchOffLogTargets(LogTarget targets)
        {
            var appenders = _logger.Logger.Repository.GetAppenders().ToList();

            if (!targets.HasFlag(LogTarget.Database))
            {
                var db = appenders.FirstOrDefault(piA => piA is AdoNetAppender);
                if (db != null)
                    ((AdoNetAppender)db).AddFilter(new DenyAllFilter());
            }

            if (!targets.HasFlag(LogTarget.TextFile))
            {
                var file = appenders.FirstOrDefault(piA => piA is RollingFileAppender);
                if (file != null)
                    ((RollingFileAppender)file).AddFilter(new DenyAllFilter());
            }

            if (!targets.HasFlag(LogTarget.Trace))
            {
                var trace = appenders.FirstOrDefault(piA => piA is AspNetTraceAppender);
                if (trace != null)
                    ((AspNetTraceAppender)trace).AddFilter(new DenyAllFilter());
            }
        }
    }

Test

Ok, now we have a sample console program to test the logging library with the pre-defined and custom log classes.

C#
class Program
 {
     static void Main(string[] args)
     {
         var log = new Logger();
         log.Warn(new MessageLogEntry() { Message = "Jut a Warn" });
         log.Info(new ConsoleLogEntry() { Console = "Info from console" });
         try
         {
             throw new ApplicationException("just throwing an error");
         }
         catch (Exception ex)
         {
             log.Error(new ErrorLogEntry() { Error = ex });
         }
     }
 }

The result as in the text file.

C#
{
  "Message": "Jut a Warn",
  "Level": "Warn",
  "TimeStamp": "2016-08-20T13:40:06.5447873+02:00",
  "MethodInfo": {
    "ClassName": "ConsoleApplication1.Program",
    "Name": "Main",
    "Signature": "Void Main(System.String[])"
  },
  "LogEntryType": "Contracts.Log.MessageLogEntry, Contracts, Version=1.0.0.0, 
                   Culture=neutral, PublicKeyToken=null"
}
{
  "Console": "Info from console",
  "Level": "Info",
  "TimeStamp": "2016-08-20T13:40:12.8315532+02:00",
  "MethodInfo": {
    "ClassName": "ConsoleApplication1.Program",
    "Name": "Main",
    "Signature": "Void Main(System.String[])"
  },
  "LogEntryType": "Contracts.CustomLog.ConsoleLogEntry, Contracts, Version=1.0.0.0, 
                   Culture=neutral, PublicKeyToken=null"
}
{
  "Error": {
    "ClassName": "System.ApplicationException",
    "Message": "just throwing an error",
    "Data": null,
    "InnerException": null,
    "HelpURL": null,
    "StackTraceString": "   at ConsoleApplication1.Program.Main(String[] args) in 
        C:\\Prabu\\Lab\\FlexibleLoggingLog4Net - Copy\\ConsoleApplication1\\Program.cs:line 20",
    "RemoteStackTraceString": null,
    "RemoteStackIndex": 0,
    "ExceptionMethod": "8\nMain\nConsoleApplication1, Version=1.0.0.0, 
     Culture=neutral, PublicKeyToken=null\nConsoleApplication1.Program\nVoid Main(System.String[])",
    "HResult": -2146232832,
    "Source": "ConsoleApplication1",
    "WatsonBuckets": null
  },
  "Level": "Error",
  "TimeStamp": "2016-08-20T13:40:12.8705512+02:00",
  "MethodInfo": {
    "ClassName": "ConsoleApplication1.Program",
    "Name": "Main",
    "Signature": "Void Main(System.String[])"
  },
  "LogEntryType": "Contracts.Log.ErrorLogEntry, Contracts, Version=1.0.0.0, 
   Culture=neutral, PublicKeyToken=null"
} 

As you see here, the log is saved as json format which is useful in many ways.

Finishing

I hope this is useful for some who are looking for a logging example code. Please download the source code (you need a Visual Studio 2015 to open the solution file. If you don't have it, you can use other versions as well, but you need to copy the files manually).

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)