The most important place for logging in your application is on the server side, where your remote components execute. The reasons for this include the following:
You need to make two decisions when implementing server-side logging: where to log the information and where to add the logging code. .NET provides numerous possibilities, each with its own benefits and drawbacks. Table 14-1 outlines some of your choices.
Windows event logging and direct mailing haven't yet been addressed in this book; they are discussed a little later in this chapter. ADO.NET database interaction and Message Queuing are easy to implement using the information covered in the first part of the book. The Message Queuing approach is particularly interesting and extremely flexible. Although it adds complexity, it enables you to completely separate your error logging code from your error response strategy. The remainder of this section outlines a sample approach you can use with Message Queuing. First, you define a custom error object that contains information about problems. Listing 14-1 shows an example. Listing 14-1 A serializable LoggedError class' This enumeration represents how a component chooses to react ' to an error. <Serializable()>_ Public Enum ErrorAction Unknown RecoveredFull RecoveredPartial Ignored RaisedToClient End Enum ' This is the class that will be send to the message queue. <Serializable()> _ Public Class LoggedError ' This contains the original (caught) exception. Public OriginalException As Exception ' This indicates the name of the component and procedure ' where the error occurred. Public FailedComponent As String Public FailedProcedure As String Public FailedComponentVersion As String ' A severity level is assigned as an integer from 0 to 10. Public SeverityLevel As Integer Public ActionTaken As ErrorAction End Class An exception handler in your component catches an error. It then creates an instance of the error object and calls a helper method to deal with it (as shown in Listing 14-2). Listing 14-2 Logging the error through a facadeTry ' (Attempt action here.) Catch Err As Exception Dim LogItem As New LoggedError() LogItem.OriginalException = Err ' (Other property set statements omitted.) ' Log the error. In this case, LogComponent is a member variable ' that references an instance of the EventLogger class shown in the ' next code listing. LogComponent.LogError(LogItem) ' (Handle the error accordingly.) End Try The LogComponent object is an instance of the EventLogger class shown in Listing 14-3, which sends the error information to a message queue. Listing 14-3 Sending the error information to a message queuePublic Class EventLogger Private Queue As New MessageQueue("MyDomain/MyLogQueue") Public Sub LogError(ByVal logItem As LoggedError) Dim ErrMessage As New Message(LogItem) Queue.Send(LogItem, "Error") End Sub ' (Other methods could be added to log informational messages ' differently from errors or warning, or to change the current ' queue.) End Class Finally, a dedicated listener application receives the error message, examines it, and then decides how to act on it. Figure 14-1 shows the full approach. Figure 14-1. Using message queuing to handle error logging
Logging with Facades and ReflectionThe Message Queuing example is shows the basic pattern you should follow when adding logging. As much as possible, the code that performs the logging should be isolated from the code that handles the exception. This basic pattern enables you to quickly replace your logging strategies without touching the business code. For example, Listing 14-2 works with any class that exposes a LogError method. If you modify the LogComponent variable so that it contains an instance of a DatabaseLogger class, the application can continue to function seamlessly. This is nothing new in fact, it's a straightforward implementation of the facade pattern introduced in Chapter 10. To improve on this approach, you can formalize the arrangement by defining an ILogger interface, which might look something like this: Public Interface ILogger Sub LogError(logItem As LoggedError) Sub LogWarning(logItem As LoggedWarning) Sub LogInformation(logItem As LoggedInfo) End Interface Every logging class will then implement this interface: Public Class EventLogger Implements ILogger Private Queue As New MessageQueue("MyDomain/MyLogQueue") Public Sub LogError(ByVal logItem As LoggedError) _ Implements ILogger.LogError Dim ErrMessage As New Message(LogItem) Queue.Send(LogItem, "Error") End Sub ' (Remainder of implementation code omitted.) End Class Finally, the business facade can just retain LogComponent as an ILogger variable: Private LogComponent As ILogger It can then use some initialization code at startup to determine which type of logging to implement. To implement this code, you might use a value in a database or you might use a setting in an application configuration file. Listing 14-4 shows a constructor for the business facade that retrieves the name of the logging component class and the assembly it is contained in using the application configuration file. Keep in mind that the assembly of the logger component is probably not the same assembly as the one that contains the facade. You can deal with this reality using .NET reflection, which enables you to programmatically load a class from an external assembly using only the filename and class name. Listing 14-4 Instantiating a logger dynamicallyPublic Sub New() ' Retrieve the name of the logger assembly. ' This is a full path like "s:\MyAssemblies\Loggers\EventLog.dll" Dim File As String File = ConfigurationSettings.AppSettings("AssemblyFile") ' Retrieve the name of the logger class. ' This is a fully qualified name like "Loggers.EventLogger" Dim Class As String Class = ConfigurationSettings.AppSettings("LogClass") ' Load the assembly. Dim LoggerAssembly As Sytem.Reflection.Assembly LoggerAssembly = System.Reflection.Assembly.LoadFrom(File) ' Create an instance of the logger. LogComponent = CType(LoggerAssembly.CreateInstance(Class), ILogger) End Sub For a much closer look at reflection, refer to Chapter 15, where it's used as the basis for a self-updating application and dynamic application browser. For now, note that reflection can come in handy in your exception handling code to determine information about the executing assembly (as shown in Listing 14-5). This way, when you review the logged message later, you'll know exactly which version of the application failed. Listing 14-5 Using reflection to retrieve version informationTry ' (Attempt action here.) Catch Err As Exception Dim LogItem As New LoggedError() LogItem.OriginalException = Err ' Set some properties obtained through reflection. Dim CurrentAssembly As Sytem.Reflection.Assembly CurrentAssembly = Sytem.Reflection.Assembly.GetExecutingAssembly() LogItem.FailedComponentVersion = CurrentAssembly.GetName().Version LogItem.FailedComponent = CurrentAssembly.GetName().Name ' (Other property set statements omitted.) ' Log the error. LogComponent.LogError(LogItem) ' (Handle the error accordingly.) End Try You now know the types of details you need to consider details that have less to do with .NET technology than they do with the overall design of your logging framework. The next few sections consider how to actually implement the code that goes into an ILogger class such as EventLogger. The Windows Event LogThe Windows event log is a repository for information and error-related system and application messages. It provides its own basic management features (for example, a size limit that ensures it won't grow too large) and can be easily examined using the graphical Event Viewer utility included with Windows. The Windows event log offers a number of advantages:
The Windows event log often has some nontrivial limitations as well, including these:
In short, the event log is an ideal place to log short-term information with a minimum of fuss. It's not a suitable location for the long-term storage of auditing or profiling information. To view the event log on the current computer, select Event Viewer from the Administrative Tools section of Control Panel. By default, you'll see the three standard event logs on the current computer, as described in Table 14-2.
To connect to the event logs on a different computer, right-click on the Event Viewer item at the top of the tree and choose Connect To Another Computer (as shown in Figure 14-2). Figure 14-2. The Event Viewer
You also can right-click on one of the logs in the Event Viewer and choose Properties to modify its maximum size and overwrite settings, or you can create a new custom log. Figure 14-3 shows the properties of the Application event log. Figure 14-3. Event log properties
To get an idea about what type of information is stored in the log, you might want to look at an average log entry that's been left by another application. You'll find that messages are generally classified as warnings, errors, or notifications. They include time and date information, along with the name of the application or component that entered the event and a full message. Writing Log EntriesYou can interact with event logs using the classes in the System.Diagnostics namespace. The EventLog class represents an individual log, and it's this class that you need to work with to retrieve and write log entries. The event log doesn't permit indiscriminate writing. Before you can add a message to a log, you need to register your application as an event-writing source for that log. (Technically, you don't register your application just a string that identifies it.) You can perform this task manually using the EventLog.CreateEventSource method, or you can coax .NET into doing the work for you by setting the EventLog.Source property before you attempt to write a message. If you neglect to perform one of these steps, an error will occur. Listing 14-6 shows the .NET code required to write an error message to the Application log. Listing 14-6 Windows event logging' Create a reference to the application log. Dim Log As New EventLog() ' Define an event source. If it is not registered, .NET will register ' it automatically when you write the log entry. Log.Source = "MyComponent" ' Write the entry. Log.WriteEntry("This is an error message", EventLogEntryType.Error) Figure 14-4 shows the corresponding item as viewed in the Event Viewer, and Figure 14-5 shows the actual error message content. Figure 14-4. The updated error listing
Figure 14-5. The error message
Note that when you create an EventLog object without specifying the log you want to use, the Application log on the current computer is used by default. You can change this logic by using a different EventLog constructor, as shown here: ' Create a reference to the Security log. Dim SecurityLog As New EventLog("SecurityLog") ' Create a reference to the Application log on a computer ' named MyServer. Dim RemoteLog As New EventLog("Application", "MyServer") You can even create your own log using the shared EventLog.CreateEventSource method. This is a useful way to separate your messages from the clutter of entries added by other applications (as shown in Listing 14-7). Listing 14-7 Creating a custom log' Create the log (and register the event source) if the log does not ' already exist. If Not EventLog.Exists("MyCustomLog") Then EventLog.CreateEventSource("MyComponent", "MyCustomLog") End If Dim Log As New EventLog("MyCustomLog") Log.Source = "MyComponent" Log.WriteEntry("This is an error message", EventLogEntryType.Error) When writing a log entry, you have the choice of 10 overloaded versions of the WriteEntry method. You can choose to supply an event ID (a number you use to identify the event), a category number (a number you use to group messages), and an array of bytes. The EventLogEntryType enables you to indicate how the Event Viewer should classify the entry, according to the values shown in Table 14-3.
Note Unless you want the responsibility of creating a byte array (and, if needed later, translating it back to its original form), the event log limits you to an ordinary string of human-readable information. If you're logging an error, you can use the Exception.ToString method. The ToString method returns a string with the name of the class that threw the exception, the exception message, and the stack trace. The return string also includes the result of calling ToString on the inner exception. Retrieving Log EntriesWhen you create an application that uses a Windows event log, you also create a requirement for someone (generally a system administrator) to review that information. That person can perform this task using the Event Viewer, but the task can be complicated if the log is on another computer that doesn't allow remote access. In this case, you might need to create a remote component or XML Web service that exposes methods that allow authenticated users to retrieve log information. This service can retrieve the log information using the classes in the System.Diagnostics namespace. Consider the Web method shown in Listing 14-8, which uses the EventLog.Entries property. This collection represents all the entries added to an event log. The Web method converts this information into a DataTable, which can easily be sent to the client and displayed using data binding. Listing 14-8 Returning log information<WebMethod()> _ Public Function RetrieveLog(ByVal logName As String) As DataSet Dim Log As New EventLog(logName) ' Create a table to store the event information. Dim dt As New DataTable() dt.Columns.Add("EntryType", GetType(System.String)) dt.Columns.Add("Message", GetType(System.String)) dt.Columns.Add("Time", GetType(System.DateTime)) Dim Entry As EventLogEntry For Each Entry In Log.Entries ' Add this entry to the table. Dim dr As DataRow = dt.NewRow() dr("EntryType") = Entry.EntryType.ToString() dr("Message") = Entry.Message dr("Time") = Entry.TimeGenerated dt.Rows.Add(dr) Next ' The DataTable is not a valid XML Web service return type. ' Thus, you must add it to a DataSet before returning it. Dim ds As New DataSet() ds.Tables.Add(dt) Return ds End Function The client requires a mere two lines to display this information using a DataGrid control: Dim Proxy As New localhost.MyService() DataGrid1.DataSource = Proxy.RetrieveLog("Application").Tables(0) Figure 14-6 shows the retrieved data. Figure 14-6. The retrieved log information
Handling Log EventsThe EventLog class also provides an EntryWritten event that enables you to respond when a new entry is added to a log. Using this technique, you can design a utility that waits for events and then attracts the administrator's attention by displaying a message box or playing a sound. Before using the EntryWritten event, familiarize yourself with a few details:
Handling the EntryWritten event is as straightforward as any other .NET event. You just connect the event handler (after creating a valid EventLog object): Dim Log As New EventLog("MyCustomLog") Log.Source = "MyComponent" AddHandler Log.EntryWritten, AddressOf OnEntryWritten Log.EnableRaisingEvents = True Then you handle the event: Private Sub OnEntryWritten(ByVal sender as System.Object, _ ByVal e as System.Diagnostics.EventLogEvent) ' You can check what source sent the message, if desired. If e.Entry.Source = "MyComponent" Then MessageBox.Show("Entry written with message: " & _ e.Entry.Message) End If End Sub Direct Mail NotificationYou can also send e-mail from a .NET application without needing to do much more than create a single object. The only catch is that to use this e-mail capability, you must have a properly configured SMTP server to send the message. (You can configure settings for the SMTP server using IIS manager.) If you don't, you won't be informed of an error and the message will never be received. For more information, refer to Microsoft's excellent introduction to Internet e-mail and mail servers at http://www.microsoft.com/TechNet/prodtechnol/iis/deploy/config/mail.asp. The .NET e-mail features are found in the System.Web.Mail namespace. The key classes include MailMessage (which represents a single e-mail message), SmtpMail (which exposes shared methods you can use to send a message), and MailAttachment (which represents a file attachment that you can link to a message). Sending a message is as simple as creating a new MailMessage object; setting some properties to identify the recipient, the priority, the subject, and the message itself; and using the SmtpMail.Send method to send it on its way (as shown in Listing 14-9). Listing 14-9 Sending an error e-mailDim MyMessage As New MailMessage() MyMessage.To = "someone@somewhere.com" MyMessage.From = "Automatic Logger" MyMessage.Subject = "Error" MyMessage.Priority = MailPriority.High MyMessage.Body = "Critical error: " & _ LoggedError.OriginalException.ToString() SmtpMail.SmtpServer = "localhost" SmtpMail.Send(MyMessage) |