Z Code
Output Your Trace Info
Output your trace information to new targets, including SQL Server and XML—and do it without parsing.
Technology Toolbox: VB.NET, XML
Tracing allows you to record information about your application as it runs. Custom trace listeners let you leverage this trace information. In this article, I'll show you how to output your trace information to new targets including SQL Server and XMLand how to do this without parsing. I'll also show you an assert technique that's more flexible than Debug.Assert and remains available throughout your application's life cycle while protecting the end user from assert message boxes. I'll review the basics of the Trace and Debug classes and show how you can use them to improve the effectiveness of your trace output.
The calling code creates trace entries, often by calling the Trace class's shared (C# static) WriteLine method directly. Using a custom trace library to manage your output improves the readability and consistency of tracing in your application. The key to an effective trace library design is making it fast when you aren't tracing and complete, consistent, and flexible when you are tracing.
My May column used a trace entry class to provide strong typing for your tracing; I'll reuse that core mechanism here. The TraceEntry class also offers consistent information and single-point formatting for string output. Wrapper methods for different categories of output provide strong typing benefits. The constructor call looks like this, whether called from your code directly or through a wrapper:
traceEntry = New traceEntry(mRunGUID, _
stackFrame, params, traceLevel, _
value, valueName, traceIssue, issue)
mRunGUID is created in a shared constructor and is specific to this run of the application. stackFrame is the .NET stack frame for the position you're tracing, params is an object array of parameter values or nothing, and traceLevel is the entry's level of importance. value and valueName are optional and let you describe a value that fails validation. traceIssue is an enum value specifying the type of entry, and issue describes what you're outputting (download the TraceEntry class here).
Attach Trace Listeners
The .NET Trace class and its twin sister, the .NET Debug class, route parameters passed to their shared output methods (Write, WriteLine, Asset, and Fail) to all of the attached trace listeners. You can attach listeners from a config file in your application's code, or from the Immediate window in break mode. You can't vary the output sent to different listenersall the listeners get the same output. You also need to set the AutoFlush property so you don't have to flush trace listeners explicitly. Setting AutoFlush affects all listeners. The System.Diagnostics section from an app.config looks like this:
<system.diagnostics>
<switches>
<add name="TraceLevelSwitch"
value="2" />
<add name="ShowDialogIf"
value="1" />
</switches>
<trace autoflush="false"
indentsize="4">
<listeners>
<add name="SQLListener" type=
"KADGen.TracingTools.SQLTraceListener,
TracingTools"
initializeData=
"data source=(local);
initial catalog=TraceOutput;
integrated security=SSPI" />
</listeners>
</trace>
</system.diagnostics>
This sample config file shows two trace switches. Trace switches let you specify the level of importance, including off, error, warning, information, and verbose, identified by zero to four respectively. You can use switches for any purpose; you'll use them commonly to manage the verbosity of the trace output.
SQLTraceListener is a custom trace listener (find details about specifying it in App.config in the sidebar, "What's in a Name?"). Custom trace listeners, which derive from the abstract System.Diagnostics.TraceListener class, let you enhance trace output behavior. You start with the interface or contract, and the intention behind the responsibility, when planning derivation from any class. The code that calls the .NET Trace methods is responsible for collecting the key information. The .NET Trace class is responsible for routing, and the trace listener is responsible for outputting to the right location.
The calling code often applies formatting and passes a string to trace listeners. However, there's a better and often overlooked option for formatting with custom trace listeners. Calling code can pass anything (System.Object) to the Trace class's Write methods (not Assert or Fail). There's an important inferred contract: The goal of the system is to output trace information, and many trace listeners output strings, so the object's ToString method should output trace information as an intelligently formatted string. This lets you support all listeners while you support strong typing of your own trace entries.
As with any other problem you're solving in an OOP environment, explore putting reusable code in a base class. This reduces the code you must write and, more importantly, provides consistency and a single point for change. In this case, the SpecialListener base class performs several common functions (see Listing 1). SQLTraceListener and XMLTraceListener derive from the SpecialListener class, which itself derives from System.Diagnostics.TraceListener (see Figure 1).
SQLTraceListener lets you load information into SQL Server, which can manage and analyze a massive amount of information (see Listing 2). You'll be able to filter by run or priority, order by time, and so on. SQLTraceListener needs a connection string passed as a constructor parameter because it inserts entries in a SQL Server database. It's important to protect the tracing tables whether they exist in your main database or on an entirely different server using normal SQL Server techniques. The trace information provides a lot of detail about how your application behaves, even if traces are isolated from your main database. You don't want this information in the hands of your competitor or a hacker. Running traces to a local instance (including MSDE) provides adequate performance (see Figure 2).
Custom Listener to the Rescue
Traditional asserts have two problems, both of which you can solve using a custom listener. First, Debug.Assert ensures your users aren't faced with unfriendly dialogs, but you can't check your deployed application. Second, the Assert and Fail methods take only string parameters, not object parameters. This means you must parse to fine-tune your custom trace listener's behavior; parsing is slow and error-prone because it loses strong typing benefits.
You can provide traditional assert behavior using WriteLine, a TraceEntry object, and a custom listener. For example, you can tune the listener to display a dialog for trace entries with a trace level more serious than the ShowDialogIf switch, but only if the running application is in Debug mode. In Release mode, the trace listener never displays the dialog, but you can filter critical items from trace output or extend the listener to create a special error log. Note that the ShowDialogIf switch is separate from the level of tracing. You'll often want to output traces at a particular verbosity, but display dialogs only for the errors.
You need to determine the correct build status because each assembly is compiled separately as either a release or debug build. A number of assemblies might be involved; for example, different assemblies might hold the startup code, the trace listener, the class you're tracing, and the class declaring the method you're tracing. The logical assembly to use is the one that contains the method you're tracing. You can retrieve this assembly from StackFrame's MethodBase and check its DebuggableAttribute. The IsDebugBuild method performs this check (see Listing 1).
Your special listeners are probably not stream-based. The Write method outputs partial lines; this doesn't fit easily with row-based output such as SQL Server. You should append any Write parameters to a class-level variable and output the concatenated message when the user calls the WriteLine method.
The stream-based approach causes a problem with XMLTraceListener, which uses XMLTextWriter. You've got to close the top-level element before closing the writer. You can do this by capturing the AppDomain's ProcessExit event, but only if you're running a single process. Declare a WithEvent variable at the top of your class:
Private WithEvents appDomain As _
System.AppDomain = _
Threading.Thread.GetDomain
Execute this code later in the class:
Private Sub CloseUp( _
ByVal sender As Object, _
ByVal e As EventArgs) _
Handles appDomain.ProcessExit
mxmlTW.WriteEndDocument()
mxmlTW.Close()
End Sub
The more traditional IDisposable pattern doesn't work for TraceListeners. Output an XML fragment without open or close elements if you need to support multiple processes, and fix it up later. This approach also allows you to concatenate the output from multiple runs of your application.
You could extend these ideas to write to an error stream or file based on a WriteErrorLogIf switch similar to the DisplayDialogIf switch. The key to effective use of the Trace and Debug classes is thinking outside the box implied by the help system.
About the Author
Kathleen is a consultant, author, trainer and speaker. She’s been a Microsoft MVP for 10 years and is an active member of the INETA Speaker’s Bureau where she receives high marks for her talks. She wrote "Code Generation in Microsoft .NET" (Apress) and often speaks at industry conferences and local user groups around the U.S. Kathleen is the founder and principal of GenDotNet and continues to research code generation and metadata as well as leveraging new technologies springing forth in .NET 3.5. Her passion is helping programmers be smarter in how they develop and consume the range of new technologies, but at the end of the day, she’s a coder writing applications just like you. Reach her at [email protected].