Introduction
Logging is something any respectable application needs to do. Whether it's to keep track of what the application is doing under the hood (for possible trouble shooting), to show a list of events that we might need to inform the user of, or simply to keep a history of events, logging is as necessary to the programming world as note taking is to a class room. If you don't do it, you might be able to get by for the moment. Just hope you never need to refer back to something that happened a few minutes, hours or days ago.
This article shows an excellent mechanism for adding logging to a C# project. It is extremely powerful and versatile, yet very simple to implement and use. It uses two very common programming patterns: the singleton and the observer.
Background
The ideas and code shown in here are not rocket science, so as long as you have at least a basic idea of object oriented programming, and have been playing around with C# for a while, I'd say you're safe.
This having been said, it is always a good idea to brush up on a couple of things if you want to get the most out of this. I would suggest reading up on patterns, and focusing on some of the more common ones such as the Singleton and Observer.
To keep it brief and concise, however, I'll delve into these two for a bit and give you an idea of what they're about.
The Singleton Pattern
In Layman's Terms ...
Probably the most widely known and one of simplest patterns, the singleton is a mechanism that ensures all objects in your process have access to a shared and single set of data and methods.
Imagine an intersection where several roads meet, and where the cars need to ensure they do not crash into each other when crossing this intersection. Assuming there were no traffic lights to guide them, we would need a police officer standing in the middle to direct traffic. This officer would be a real life approximation of a singleton.
The officer would receive visual queues from all roads, telling him which one is filling up the most with cars. With all this information, he would be able to decide which road to let the cars come through, and which roads to block.
This would be significantly more complex if there were several officers controlling the intersection, where they would all be receiving visual queues from the roads, would have to communicate with each other and then collectively decide which road to let traffic come from.
Similarly, a singleton object is something shared by all the objects in the system. For the purposes of the logging mechanism, the singleton is the one and only object to which all other objects will send the information they wish to have logged.
Not only does this centralize and simplify control of the logging mechanism, this also gives the developer an excellent way to provide uniform formatting to the log output. You can add timestamps, titles, parameter information and more in just one place. None of the objects that need information to be logged need be concerned with this, as the singleton logger takes care of it on its own.
Putting This In Code ...
When regular classes need to be used, they are instantiated as an object and then used. A singleton class, however, does not allow anyone except itself to instantiate it. This guarantees that only one copy of this object will ever run, and all objects in the program will be accessing this one copy.
To accomplish this, the logger has only one private
constructor and a private
variable of its own type. The only way to really get a hold of this object from the outside then is to instantiate the Logger
class and equate the new object to the static
handle exposed in the class. The Instance
property then checks to see if the private mLogger
object was ever created, and if it was not, this is the only place where Logger
will ever get instantiated.
class Logger
{
private static object mLock;
private static Logger mLogger = null;
public static Logger Instance
{
get
{
if (mLogger == null)
{
lock(mLock)
{
if (mLogger == null)
{
mLogger = new Logger();
}
}
}
return mLogger;
}
}
private Logger()
{
mLock = new object();
}
}
public class SomeWorkerClass
{
private Logger mLogger = Logger.Instance;
}
The Observer Pattern
In Layman's Terms Again ...
This is where things get a bit more complex, but a bit more fun as well. The observer pattern is simply a mechanism where one object (such as our police officer from the example above) has the ability to dispatch a message to one or more observers. What they do with this message is completely up to them, and in fact, the officer doesn't even know who they are or what they do. He just knows how many observers there are, and has a predefined method for giving them this information.
Going back to the traffic example: whenever the officer needs to let traffic on one road stop moving, he shows his open palm to that road's direction. He is in effect sending all traffic, which is observing him for input, a message. The traffic facing him will interpret this message and stop, and all other traffic will interpret the message and subsequently ignore it has no effect on them.
Next, he waves to traffic in another road indicating that the drivers should start moving. Again, he has sent another message, and all the drivers will receive it in the same way, but act on it in different ways. Those facing him will start moving, and all others will simply ignore the message.
So far, we have one observed subject (the officer) and several observers (the drivers). The beauty of this pattern, however, is that there could be observers of other types as well, and as long as they can receive the officer's messages in the same way as the drivers (i.e. as long as they have eye sight and are looking at him), they can act on the messages as well. A simple example would be for the officer to actually be a cadet on training, and a senior officer to be sitting in his car watching this cadet and taking notes of the stop and go messages. Again, the senior officer is observing the cadet and receiving his messages in the same way as the drivers, but is acting on them in a different manner.
In programming terms, the officer would be sending his messages to the observers via an interface. If a given object implements an interface, any other object can interact with it through the properties and methods exposed in that interface without even knowing what the true nature of the object really is.
Putting This In Code As Well ...
The first part of this pattern is the use of an interface which will allow the Logger
to dispatch new log entries to the objects observing it. The beauty of this is that those objects could be of literally any type and offer all sorts of functionality. They could be forms, file writers, database writers, etc. But the Logger
will only know that they implement this interface and will be able to communicate with them through it.
interface ILogger
{
void ProcessLogMessage(string logMessage);
}
Next is the management of these observers within Logger
. We need an array (or List<>
) to store them (technically speaking, though, we're only storing a reference to them), and a public
method by which they can be added to the array:
class Logger
{
private List<ILogger> mObservers;
private Logger()
{
mObservers = new List<ILogger>();
}
public void RegisterObserver(ILogger observer)
{
if (!mObservers.Contains(observer))
{
mObservers.Add (observer);
}
}
}
Whenever we want to implement a new observer, we just need to ensure it implements the ILogger
interface and does something meaningful when the ProcessLogMessage
method is executed. An example would be a FileLogger
object that writes the log messages to a file:
class FileLogger: ILogger
{
private string mFileName;
private StreamWriter mLogFile;
public string FileName
{
get
{
return mFileName;
}
}
public FileLogger(string fileName)
{
mFileName = fileName;
}
public void Init()
{
mLogFile = new StreamWriter(mFileName);
}
public void Terminate()
{
mLogFile.Close();
}
public void ProcessLogMessage(string logMessage)
{
mLogFile.WriteLine(logMessage);
}
}
This class would then be instantiated and passed to Logger
as follows:
public partial class Form1 : Form
{
private Logger mLogger;
private FileLogger mFileLogger;
private void Form1_Load(object sender, EventArgs e)
{
mLogger = Logger.Instance;
mFileLogger = new FileLogger(@"c:\temp\log.txt" );
mFileLogger.Init();
mLogger.RegisterObserver(mFileLogger);
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
mFileLogger.Terminate();
}
}
Putting the Patterns Together
Our logger
will use both of these patterns, and the attached sample code uses two observers: the FileLogger
mentioned above, and the actual form itself. The FileLogger
logs the messages to a file, and the form shows the messages in a textbox
control. This is obviously a simple implementation, but it could be used in more complex scenarios. We could have an observer that writes the log entries to a database table, another that concatenates all the entries and then emails the log, etc.
Whenever the form needs to have something logged, it simply executes the AddLogMessage()
on the logger and passes it the log entry:
class Logger
{
public void AddLogMessage(string message)
{
string formattedMessage = string.Format("{0} - {1}",
DateTime.Now.ToString(), message);
foreach (ILogger observer in mObservers)
{
observer.ProcessLogMessage(formattedMessage);
}
}
}
public partial class Form1 : Form
{
private void button1_Click(object sender, EventArgs e)
{
mLogger.AddLogMessage("The button was clicked.");
}
}
Using the Code
The sample project does not perform any complex operations, but showcases the logging discussed in this article. The main form has a button that, when clicked, increments a private
counter and displays the value in a text box.
Every time the counter is increased, however, the logger is informed. In turn, the logger formats the message it receives and dispatches it to all the observers for processing.
As a nice plus, the Logger
's AddLogMessage()
method is overwritten to accept an exception. If the application throws an exception and we want it properly logged, we just pass the exception to the Logger
and it extracts all the messages from the exception and inner exceptions (if applicable), puts them together, adds the stack trace, and then logs the whole thing. Very useful.
In Conclusion
I hope this article and the attached sample prove useful. If you have any suggestions for improvement I'm all ears, so post a message and let us know what you think!
History
- 17th November, 2007 – Initial post