How to implement a simple logger in C#

It's easy to create a custom logging framework to log your .Net application's errors and events to a flat file, a database, or the event log

Implement a simple logger in C#
Kungfuman (CC BY-SA 3.0)

You will often want to log events or errors as they occur in your .Net application. To do this, you could take advantage of one of the many popular logging frameworks available, or you could design and develop your own logging framework. In this article we’ll see how we can design and develop our own logging framework with ease, and walk through the steps to build a simple logger in C#.

First off, you will need to understand the log targets—the various places where the data could be logged. Let’s assume that we will log the data to flat files, a database, and the event log. The following enumeration defines the log targets we would use in this simple framework.

public enum LogTarget
    {
        File, Database, EventLog
    }

C# logger classes

The next step is to design and implement the classes. We will use three distinct classes—namely, FileLogger, DBLogger, and EventLogger—to log data to a file, a database, and the event log respectively. All of these classes should inherit the abstract base class named LogBase. Here is how these classes are organized.

    public abstract class LogBase
    {
        public abstract void Log(string message);
    }

    public class FileLogger : LogBase
     {
        public string filePath = @”D:\IDGLog.txt”;
        public override void Log(string message)
        {
            using (StreamWriter streamWriter = new StreamWriter(filePath))
            {
                streamWriter.WriteLine(message);
                streamWriter.Close();
            }           
        }
    }

public class DBLogger : LogBase
    {
        string connectionString = string.Empty;
        public override void Log(string message)
        {
            //Code to log data to the database
        }
    }

    public class EventLogger: LogBase
    {
        public override void Log(string message)
        {
            EventLog eventLog = new EventLog(“”);
            eventLog.Source ="IDGEventLog";
            eventLog.WriteEntry(message);
        }
    }                                

I have left the DBLogger class incomplete. I will leave it to you to fill in the appropriate code to log your messages to the database.

As you can see, all three of the classes – FileLogger, EventLogger, and DBLogger – extend the abstract base class LogBase. The abstract base class LogBase declares the abstract method called Log(). The Log() method accepts a string as the parameter; this string is what will be logged to a file or a database or the event log. 

The C# LogHelper class

Now let’s create a helper class that can be used to invoke the respective logger based on the parameter passed. This helper class will be used to simplify the calls to the Log() method in each of the logger classes. The following code snippet illustrates this helper class.

public static class LogHelper
    {
        private static LogBase logger = null;
        public static void Log(LogTarget target, string message)
        {
            switch(target)
            {
                case LogTarget.File:
                    logger = new FileLogger();
                    logger.Log(message);
                    break;
                case LogTarget.Database:
                    logger = new DBLogger();
                    logger.Log(message);
                    break;
                case LogTarget.EventLog:
                    logger = new EventLogger();
                    logger.Log(message);
                    break;
                default:
                    return;
            }
        }
    }

The Log() method of the LogHelper class accepts a string and an instance of the LogTarget enumeration as parameters. It then uses a switch: case construct to determine the target where the text message will be logged.

Synchronizing calls to the C# Log method

Oops! We forgot to synchronize the calls to the respective Log() methods. To do this, we need to use the lock keyword in the Log() method of each of the logger classes and incorporate the appropriate code to synchronize those Log() methods. Refer to the LogBase class given below. We have incorporated a protected member that will be used to apply the lock in the Log() method of each of the derived classes. Here are the modified versions of these classes.

public abstract class LogBase
    {
        protected readonly object lockObj = new object();
        public abstract void Log(string message);
    }

    public class FileLogger : LogBase
    {
        public string filePath = @”D:\IDGLog.txt”;
        public override void Log(string message)
        {
            lock (lockObj)
            {
                using (StreamWriter streamWriter = new StreamWriter(filePath))
                {
                    streamWriter.WriteLine(message);
                    streamWriter.Close();
                }
            }
        }
    }

    public class EventLogger : LogBase
    {
        public override void Log(string message)
        {
            lock (lockObj)
            {
                EventLog m_EventLog = new EventLog(“”);
                m_EventLog.Source ="IDGEventLog";
                m_EventLog.WriteEntry(message);
            }
        }
    }

    public class DBLogger : LogBase
    {
        string connectionString = string.Empty;
        public override void Log(string message)
        {
            lock (lockObj)
            {
                //Code to log data to the database
            }
        }
    }

You can now call the Log() method of the LogHelper class and pass the log target and the text message to log as parameters.

class Program
    {
        static void Main(string[] args)
        {
            LogHelper.Log(LogTarget.File, “Hello”);
        }
    }

If you ever need to log the text message to a different log target, you would simply pass the appropriate log target as a parameter to the Log() method of the LogHelper class.

There are many ways you could improve this logging framework. You could implement asynchrony and a queue so that when large numbers of messages arrive, the logger can process these messages asynchronously without having to block the current thread. You may also want to implement message criticality levels, such as informational messages, warning messages, error messages, and so on.