Pages

Advertisement

Wednesday, July 11, 2007

Building a Logging Object in .NET

Welcome to the next installment of the .NET Nuts & Bolts column. In this column we talk about how you can use some of the classes in the .NET Framework to log information to different locations. The locations of choice in this article will be to a file and the windows event log.

Even though many programmers deny it about their code, or try using some name such as a "feature" to make it sound better, code will sometimes contain defects and errors. Whether that error is caused by a flawed design, bad specifications, incorrect formula, or the fact that you worked on it so long without sleep you were seeing things, you will eventually need a way to capture error messages to some location. In addition to error messages, it may also be advantageous to capture information about your code for performance or some other reason.

This brings me to the focus of this column. We'll explore how to build an object that can log information to a file or the event log. A database is another logical location to log information, but since I've covered some database stuff in the past, for this article I'll focus on accessing files and the event log.

Designing the Logging Object

Step one is to identify the problem. We want to build some type of logger object that can log to multiple locations. There are plenty of ways to approach this. One way would be to create different classes for each type of log we want to use then just create and use the desired class in our code. The downside to this approach is that it locks us into logging to the same place all of the time, or we have to put a bunch of duplicated logic in our code to decide which object to create and use. A better way to handle this, at least in my mind, is to have a single logger object with which to interact. This logger can be configurable to which location it will record information. Based on how it is configured at run time, it will create and log messages to the appropriate location.

Now we've decided on a single object to interact with when logging, and we are going to allow the object to log to different locations. Wouldn't it be nice to have components that can be added to the logger or taken out rather than have to add a bunch of code to or take away code from the logger? In order to accomplish this, we'll create an abstract class that defines the methods our individual log objects should contain. If all of the individual logging classes our logger uses all implement the same interface it makes our logger relatively simple.

The last remaining item is to define what functionality our logger and its individual log components should have. To come up with our definition we need to think about what methods and properties we want our logger to provide. It stands to reason since the primary purpose of our logger is for error logging we should have a method that accepts an exception as an input parameter. It also stands to reason that if we plan to use our logger to record informational errors as well that we'll need another method that accepts a generic message along with an indicator of whether the message is an error or some other type of informational or warning message that will control how the logging occurs.

First we'll define a base class for our log objects to ensure that our logger object can interact with each of them. After the base class is defined, we'll create the individual classes that will handle logging to a file and the event log, and then we'll tie it all together by creating the logger component.

Sample Abstract Log Class

The following code outlines a base class for the log objects with which our logger will interact.

using System;
namespace CodeGuru.ErrorLog.Logs
{
/// <remarks>
/// Abstract class to dictate the format for the logs that our
/// logger will use.
/// </remarks>
public abstract class Log
{
/// <value>Available message severities</value>
public enum MessageType
{
/// <value>Informational message</value>
Informational = 1,
/// <value>Failure audit message</value>
Failure = 2,
/// <value>Warning message</value>
Warning = 3,
/// <value>Error message</value>
Error = 4
}

public abstract void RecordMessage(Exception Message,
MessageType Severity);

public abstract void RecordMessage(string Message,
MessageType Severity);
}
}

Creating the Logging Object to Write to a File

Reading and writing to files is accomplished through classes in the System.IO namespace. The FileStream object is used to read or write files. The StreamReader and StreamWriter are used in conjunction with the FileStream to perform the actual action. Below we'll create an object that extends our Log base class and that uses the FileStream and the StreamWriter to write a message to a file.

Sample File Logging Class

