Creating the LogError Classes


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.

The LogErrorDC Class

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 

Note

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 

Next, modify the LogErrorDC class so that it contains the code shown in Listing 4-3.

Listing 4-3: The LogErrorDC Class

start example
 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 
end example

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.

Note

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:

 System.Security.Principal.WindowsIdentity.GetCurrent.Name 

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 user who was logged onto the remote machine, not your logon. You would be right, except that in your web.config file you set the Identity Impersonate flag equal to true, so for the space of this transaction Windows believes you are the person logged onto this machine. You could use the user_name function in SQL Server to automatically add this information into the application_errors table when you add a record, but this method makes the functionality a little more flexible. The other thing to note is the If..Then statement around the stack trace variable:

 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 follows:

 <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.

The LogError Class

In the NorthwindUC project, add a class module called LogError. This class performs a simple operation: It sends an exception to the data-centric object for storage in the database. Delete the empty class declaration and add the code shown in Listing 4-4 to the LogError code module.

Listing 4-4: The LogError Class

start example
 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 
end example

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 passes the application exception to the data-centric objects.

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.

Creating the LogErrorEvent Class

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 inform the interface that the event log was cleared. You will also use constants for the event log name and the error log file so that it makes it easier to reuse this object (of course, the ultimate in reusability is to make these properties, but that is a change I will let you make). Add the class in Listing 4-5 to the ErrorLogging namespace in the LogError code module.

Listing 4-5: The LogErrorEvent Class

start example
 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 
end example

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 turn it into an XML format, you need to add another Imports statement:

 Imports System.Xml.Serialization 

The System.Xml.Serialization namespace contains several libraries to help you serialize and deserialize data to and from XML. You will also need to add one more Imports statement:

 Imports System.IO 

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.

Logging the Error

Add the routine in Listing 4-6 to the LogErrorEvent class and then examine it.

Listing 4-6: The LogErr Method

start example
 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 
end example

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 knows how to handle the data you will be passing to it.

start sidebar
Using the StringBuilder Class

The StringBuilder class is a useful class for a variety of reasons. One reason is how it manages memory. Strings, by their nature are immutable—that is, once a string is created in memory, it cannot be changed. For example, look at the following two lines of code:

 Dim strName as String = John strName += Smith 

It looks as though I have simply added the word Smith onto the end of the string variable called strName. But, if strings are immutable, then that is not what really happened here. What happens when you perform this operation is that .NET creates a temporary string variable large enough to hold the entire value of the variable plus the string you are adding to it. It places the contents of the second call into this temporary variable, destroys the strName variable, and re-creates it in the correct size. It then copies the contents of the temporary variable into the new variable named strName. Obviously this is an involved process, especially if you are performing operations on thousands of strings. The StringBuilder class can size itself dynamically without having to go through this entire process, which makes it a fast string processor.

end sidebar

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.

Note

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.

click to expand
Figure 4-1: The Windows Application Event Log properties page

Now you can store your error in the event log. Next you need to be able to retrieve this information from the event log.

Retrieving the Error Log

Add the method in Listing 4-7 to the LogErrorEvent class.

Listing 4-7: The RetrieveErrors Method

start example
 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 
end example

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 loops through the entries in the event log. First you create a new StringReader object that accepts a string. Remember that the information store for a StringReader object is a StringBuilder object. The next line deserializes the string into a structLoggedError structure:

 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 

Reporting the Errors

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:

 Imports System.Web 

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

start example
 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 = "user.name@somewhere.com"           .To = "tech.support@somewhere.com"           .Subject = "Northwind Error Log"           .Priority = Web.Mail.MailPriority.High           .Attachments.Add(emailAttach)      End With      Mail.SmtpMail.Send(email)      Kill(strPath & "\" & ERRORFILENAME)      ClearErrors() End Sub 
end example

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 multitude of uses. It can tell you everything you need to know about the system. All of the class's members are shared, so it never needs to be instantiated.

Note

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 generally not locked down.)

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 = "user.name@somewhere.com" .To = "tech.support@somewhere.com" .Subject = "Northwind Error Log" .Priority = Web.Mail.MailPriority.High .Attachments.Add(emailAttach) End With Mail.SmtpMail.Send(email) Kill(strPath & "\" & ERRORFILENAME) ClearErrors() 

Note

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.

Caution

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!

Testing the LogErrorEvent Class

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.

Note

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

start example
 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 
end example

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:

  1. When the error has been logged, check the event log (shown in Figure 4-2).

    click to expand
    Figure 4-2: Event Viewer with the Northwind custom error log

  2. Open the error message and examine it (shown in Figure 4-3).

    click to expand
    Figure 4-3: Your custom-generated event log entry

  3. Press a key to continue the application until the end.

  4. Examine the attachment in the e-mail you receive (shown in Figure 4-4).

    click to expand
    Figure 4-4: Custom error log file sent by e-mail

Note

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 threw the error. This makes it simple to find and fix errors.




Building Client/Server Applications with VB. NET(c) An Example-Driven Approach
Building Client/Server Applications Under VB .NET: An Example-Driven Approach
ISBN: 1590590708
EAN: 2147483647
Year: 2005
Pages: 148
Authors: Jeff Levinson

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net