First, you need to add a table to the database that will store unexpected application errors. Execute the SQL statement in Listing 4-1 against the Northwind database in the Query Analyzer.
Listing 4-1: Creating the application_errors Table
CREATE TABLE application_errors ( error_id int IDENTITY (1, 1) PRIMARY KEY NOT NULL, username varchar(50) NOT NULL, error_message varchar(200) NOT NULL, error_time smalldatetime NOT NULL, stack_trace varchar(4000) NULL)
I come from the database design camp that advocates using a surrogate primary key on every table. I also like to make each of the primary keys an identity field with a seed of one and an increment of one. There are many data
Surrogate keys are artificial keys that have no relationship to the data within the table. Natural keys are keys created from the data within the table. Natural keys are usually multiple column keys that can be unwieldy. Surrogate keys are mandatory in dimensional database design (the type of model used for data warehouses).
Next you need to create the stored procedure through which information will be saved to this table. Execute the SQL in Listing 4-2 to create the stored procedure.
Listing 4-2: The usp_application_errors_save Stored Procedure
CREATE PROCEDURE usp_application_errors_save @user_name varchar(50), @error_message varchar(200), @error_time smalldatetime, @stack_trace varchar(4000) AS INSERT INTO application_errors (username, error_message, error_time, stack_trace) VALUES (@user_name, @error_message, @error_time, @stack_trace)
You now have the ability to save your application errors in the database. It is time to actually create the classes responsible for capturing and saving errors.
These classes have dual responsibilities. The first responsibility is to send error information to the data-centric business object so that the error can be logged to the database. The second responsibility is to store the data in a custom Windows Event Log, which your class will also be responsible for creating. As part of this responsibility, it also retrieves errors from the event log, formats them in a file, and sends an e-mail to technical support with the contents of the event log. It then clears the event log. Although this might seem like a lot of work, all of this is generic enough to be reusable on any application with no modifications.
First you need to create the data-centric business object to pass the data to this stored procedure. But before you do that, you need to create an interface to the LogErrorDC class, just as with any other object that needs to communicate across a remoting channel. Add the following interface to the Interfaces.vb code module in the NorthwindShared project:
Public Interface ILogError Sub Save(ByVal exc As Exception) End Interface
Although the class you are creating only allows the application to store the error, it is a simple matter to extend the interface and the classes so that the application can read from this log as well. However, this ability is not something that should be available to most applications. This type of error log is for development and maintenance people only.
In the NorthwindDC project, add a class module and call it LogErrorDC . Add the following code to the header of the LogErrorDC code module:
Imports System.Data.SqlClient Imports System.Configuration Imports NorthwindTraders.NorthwindShared.Interfaces
Listing 4-3: The LogErrorDC Class
Public Class LogErrorDC Inherits MarshalByRefObject Implements ILogError Public Sub Save(ByVal exc As Exception) Implements ILogError.Save Dim strCN As String = _ ConfigurationSettings.AppSettings("Northwind_DSN") Dim cn As New SqlConnection(strCN) Dim cmd As New SqlCommand() cn.
Open() With cmd .Connection = cn .CommandType = CommandType.StoredProcedure .CommandText = "usp_application_errors_save" .Parameters.Add("@user_name", _ System.Security.Principal.WindowsIdentity.GetCurrent. Name) .Parameters.Add("@error_message", exc.Message) .Parameters.Add("@error_time", Now) If exc.StackTrace = "" Then .Parameters.Add("@stack_trace", DBNull.Value) Else .Parameters.Add("@stack_trace", exc.StackTrace) End If End With cmd.ExecuteNonQuery() cmd = Nothing cn.Close() cn = Nothing End Sub End Class
Notice again that this class inherits from the MarshalByRef class because you need to create a proxy object on the local machine when this method is called. You implement the ILogError class by implementing its only subroutine, Save.
The Exception class implements the ISerializable interface. This means you can pass exceptions over the network with no additional work necessary. However, this does not apply to classes that inherit from the Exception class. If you want to be able to pass custom exceptions across the network, they will also need to implement the ISerializable interface.
The Save routine is a straightforward call to the database to pass your exception information. One call you have not seen before is the following:
This simply retrieves the name of the user currently logged onto the machine. But, because you are actually on a remote machine, you might think it would retrieve the name of the
If exc.StackTrace = "" Then .Parameters.Add("@stack_trace", DBNull.Value) Else .Parameters.Add("@stack_trace", exc.StackTrace) End If
Even though you have set the stack_trace column in the database to nullable, you cannot pass in an empty string value or else SQL Server returns an error saying that it expected a value. Therefore, if the value can be an empty string, you need to check for it and pass in the value of DBNull. For some reason SQL Server recognizes this, but not an empty string.
The last thing you need to do is to modify the web.config file. Add another wellknown tag to reference the LogErrorDC class (below the existing wellknown tag) as
<wellknown mode="Singleton" type="NorthwindTraders.NorthwindDC.LogErrorDC,_NorthwindDC" objectUri="LogErrorDC.rem"/>
Now you need to add the class that is going to be responsible for the bulk of the work.
In the NorthwindUC project, add a class module called
. This class
Listing 4-4: The LogError Class
Option Explicit On Option Strict On Imports NorthwindTraders.NorthwindShared.Interfaces Namespace ErrorLogging Public Class LogError Private Const LISTENER As String = "LogErrorDC.rem" Public Sub New() End Sub Public Sub New(ByVal exc As Exception) LogException(exc) End Sub Public Sub LogException(ByVal exc As Exception) Dim objILog As ILogError objILog = CType(Activator.GetObject(GetType(ILogError), _ AppConstants.REMOTEOBJECTS & LISTENER), ILogError) objILog.Save(exc) objILog = Nothing End Sub End Class End Namespace
The empty constructor is available in case you want to declare an object before you send it the exception. The second constructor is so that you can pass the exception at object instantiation time, and finally the LogException routine
Next you need to create the class that will handle the Windows Event Log and all of the other overhead of logging an error locally.
This class is responsible for the bulk of your error logging code. Your LogErrorEvent class is going to use the Singleton pattern because you only need to ever have one instance of this object in memory. Your class raises two events: one to inform the interface that an error was logged and one to
Listing 4-5: The LogErrorEvent Class
Public Class LogErrorEvent Private Shared mobjLogErrEvent As LogErrorEvent Public Event ErrorLogged() Public Event ErrorsCleared() Private Const EVENTLOGNAME As String = "Northwind" Private Const ERRORFILENAME As String = "NorthwindErrors.xml" Public Shared Function getInstance() As LogErrorEvent If mobjLogErrEvent Is Nothing Then mobjLogErrEvent = New LogErrorEvent() End If Return mobjLogErrEvent End Function Protected Sub New() End Sub End Class
Next, you need to add code to create your custom event log if it does not exist. Add the following code to the constructor:
Dim objEventLog As New EventLog() If Not objEventLog.Exists(EVENTLOGNAME) Then objEventLog.CreateEventSource(EVENTLOGNAME, EVENTLOGNAME) End If
Here you create a new EventLog object that gives you a reference to the Windows Event Log. Then you check to see if the Northwind event log exists. If it does not exist, you create the event log and specify that the application that is the source of events in this log is the Northwind application. This object will be checked every time the application throws the first error that needs to be stored in the event log, so you are always sure that the event log exists.
Although the event log provides the ability to store a lot of information about the event that was logged, you are going to largely ignore this capability. Instead, you are going to store the information about your errors in an Extensible Markup Language (XML) format in the message section of the event log. There are several reasons why you are going to do this:
It is easier to read the information about the exception.
It is easier to manipulate given .NET's built-in support for XML.
It is easier to serialize into a file.
You can pick out specific pieces of information quickly.
To be able to do this, you need to create a structure you can use to store your exception information. Create the following structure in the LogError code module as part of the ErrorLogging namespace (but outside of other classes):
<Serializable()> Public Structure structLoggedError Public UserName As String Public ErrorTime As Date Public Source As String Public Message As String Public StackTrace As String End Structure
Now that you have your structure, and knowing that you are going to
The System.Xml.Serialization namespace contains several libraries to help you serialize and
The System.IO namespace contains a class library called StringWriter . This class inherits from the System.IO.TextWriter class, which is an object that can be passed to the Serialize method of the XML class. But, because you want to serialize the data to a string so that you can write the string to the event log, you have to take a roundabout way to get there. You need to serialize the data to a StringBuilder object, which you can then turn into a string.
Add the routine in Listing 4-6 to the LogErrorEvent class and then examine it.
Listing 4-6: The LogErr Method
Public Sub LogErr(ByVal exc As Exception) Dim objEventLog As New EventLog() Dim objErr As structLoggedError Dim xmlSer As New XmlSerializer(GetType(structLoggedError)) Dim strB As New System.Text.StringBuilder() Dim txtWriter As New StringWriter(strB) Dim strText As String With objErr .UserName = System.Security.Principal.WindowsIdentity.GetCurrent.Name .ErrorTime = Now .Source = exc.Source .Message = exc.Message .StackTrace = exc.StackTrace End With xmlSer.Serialize(txtWriter, objErr) strText = strB.ToString objEventLog.WriteEntry(EVENTLOGNAME, strText, EventLogEntryType.Error) objEventLog = Nothing RaiseEvent ErrorLogged() End Sub
Because this code introduces a couple of new things, let's examine each block of code:
Dim objEventLog As New EventLog() Dim objErr As structLoggedError Dim xmlSer As New XmlSerializer(GetType(structLoggedError)) Dim strB As New System.Text.StringBuilder() Dim txtWriter As New StringWriter(strB) Dim strText As String
The first line retrieves a reference to the Windows Event Log. The second line declares a variable for your structure, which holds your error information. The third line declares the XmlSerializer that accepts the type of object you will be serializing. The XmlSerializer needs this information ahead of time so that it
The StringBuilder class is a useful class for a variety of reasons. One reason is how it
Dim strName as String = John strName += Smith
It looks as though I have simply added the word
onto the end of the string variable called
. But, if strings are immutable, then that is not what really
A StringBuilder object serves as the data store for the StringWriter. That means you can serialize a structure using the Serialize method of the Xml class and store that data in a StringWriter object. You can then take the StringBuilder object that is storing that information and convert it to a true string object, as you can see in the following code from the middle of Listing 4-6:
xmlSer.Serialize(txtWriter, objErr) strText = strB.ToString objEventLog.WriteEntry(EVENTLOGNAME, strText, EventLogEntryType.Error)
Here, you serialize the data to the txtWriter object, which is of type StringWriter and then you can call the ToString method of the StringBuilder object to return the contents of the StringBuilder object in a string format. You then take this information and store it in the Northwind event logs message property.
The event log can store a limited amount of information. Figure 4-1 shows the properties page of the Windows Application Event Log. Note the maximum log size of 512KB. Although you can configure this manually, you should leave the settings at their defaults.
Now you can store your error in the event log. Next you need to be able to retrieve this information from the event log.
Add the method in Listing 4-7 to the LogErrorEvent class.
Listing 4-7: The RetrieveErrors Method
Public Function RetrieveErrors() As structLoggedError() Dim objEL As New EventLog(EVENTLOGNAME) Dim objEntry As EventLogEntry Dim errArray(objEL.Entries.Count - 1) As structLoggedError Dim i As Integer = 0 Dim xmlSer As New XmlSerializer(GetType(structLoggedError)) For Each objEntry In objEL.Entries Dim txtReader As New StringReader(objEntry.Message) errArray(i) = CType(xmlSer.Deserialize(txtReader), structLoggedError) i += 1 txtReader = Nothing Next objEL = Nothing Return errArray End Function
Let's examine this code so you can see exactly what is happening. The following line retrieves a reference to your specific event log:
Dim objEL As New EventLog(EVENTLOGNAME)
This next line creates an array variable of type structLoggedError to store the XML information from the event log. You dimension it to the number of errors stored in the event log:
Dim errArray(objEL.Entries.Count - 1) As structLoggedError
The following block of code
For Each objEntry In objEL.Entries Dim txtReader As New StringReader(objEntry.Message) errArray(i) = CType(xmlSer.Deserialize(txtReader), structLoggedError) i += 1 txtReader = Nothing Next
Finally, you need to add the code to send these errors to the technical support personnel. To do this, you need to get a reference to the System.Web namespace, which is not available by default when you create a Windows Forms application. To get this reference, right-click the NorthwindUC project and select Add Reference. Then select the System.Web.dll entry from the .NET tab. Once you have done that, add an Imports line at the top of the LogError code module, as shown here:
You also need to add one additional class. For .NET to be able serialize the XML directly to a file, it needs to have a class that it can serialize—just passing it an Array type will cause a runtime error. So let's add a simple one-line class that you can use to serialize your errors to the LogError code module:
Public Class CustomErrors Public ErrArray() As structLoggedError End Class
Now add the method in Listing 4-8 to the LogErrorEvent class.
Listing 4-8: The SendErrors Method
Public Sub SendErrors(ByVal errArray() As structLoggedError) Dim objAllErrors As New CustomErrors() Dim xmlFileSer As New XmlSerializer(GetType(CustomErrors)) Dim dirPath As Environment Dim strPath As String = _ dirPath.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) Dim fStream As New FileStream(strPath & "\" & ERRORFILENAME, _ FileMode.Create) Dim email As New Mail.MailMessage() Dim emailAttach As Mail.MailAttachment objAllErrors.ErrArray = errArray xmlFileSer.Serialize(fStream,Objallerrors) fStream.Close() xmlFileSer = Nothing emailAttach = New Mail.MailAttachment(strPath & "\" & ERRORFILENAME) With email .From = "email@example.com" .To = "firstname.lastname@example.org" .Subject = "Northwind Error Log" .Priority = Web.Mail.MailPriority.High .Attachments.Add(emailAttach) End With Mail.SmtpMail.Send(email) Kill(strPath & "\" & ERRORFILENAME) ClearErrors() End Sub
The first line of this code declares your CustomErrors class, which you will serialize to an XML file. Then you tell the XmlSerializer what type of object it will be serializing:
Dim objAllErrors As New CustomErrors() Dim xmlFileSer As New XmlSerializer(GetType(CustomErrors))
The following line gets the location of the place where you will store your temporary file:
Dim strPath As String = _ Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)
The Environment class is a very special, very cool class that has a
See the MSDN documentation for the Environment class. This class can provide a lot of help in dealing with the local environment, and it is one of the better-documented classes.
The GetFolderPath method can retrieve a variety of special Windows folders. (I chose this because many companies lock down user machines, but the folders in the Documents and Settings folder for the current user are
Next, you declare a FileStream variable that holds a reference to your temporary file. Notice that the file is opened using FileMode.Create. This destroys a file with the same name if it exists and creates a new one. The MailMessage class is the class that actually constructs the e-mail, and the MailAttachment class stores a reference to the file that you will be attaching. The MailMessage class can store a collection of MailAttachment objects so that you can attach multiple files to an e-mail:
Dim fStream As New FileStream(strPath & "\" & ERRORFILENAME, FileMode.Create) Dim email As New Mail.MailMessage() Dim emailAttach As Mail.MailAttachment
The next block of code stores your array of messages in a CustomErrors variable. The object is then serialized to the temporary file and the file stream is closed. Finally, you create an e-mail attachment and attach the temporary file that contains your XML serialized errors:
objAllErrors.ErrArray = errArray xmlFileSer.Serialize(fStream, objAllErrors) fStream.Close() xmlFileSer = Nothing emailAttach = New Mail.MailAttachment(strPath & "\" & ERRORFILENAME)
Lastly, you set the e-mail properties, attach the attachments, and send the e-mail. To send the e-mail, you call the Send method of the SmtpMail object. This is a shared method, so the object does not have to be instantiated. Finally, you delete the temporary file and clear the errors from the error log:
With email .From = "email@example.com" .To = "firstname.lastname@example.org" .Subject = "Northwind Error Log" .Priority = Web.Mail.MailPriority.High .Attachments.Add(emailAttach) End With Mail.SmtpMail.Send(email) Kill(strPath & "\" & ERRORFILENAME) ClearErrors()
The From properties and To properties must have valid e-mail addresses. If only the To property has a valid e-mail address, the e-mail still will not reach its intended recipient. This may be something that is network specific, but in all of my tests (on different networks) I have found this to be true.
To send e-mail by the SmtpMail.Send method, you use the CDO objects on a user's machine. There are some known issues with this because not everyone has the CDO objects. At this time, the only way to guarantee that this will work is if Internet Information Server (IIS) is installed on the machine or the CDO objects have been installed in some other manner. To guarantee this works in an enterprise, it may be a better choice to write a small routine that automates Microsoft Outlook or whatever the standard e-mail client is. Note that if you do this, you still do not have to change the routine, just plug in a different class to this method!
You will notice that the last line of code calls the ClearErrors method, which you have not yet created, so let's go ahead and do that now and then test this class. Add the following code to the LogErrorEvent class:
Public Sub ClearErrors() Dim objEventLog As New EventLog(EVENTLOGNAME) objEventLog.Clear() objEventLog = Nothing RaiseEvent ErrorsCleared() End Sub
This routine simply gets a reference to our custom event log, calls the clear method on it, and raises the appropriate event. There is nothing to it!
Now that you have a working class, let's try it. To do this you will create a small console application and copy your code over to it. Open a new instance of the .NET Integrated Development Environment (IDE) and select New Ø Project from the main menu. Select a Console Application project and call it EventLogTest . The code window you are presented with looks like the following:
Module Module1 Sub Main() End Sub End Module
Copy all of the classes and structures in the ErrorLogging namespace, except the LogError class, to this code module and paste it after the End Module statement. Next, copy the Imports statement from the head of the LogError code module in the NorthwindUC project to the console project (do not copy the Imports statement for the NorthwindUC namespace). Finally, add a reference to the System.Web namespace in your console application.
Make sure to change the From and To e-mail address (preferably to an e-mail address you have access to so that you can see the results). They can be the same value.
Now, modify the Sub Main procedure so that it looks like the procedure in Listing 4-9.
Listing 4-9: The Sub Main Procedure
Sub Main() Console.WriteLine("Press the enter key to start the test") Console.ReadLine() Console.WriteLine("Throwing an error...") Try Throw New ApplicationException("My error") Catch exc As Exception Dim objEL As LogErrorEvent Dim errArray() As structLoggedError objEL = objEL.getInstance objEL.LogErr(exc) Console.WriteLine("Error has been logged. Press the enter key to " _ & "continue.") Console.ReadLine() Console.WriteLine("Reading error log...") errArray = objEL.RetrieveErrors() Console.WriteLine("Errors Retrieved. Press the enter key to " _ & "continue.") Console.ReadLine() Console.WriteLine("Sending Errors...") objEL.SendErrors(errArray) Console.WriteLine("Errors Sent. Press the Enter Key To Exit " _ & "Application.") Console.ReadLine() End Try End Sub
This code throws an error that is caught by the Catch block. Then a reference to the LogErrorEvent class is made and the error is passed to the LogErr method. Next you retrieve the error from the event log and then send the error. The reason why there are so many Readline statements is because these pause the application execution until you press a key. At this point, run the application and do the following:
When the error has been logged, check the event log (shown in Figure 4-2).
Figure 4-2: Event Viewer with the Northwind custom error log
Open the error message and examine it (shown in Figure 4-3).
Figure 4-3: Your custom-generated event log entry
Press a key to continue the application until the end.
Examine the attachment in the e-mail you receive (shown in Figure 4-4).
Figure 4-4: Custom error log file sent by e-mail
If you already had the event log open, you may have to refresh the window to see the error.
The important things here are the Message and the StackTrace. The StackTrace is extremely helpful in finding and debugging errors. If you go back and check the console application, line 16 is the line that