using System;
using System.IO;
using System.Text;
namespace CodeGuru.ErrorLog.Logs
{
/// <remarks>
/// Log messages to a file location.
/// </remarks>
public class FileLog : Log
{
// Internal log file name value
private string _FileName = "";
/// <value>Get or set the log file name</value>
public string FileName
{
get { return this._FileName; }
set { this._FileName = value; }
}

// Internal log file location value
private string _FileLocation = "";
/// <value>Get or set the log file directory location</value>
public string FileLocation
{
get { return this._FileLocation; }
set
{
this._FileLocation = value;
// Verify a '\' exists on the end of the location
if( this._FileLocation.LastIndexOf("\\") !=
(this._FileLocation.Length - 1) )
{
this._FileLocation += "\\";
}
}
}

/// <summary>
/// Constructor
/// </summary>
public FileLog()
{
this.FileLocation = "C:\\";
this.FileName = "mylog.txt";
}

/// <summary>
/// Log an exception.
/// </summary>
/// <param name="Message">Exception to log. </param>
/// <param name="Severity">Error severity level. </param>
public override void RecordMessage(Exception Message,
Log.MessageType Severity)
{
this.RecordMessage(Message.Message, Severity);
}

/// <summary>
/// Log a message.
/// </summary>
/// <param name="Message">Message to log. </param>
/// <param name="Severity">Error severity level. </param>
public override void RecordMessage(string Message,
Log.MessageType Severity)
{
FileStream fileStream = null;
StreamWriter writer = null;
StringBuilder message = new StringBuilder();

try
{
fileStream = new FileStream(this._FileLocation +
this._FileName, FileMode.OpenOrCreate,
FileAccess.Write);
writer = new StreamWriter(fileStream);

// Set the file pointer to the end of the file
writer.BaseStream.Seek(0, SeekOrigin.End);

// Create the message
message.Append(System.DateTime.Now.ToString())
.Append(",").Append(Message);

// Force the write to the underlying file
writer.WriteLine(message.ToString());
writer.Flush();
}
finally
{
if( writer != null ) writer.Close();
}
}
}
}

Creating the Logging Object to Write to the Event Log

The .NET Framework includes classes for interfacing with the Windows Event Log. The classes are located in the System.Diagnostics namespace. The classes allow you to write to any of the existing log locations, or it allows you to create your own Event Log. There are different message types that can be logged to the event log, which is where I got the definition of the types that I used (informational, failure, warning, and error).

It is important to note that the Event Log was designed to hold information about normal application errors. It is designed to hold items of a more catastrophic system level. In the course of normal application errors it should be logged to another location such as a file or a database.

Below we'll create an object that extends our Log base class and that uses the Event log classes in the System.Diagnostics namespace to write a message to the event log.

Sample Event Logging Class

using System;
using System.Diagnostics;
using System.Text;
namespace CodeGuru.ErrorLog.Logs
{
/// <remarks>
/// Log messages to the Windows Event Log.
/// </remarks>
public class EventLog : Log
{
// Internal EventLogName destination value
private string _EventLogName = "";
/// <value>Get or set the name of the destination log</value>
public string EventLogName
{
get { return this._EventLogName; }
set { this._EventLogName = value; }
}

// Internal EventLogSource value
private string _EventLogSource;
/// <value>Get or set the name of the source of entry</value>
public string EventLogSource
{
get { return this._EventLogSource; }
set { this._EventLogSource = value; }
}

// Internal MachineName value
private string _MachineName = "";
/// <value>Get or set the name of the computer</value>
public string MachineName
{
get { return this._MachineName; }
set { this._MachineName = value; }
}

/// <summary>
/// Constructor
/// </summary>
public EventLog()
{
this.MachineName = ".";
this.EventLogName = "MyEventLog";
this.EventLogSource = "MyApplication";
}

/// <summary>
/// Log an exception.
/// </summary>
/// <param name="Message">Exception to log.</param>
/// <param name="Severity">Error severity level.</param>
public override void RecordMessage(Exception Message,
Log.MessageType Severity)
{
this.RecordMessage(Message.Message, Severity);
}

/// <summary>
/// Log a message.
/// </summary>
/// <param name="Message">Message to log.</param>
/// <param name="Severity">Error severity level.</param>
public override void RecordMessage(string Message,
Log.MessageType Severity)
{
StringBuilder message = new StringBuilder();
System.Diagnostics.EventLog eventLog =
new System.Diagnostics.EventLog();

// Create the source if it does not already exist
if( !System.Diagnostics.EventLog.SourceExists(
this._EventLogSource) )
{
System.Diagnostics.EventLog.CreateEventSource(
this._EventLogSource, this._EventLogName);
}
eventLog.Source = this._EventLogSource;
eventLog.MachineName = this._MachineName;

// Determine what the EventLogEventType should be
// based on the LogSeverity passed in
EventLogEntryType type = EventLogEntryType.Information;

switch(Severity.ToString().ToUpper())
{
case "INFORMATIONAL":
type = EventLogEntryType.Information;
break;
case "FAILURE":
type = EventLogEntryType.FailureAudit;
break;
case "WARNING":
type = EventLogEntryType.Warning;
break;
case "ERROR":
type = EventLogEntryType.Error;
break;
}
message.Append(Severity.ToString()).Append(",").Append(
System.DateTime.Now).Append(",").Append(Message);
eventLog.WriteEntry(message.ToString(), type);
}
}
}

Building the Logger

Now to tie it all together we need to create our logger object to interact with the individual log classes. The sample code is given below. An item of note is how the set accessor method of the LogType property results in the appropriate log object being created. The actual RecordMessage methods do nothing more than call the appropriate method on the desired log class.

The actual object used to do the logging is declared of type Logs.Log. This will allow us to create and use any objects that extend Log as its base class.

Sample Logging Class

using System;
namespace CodeGuru.ErrorLog
{
/// <remarks>
/// Managing class to provide the interface for and control
/// application logging. It utilizes the logging objects in
/// ErrorLog.Logs to perform the actual logging as configured.
/// </remarks>
public class Logger
{
/// <value>Available log types.</value>
public enum LogTypes
{
/// <value>Log to the event log.</value>
Event = 1,
/// <value>Log to a file location.</value>
File = 2
}

// Internal logging object
private Logs.Log _Logger;

// Internal log type
private LogTypes _LogType;
/// <value></value>
public LogTypes LogType
{
get { return this._LogType; }
set
{
// Set the Logger to the appropriate log when
// the type changes.
switch( value )
{
case LogTypes.Event:
this._Logger = new Logs.EventLog();
break;
default:
this._Logger = new Logs.FileLog();
break;
}
}
}

/// <summary>
/// Constructor
/// </summary>
public Logger()
{
this.LogType = LogTypes.File;
}

/// <summary>
/// Log an exception.
/// </summary>
/// <param name="Message">Exception to log.</param>
/// <param name="Severity">Error severity level.</param>
public void RecordMessage(Exception Message,
Logs.Log.MessageType Severity)
{
this._Logger.RecordMessage(Message, Severity);
}


/// Log a message.
/// </summary>
/// <param name="Message">Message to log.</param>
/// <param name="Severity">Error severity level.</param>
public void RecordMessage(string Message,
Logs.Log.MessageType Severity)
{
this._Logger.RecordMessage(Message, Severity);
}
}
}

Using the Logger

Now that we've built our logger object that we'll use to do all of the logging and its supporting log objects, let's give it a try. The example below should result in a file c:\mylog.txt being written and an MyEventLog being added to the Windows Event Log.

Sample Logger Usage

Logger logger = new Logger();

// Log to a file (default settings)
logger.RecordMessage("Testing", Logs.Log.MessageType.Error);
logger.RecordMessage(new Exception("My test exception"),
Logs.Log.MessageType.Error);

// Log to the event log
logger.LogType = Logger.LogTypes.Event;
logger.RecordMessage("Testing", Logs.Log.MessageType.Error);
logger.RecordMessage(new Exception("My test exception"),
Logs.Log.MessageType.Error);

Possible Enhancements

Now we have an object that can be used for logging information and errors. There are all sorts of enhancements that could make this even more valuable. Here are some ideas that you can consider for yourself.

No comments:

Post a Comment