Customizing Assert Failures


For automated test suites or programs that you anticipate might have a significant number of assert failures, having a UI dialog box pop up for each failure might not appear to be a very scalable solution. However, the .NET Framework's asserts can be customized to implement custom logging or failure logic. The backbone for this is the System.Diagnostics.TraceListener abstract class (which we discuss further below).

When your program fails an assert for the first time, a new DefaultTraceListener gets allocated and placed into the Debug.TraceListeners collection. This type derives from TraceListener and implements the UI logic in addition to some simple logging if you choose to enable it. This collection is what the Debug.Assert methods look to when a failure occurs.

There are a variety of options you can change on the DefaultTraceListener instance to customize behavior. You can also write a custom TraceListener or use one of the other existing listeners such as the EventLogTraceListener to write to the Windows Event Log, described further below. For now, let's consider the case where we want to disable the UI and enable simple file logging:

 void ChangeAssertBehavior() {     // Mutate the default trace listener.     DefaultTraceListener dtl = Debug.Listeners[0] as DefaultTraceListener;     dtl.AssertUiEnabled = false;     dtl.LogFileName = "AssertFailures.txt";     // And now fail an assert.     F(0, 1); } 

This uses the simple method we defined above to fail an assert. Instead of popping up a UI, however, it pipes the output to a file named AssertFailures.txt, appending to any existing text. The result after a single run of this program, assuming that the detailed error message is supplied, is:

 ---- DEBUG ASSERTION FAILED ---- ---- Assert Short Message ---- x does not equal y ---- Assert Long Message ---- This method assumes that x and y are equal, but they are not (x: 0, y: 1); continuing execution could result in data corruption.     at Program.F(Int32 x, Int32 y)     at Program. ChangeAssertBehavior()     <snip/>     at Program.Main(String[] args) 

This is obviously much more useful for automated test scenarios but does not permit you to halt execution on failure of an assert. Thus, the program will continue executing blindly, spewing failed assert messages into the supplied text file. In other words, your program will attempt to limp along and could cause real damage, for example to a database, files on disk, and so on. As long as you're working in a test environment, this might be acceptable.

You might consider writing your own trace listener that automatically calls Environment.Exit upon an assert failure. And you can filter messages, direct them to a fancier store (e.g., event log, database), perform custom actions, and so on, all by simply plugging in your own TraceListener. This core technology spans more than just asserts, and thus is described in more detail in the "Trace Listeners" section below.

Debug and Trace

The Debug and Trace classes have a few other useful methods in addition to Assert. They're quite simple and self-explanatory. We'll take a brief look at them here. Method calls to Debug and Trace are conditionally compiled out of certain builds. Debug calls are only enabled on builds with the DEBUG symbol defined, which in Visual Studio means the Debug build. Trace calls are enabled on builds with the TRACE symbol defined, which in Visual Studio's default configuration are both the debug and release builds.

In summary: Debug calls are safe to leave in your code so that you can enable rich tracing during testing. And Trace calls usually remain in deployed applications, which can be useful for on-the-fly tracing for applications deployed in the field. You can obviously create your own custom build flavors if you want to use these symbols differently.

Before moving on to Trace and Debug's functionality is that both classes are static. This means that one target handles publishing all messages inside a single AppDomain. In many cases, this isn't what you want, for example if you need a per-component trace log. And it can actually cause problems in highly concurrent applications for two reasons: (1) multiple threads contend for a single resource, impacting performance; and (2) it makes coalescing output difficult because traces are typically interleaved in surprising ways. For finer-grained tracing that avoids some of these difficulties, you probably want to use the TraceSource APIs. (For example, you might consider a per-component, per-thread [via ThreadStatic] TraceSource.)

Writing to Output

The Print, Write, and WriteLine methods all have a variety of overloads that do basically the same thing: they write the contents supplied to the underlying TraceListener. They differ only in the formatting arguments offered, following a pattern similar to that which Console uses. Using the DefaultTraceListener, both Debug and Trace output gets sent to the attached debugger. If your code is running interactively inside VS, you'll see the output in your Debug Output Window. If you've customized the TraceListener or written your own, both of these classes simply delegate to their Write and WriteLine methods. Thus, the output will get redirected however your listener chooses.

The WriteIf and WriteLineIf methods are a mix between the Write methods above and asserts. They write to output if they succeed (rather than fail as is the case with asserts) and are essentially no-ops if the Boolean supplied is false.

Unconditional Failures

We already noted above that when an assert fails, it makes a call to the Fail method. This method just walks the list of TraceListeners (which holds a single listener of type DefaultTraceListener if you haven't changed it) and calls Fail on each one. This can be used for general failures in your code and likewise gets conditionally compiled out of certain types of builds.

You can use this method for assert-like situations where it might be more convenient to trigger unconditional failure rather than writing Assert(false), for example. Assuming the default behavior, this will show up just like an assert does. This is quite nice because you get stack trace information for free and get to reuse all of the logic prebuilt for you. The simple change to DefaultTraceListener outlined above will pipe these failures to a text file instead of showing UI.

Conditional Compilation

I've noted a couple times that calls to Debug are present only in debug builds, and that TraceSource and Trace calls are present in release builds. To ensure that you completely understand what this means, let me briefly explain.

If you take a look at Debug, Trace, and TraceSource's methods using your favorite IL inspection utility, you'll notice they are annotated with the System.Diagnostics.ConditionalAttribute. This attribute takes a single argument, conditionString, representing the conditional compilation symbol that must be defined during compilation for a call to one of these methods to remain in the compiled application. Debug uses the DEBUG symbol, while both Trace and TraceSource use TRACE.

This means that unless you compile your application with these symbols defined, any calls you make to these functions will be elided by your compiler. This is why Debug.Asserts, for example, don't have any impact whatsoever on non-Debug builds. Fortunately, the Visual Studio IDE helps with the definition of these symbols. By default, it will enable the DEBUG symbol when compiling a debug build, and TRACE for both debug and release (selectable from the Build Configuration Manager menu). You can, of course, change these settings yourself, or specify them at the command line when invoking csc:

 csc MyApp.cs /target:exe /define:DEBUG,TRACE /debug csc MyApp.cs /target:exe /define:TRACE 

These two lines are similar to compiling a debug and release build, respectively, from Visual Studio. It's possible to define the DEBUG symbol without specifying the /debug switch (and vice versa), and there's nothing preventing somebody from putting a #define DEBUG somewhere in your code that will end up turning on assert logic all over the place.

Note that you can specify conditional compilation symbols with the VB and C++/CLI compilers too:

 vbc MyApp.vb /target:exe /define:DEBUG=TRUE,TRACE=TRUE /debug cl MyApp.cpp /d:DDEBUG=TRUE /d:DTRACE=TRUE /Zi 

Fortunately, if you're working in VS, the complexities and differences among the various compiler command-line switches are hidden from you.

Trace Listeners

The abstract base class TraceListener is responsible for taking trace information from a source, performing filtering, and ultimately writing the information to some trace destination. A base trace listener consists of methods for publishing trace information and properties that can be used to configure instances of a listener. Because it's abstract, it has no public constructors with which to create an instance.

A concrete implementation determines roughly three things of notable interest:

  • Where does output go to? For example, does it get written to a file, an XML file, the console, a debugger's output window, Event Log, or other location?

  • What happens to failures? They occur whenever Fail is called, most of the time due to an assert failing. The listener can react by popping a UI dialog, simply logging to a file, or failing unconditionally with an Environment.Exit (or FailFast if things are terribly wrong).

  • What additional configuration options are available above and beyond what the base listener provides? Do you want to enable your users to customize your listener via configuration, for example?

We'll see some of the concrete implementations that ship with the Framework shortly. First, we'll look at the base listener type and what it has to offer.

Base Listener Options

The base TraceListener class has an array of configuration points, and each subclass can choose to offer its own. For example, the DefaultTraceListener offers a Boolean flag indicating whether the assert UI should be shown on failure (the default is true). We'll take a quick look at some of these options and what they control.

The Name property is a string that enables you to refer to a listener by name from inside the configuration file, and also in programmatic scenarios (e.g., so you can walk a list of listeners searching for a specific instance). IsThreadSafe is a Boolean that tells callers whether invoking trace calls on this listener is guaranteed to be thread safe. This is almost always a constant value for a particular implementation of TraceListener and doesn't vary from instance to instance. In other words, if a program is using a single listener concurrently, is the output serialized? If not — and the output gets interleaved at random — trying to use it will undoubtedly result in gibberish.

The Attributes property is a string-to-object dictionary that can be used to set extensible configuration options on a listener implementation. It's convenient because you can add items to it from the configuration file, and it is a simple way to configure listeners that are looking for certain keys in the list. Furthermore, if you're writing your own listener, this is a good way to leverage existing customizability. The Filter property is used to set a filter on the trace information. We'll see shortly how filters are used to ensure that only interesting information, based on configuration, makes its way to the persistent storage mechanism.

Lastly, the TraceOutputOptions is of a flags-style enumeration type, TraceOptions, which enables you to add auxiliary information to the base tracing information. These settings include Callstack, DateTime, ProcessId, ThreadId, and Timestamp. Because it is a flags-style enumeration, you can specify more than one value.

Default Tracing Behavior (DefaultTraceListener)

The default trace listener is an implementation that gets used by the Debug, Trace, and TraceSource APIs by default if an override is not supplied. Output gets picked up by the active debugger. If there is no active debugger, the output is ignored.

Failures by default pop a standard assert UI with the Retry, Abort, and Ignore set of options you saw above. You can turn off the UI by setting AssertUiEnabled to false. Furthermore, if the LogFileName property is given a value, ordinary trace and assert information will get written to that file. These are the only two configurable properties on the DefaultTraceListener.

Writing to the Event Log (EventLogTraceListener)

A convenient and simple way of writing to the Windows Event Log is to use the EventLogTraceListener. It logs entries into the Application Log with the Type set to Error, Warning, or Information based on the value of the TraceEventType (if using TraceSource). It has two bits of configuration to be supplied at construction time: an EventLog instance if you wish to provide your own, or alternatively just a Source string that gets logged to the Event Log when you create a new event entry.

Textual Tracing Behavior (TextWriterTraceListener)

The TextWriterTraceListener can be used to write to an arbitrary System.IO.Stream, TextWriter, or string representing a filename. The Stream and TextWriter constructor overloads are only useful if manipulating the listener programmatically. If you're using configuration, you'll likely just use the string-based filename overload — it handles the construction of a Stream and TextWriter for you.

Writing to the Console (ConsoleTraceListener)

For quick-and-dirty tracing, it doesn't get much better than the good ol' console! The console listener is a subclass of TextWriterTraceListener and is simply a shortcut to prevent you from having to pass in Console.Out or Console.Error to the TextWriter listener's constructor. This isn't possible to do using the configuration system. And because writing to the console is such a common use case, the ConsoleTraceListener was created to make it as simple as can be.

By default, all output will be written to stdout attached to the current process (i.e., Console.Out). Often you will want to write to stderr instead; for example, in command-line scenarios where streams might be piped to different locations. ConsoleTraceListener offers a constructor that takes a Boolean parameter: if an argument of true is supplied, all traces will be written to stderr (i.e., Console.Error) instead of stdout.

Writing a Delimited List of Text (DelimitedListTraceListener)

The ordinary TextWriterTraceListener prints out information in a completely unstructured format. If you want to import this information into Excel or parse it later on in some data manipulation program, you'll get fed up pretty quickly. This is where the DelimitedListTraceListener comes in handy. You can use the same destination options as the TextWriter-based listener, but this listener augments the information with delimited formatting.

By default, the delimited listener will output with semicolons as the delimiting character. The type has a Delimiter property if you wish to customize this. You can do this programmatically or through configuration by specifying a delimiter attribute. For example, this configuration entry uses a comma as the delimiter instead:

 <system.diagnostics>     <listeners>         <add type="System.Diagnostics.DelimitedListTraceListener"              name="sharedListener"              initializeData="trace.txt"              delimiter=","/>     </listeners> </system.diagnostics> 

More details on how configuration of tracing components works can be found a few sections down under the Configuration heading.

Writing to XML (XmlWriterTraceListener)

DelimitedListTraceListener is useful for structured output, but XmlWriterTraceListener is admittedly better at this job. Most tools nowadays understand XML, and quite truthfully a lot of the information that gets captured in a trace is hierarchical in nature and thus is easier to comprehend in a hierarchical data format like XML. When using the XmlWriter listener you just supply the same information you ordinarily would for a TextWriterTraceListener, and the output gets formatted as XML for you.

Unfortunately, the XmlWriterTraceListener has two major downfalls. First, it isn't extensible. If you want to write output using your own custom XML format, you'll either have to write your own Trace Listener or hack together an XSLT to translate from the default format to your own. But second — and worst of all — is that its output isn't even legal XML! It outputs each event as a root E2ETraceEvent node, meaning that there are actually multiple root elements in a single document. This is bound to confuse your favorite XML parsing utilities.

How Traces Get Published

Let's take a quick look at the methods sources use to publish trace information. Usually the front-end API (e.g., Debug, TraceSource) will make these calls for you. If you ever want to write your own TraceListener or interact with preexisting ones, this information might come in handy. But in everyday life, you likely won't go calling any of these methods directly on a TraceListener.

The core APIs are the Trace* and Write* methods. Any calls to these APIs simply format the input, filter data that needn't be logged by calling ShouldTrace on its Filter (if any), and hand it off to the two abstract Write and WriteLine overloads. If you end up creating your own listener, you often only have these two methods to implement. The Fail methods are used for failed asserts or other unconditional failures.

If you're managing the lifetime of a TraceListener, you might be interested in the Dispose and Flush methods. Some TraceListeners will cache output, for example when using a Stream as the backing store, so you might need to call Flush explicitly if you want to force pending writes to be committed. Most listeners hold on to some unmanaged resources, so calling Dispose before they get dropped and become eligible for garbage collection can help to reduce the overall resource pressure on the system.

Filtering Trace Information

There are two primary ways to filter which data is logged when using the tracing infrastructure. The first is to use a Switch at the trace site itself. Switches are configurable and avoid sending any data to the listeners at all if the configuration determines that the information you're about to publish is not interesting. This permits you to configure logic without having to change and recompile your application code. The second technique is to add a Filter to the actual listeners that will receive your trace information. This doesn't prevent the tracing information from routing through the trace infrastructure but does filter the messages out so that they aren't written to storage.

Switches are useful for global suppression or activation of trace information, while filters are more useful for fine-grained and expressive and rich filtering. Filters can encapsulate significantly more complex behavior, while switches are intended to be very simple.

Trace Switches

The base type Switch is an abstract class and isn't of much use on its own. It exists simply so that switches can be accessed and configured in a generic manner. It does define the DisplayName and Description properties, the former of which is used for cross-referencing the switch in the same way that listeners are named and referenced in configuration files. A switch is used differently depending on the source you're using. Each switch provides its own way of evaluating whether to proceed with the trace, so you'll actually need to add an explicit test before sending data to the trace source itself. APIs like Trace.WriteLineIf help this to feel more natural. Furthermore, the TraceSource API actually has integrated support for the SourceSwitch.

Source Switch

The primary switch type you'll work with — especially if you're using TraceSource — is the SourceSwitch. Its purpose is to filter based on the category of individual trace messages, represented by a SourceLevels flags-style enumeration value. SourceLevels offers values for Critical, Error, Warning, Information, and Verbose, along with Off and All, which do what you might imagine.

This switch's primary benefit is that it integrates nicely with the TraceSource class. TraceSource has a Switch property that is of type SourceSwitch, enabling you to perform filtering on the individual source level:

 TraceSource ts = new TraceSource("myTraceSource"); ts.Switch = new SourceSwitch("mySwitch"); ts.Switch.Level = SourceLevels.Error; 

The result is that any information logged via ts will only be committed to storage if it has a source level of Error or greater (i.e., Critical). You do the same using the configuration system:

 <system.diagnostics>     <sources>         <source name="myTraceSource" switchValue="Error" />     </sources> </system.diagnostics> 

This type's predicate method is ShouldTrace, which takes a TraceEventType flags-style enumeration instance. You might notice an odd discrepancy here: ShouldTrace deals in terms of TraceEventType while the property Level is specified using a SourceLevels instance. It turns out that these two enumeration types have a special relationship such that bitwise comparisons do the right thing. In other words, SourceLevels.Error when anded with TraceEventType.Error will yield success.

Boolean Switch

BooleanSwitch is the simplest of the switches available. It enables you to turn output on and off completely, regardless of source or trace type information. It has a single Boolean property, Enabled, which indicates whether or not traces should occur. Unlike using SourceSwitch with TraceSource, you must manually invoke the predicate before passing your message to the tracing infrastructure. As an example of how you might use this, consider this code:

 static BooleanSwitch testBooleanSwitch = new BooleanSwitch(     "testBooleanSwitch", "Used to globally suppress or enable all tracing"); // ... Trace.WriteLineIf(testBooleanSwitch.Enabled, "This is some tracing message"); 

By default, this switch will be turned off. You'll likely want to enable (or disable) it through configuration or by setting the Enabled property manually. You'll see later what the tracing configuration sections look like, where to put them, and so on. Suffice it to say for now that you'd need something along these lines:

 <system.diagnostics>     <switches>         <add name="testBooleanSwitch" value="True" />     </switches> </system.diagnostics> 

Trace Switch

The last switch type we will cover, TraceSwitch, enables you to filter your tracing information by one of several levels represented by the TraceLevel enumeration, very much like SourceSwitch. It works by specifying the tracing level (from most severe to least severe in ascending order), the result of which is that everything above your setting gets suppressed. One of the values is Off, suppressing all traces. The other values, in most severe to least severe, are: Error, Warning, Info, and Verbose. So, for example, specifying Warning will capture both error- and warning-level messages.

As with BooleanSwitch, the client must test one of the Boolean properties at the time of tracing to determine whether to proceed: TraceError, TraceWarning, TraceInfo, and TraceVerbose. These accessors simply use the Level property to determine whether the property requested is equal to or more severe than its value. For example, consider this code:

 static TraceSwitch testTraceSwitch = new TraceSwitch(     "testTraceSwitch", "Used to suppress traces based on severity"); // ... Trace.WriteLineIf(testTraceSwitch.TraceError, "Something bad happened"); Trace.WriteLineIf(testTraceSwitch.TraceInfo, "Some informational trace"); 

Similarly to the above, you might find the following configuration section:

 <system.diagnostics>     <switches>         <add name="testTraceSwitch" value="Verbose" />     </switches> </system.diagnostics> 

This would cause all trace output that ran through this switch to be output (Verbose is equivalent to saying "all"). So, both of the above traces would be executed. Say that you were to change the add line from above to the following:

 <add name="testTraceSwitch" value="Error" /> 

This would have the effect of suppressing the second trace line above, the one that checked the TraceInfo predicate before writing (it would return false now).

Trace Filters

Each TraceListener has a Filter property, using which you can filter out trace information. This property is of type TraceFilter. This type is abstract and can be implemented very easily. You simply override the ShouldTrace method, use the information that is passed to it by the tracing infrastructure, and return true or false to indicate to the listener whether a given message should be logged. There are two out-of-the-box implementations of this class in the .NET Framework: EventTypeFilter and SourceFilter.

EventTypeFilter is actually much like the switches we've already seen. But unlike switches, a filter stops messages from flowing through a specific listener. This type has a EventType property of type SourceLevels and also a constructor that initializes a filter using a SourceLevels argument. As with the SourceSwitch and TraceSwitch above, only messages of the specified level or more severe will be permitted to pass through the filter.

The other type, SourceFilter, is used to filter messages based on their origin. This is purely for use with TraceSources. Each TraceSource has a Name property which is automatically supplied to the tracing infrastructure so that origin can be logged and tracked. Each SourceFilter has a string-based Source property, and filters out any messages that did not originate from the specified source.

Configuration

All of the discussion up until this point has been around programmatically creating and manipulating trace objects. We've shown some simple — but not comprehensive — examples of using configuration. Programmatic configuration is a useful means of setting up tracing for testing and exploration purposes. But once you begin to productize your application, you'll want the ability to dynamically turn on and off all those tracing messages throughout your application, filter messages, redirect output to different destinations, and so on, without needing to constantly change and recompile your source code. Fortunately, the tracing infrastructure has a simple configuration subsystem that enables you to do all of this.

Your tracing configuration information usually goes into your application's configuration file. (You can, of course, set up enterprise- or machine-wide policy if you wish.) In VS, simply use the Add New Item menu to add a new configuration file your project via the Application Configuration File type. This adds a new configuration XML file to which you must add a new <system.diagnostics /> section. Note that this is named App.config by VS but gets turned into xxx.config, where xxx is your EXE or DLL's filename (e.g., MyApplication.exe.config), during the build process.

 <?xml version="1.0" encoding="utf-8" ?> <configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">     <system.diagnostics>         <!-- Detailed tracing config goes here -->     </system.diagnostics> </configuration> 

You'll notice that VS gives you IntelliSense when editing this section, making it easier to discover and remember all of the possible settings.

The general way that tracing configuration works is that nodes have a name attribute, which must match an instance defined in the application. For example, when your application instantiates a new TraceSource, you will supply a name. Then in the configuration file, you will reference the settings for that TraceSource by name. These two strings must match exactly (case sensitively) for the configuration to get picked up at initialization time.

Note that the Debug, Trace, and TraceSource classes pick up the configuration settings automatically upon their first use. If you change these while your application is running, you generally must restart for them to be reread. The exception to this rule is the Trace class: it offers a Refresh method that reinitializes state based on any changes to the configuration.

Trace Source Configuration

You'll often have a number of TraceSource instances in your application, each of which requires its own configuration. For example, you might have a single TraceSource dedicated to ComponentA. If ComponentA begins failing, you would likely want to enable just that component's tracing output for inspection; you don't want ComponentBZ's output cluttering up the trace log. We discussed filters above, which happens to be the solution to this problem. We'll take a look at how to enable this using configuration in a moment.

But first, let's take a look at the general mapping between a TraceSource and its configuration entry. Assuming that your TraceSource gets instantiated in your program as follows:

 TraceSource caTraceSource = new TraceSource("ComponentA"); 

the default setting for this TraceSource constructor is for its Switch to be initialized to SourceLevels.Off. This means that the traces will be ignored and not actually logged anywhere. In other words, it assumes a configuration like the following:

 <system.diagnostics>     <sources>         <source name="ComponentA" switchValue="Off" />     </sources> </system.diagnostics> 

This configuration entry won't actually exist unless you add it yourself. The sources node can have zero-to-many individual source elements under it. Each node is generally of the form:

 <source name="..."         (switchName="..." | switchValue="...")?>     <listeners /> </source> 

Notice that a source consists of a name, either a switchName or switchValue (optional), and an optional listeners node containing zero-to-many listeners.

Changing TraceSource Switch Settings

Assuming that you have added the entry listed above, the simplest way to enable logging is to supply a new value for the switchValue attribute. As noted in the section above on SourceSwitch, the switch type that TraceSource deals with expects a value from the enumeration SourceLevels. This will enable all values equal to or below the setting you give it. If we were to change the source node from above to:

 <source name="ComponentA" switchValue="Error" /> 

this would tell the TraceSource to log all traces that specify a TraceEventType value of Error or worse, which happens to be Error and Critical on the SourceLevels enumeration. Thus, the first two lines would succeed in the following example, while the others would simply be ignored:

 caTraceSource.TraceEvent(TraceEventType.Critical, 1000, "Ouch!"); caTraceSource.TraceEvent(TraceEventType.Error, 1001, "Something bad"); caTraceSource.TraceEvent(TraceEventType.Warning, 1002, "I'm warning you"); caTraceSource.TraceInformation("You might want to know about this..."); caTraceSource.TraceEvent(TraceEventType.Verbose, 1003, "Blah, blah, blah"); 

If you were less particular and wanted to turn all trace output on, you could simply change the source node to the following:

 <source name="ComponentA" switchValue="All" /> 

There's another way to indicate switch information. If you end up with a large number of sources, you might legitimately want to start sharing a default switch setting, or perhaps share a single switch among a set of sources. This way, you change it in one place and all referencing sources start using the new settings. The configuration file supports this by referencing a separate area of the configuration file with named switches. Instead of switchValue, you supply a switchName and then set up your switch in the switches section. For example:

 <system.diagnostics>     <sources>         <source name="ComponentA" switchName="sharedSwitch" />         <source name="ComponentB" switchName="sharedSwitch" />     </sources>     <switches>         <add name="sharedSwitch" value="Error" />     </switches> </system.diagnostics> 

In this configuration, both the ComponentA and ComponentB TraceSources share the sharedSwitch switch. Thus, if you changed "Error" to "All", for example, both components would begin using the new value.

Adding Listeners to a TraceSource

Configuring listeners for a TraceSource is much like configuring source information. The primary difference is that a single source can have many listeners. But as is the case with switches, you can define the listener inline or alternatively refer to it in a sharedListeners section of the file. Remember that by default sources will use the DefaultTraceListener.

 <system.diagnostics>     <sources>         <source name="ComponentA" switchName="sharedSwitch">             <listeners>                 <add type="System.Diagnostics.ConsoleTraceListener"                      name="consoleListener" />                 <remove name="Default" />             </listeners>         </source>     </sources>     <switches><!-- ... --></switches> </system.diagnostics> 

This example adds a private ConsoleTraceListener instance to ComponentA. This results in all tracing output being written to the Console. Notice also that we have a remove element stuck in there. Recall that all sources have a DefaultTraceListener added by default: if we don't want it, we have to remove it ourselves.

The shared listener approach looks somewhat similar, as follows:

 <system.diagnostics>     <sources>         <source name="ComponentA" switchName="sharedSwitch">             <listeners>                 <add name="sharedListener" />             </listeners>         </source>     </sources>     <sharedListeners>         <add type="System.Diagnostics.ConsoleTraceListener"              name="sharedListener" />     </sharedListeners>     <switches><!-- ... --></switches> </system.diagnostics> 

Much like switches, this approach enables you to share a single listener among many sources and to change it in a single place rather than for each and every source you have.

Listeners Configuration

Each TraceListener implementation has its own set of properties that can be supplied at configuration time. We already covered what properties each of these types have and will cover in just a moment how you go about providing such information at instantiation time. But regardless of the implementation, you'll start off with a very simple format (which you already saw in the previous section):

 <system.diagnostics>     <sharedListeners>         <add type="System.Diagnostics.ConsoleTraceListener"              name="sharedListener" />     </sharedListeners> </system.diagnostics> 

The type attribute is the fully qualified name of the listener class. This example in particular adds a ConsoleTraceListener instance to the list of available shared listeners, which can then be referenced by other sources (or the trace or assert sections, which we won't go over explicitly in this chapter) using its name sharedListener.

Simple Listener Configuration

You can also set the TraceOutputOptions property on a listener through configuration. Simply provide a comma-delimited list of values inside a traceOutputOptions attribute. For example, to add a call-stack, thread id, and timestamp to the listener we defined above, change it to:

 <add type="System.Diagnostics.ConsoleTraceListener"      name="sharedListener"      traceOutputOptions="Callstack,Timestamp,ThreadId" /> 

If the listener you are working with has a constructor, you can supply arguments to it with the initializeData attribute on the add element. For example, the console listener accepts a Boolean that, when true, redirects all output to stderr rather than the default of stdout. This configuration snippet will configure it that way:

 <add type="System.Diagnostics.ConsoleTraceListener"      name="sharedListener"      initializeData="true" /> 

Similarly, the TextWriterTraceListener has a constructor overload that takes a string. This specifies the target filename that the writer outputs to. You can provide it as follows:

 <add type="System.Diagnostics.TextWriterTraceListener"      name="sharedListener"      initializeData="MyTrace.txt" /> 

With this configuration, all traces will be written to the MyTrace.txt file. All of the other listeners are configured similarly.

Adding Filters

By default, listeners do not have a TraceFilter attached to their Filter property. Filters are often very useful ways to even further lock down on the tracing information that gets through to the backing store. This can happen regardless of the source API you have chosen to use. For instance, you can use a EventTypeFilter to do much the same thing you can do with the SourceSwitch demonstrated above. The difference here is that it gets attached to the listener and can be used for the Debug and Trace classes, too.

As an example, consider this configuration snippet:

 <system.diagnostics>     <sharedListeners>         <add type="System.Diagnostics.ConsoleTraceListener"              name="sharedListener">             <filter type="System.Diagnostics.EventTraceFilter"                     initializeData="Error" />         </add>     </sharedListeners> </system.diagnostics> 

This adds a filter to the sharedListener such that only Error-level and worse messages get logged. Similarly, you can use a SourceFilter, which filters out everything but the provided source. This often isn't very useful since the source maps directly to the name of a TraceSource instance (which can be manipulated individually in the configuration) but can sometimes be useful to selectively enable just a single source.

Asserts Configuration

Both the Trace and Debug types offer assert functions that, by default, pop a UI rather than logging failures. This is something you may want to configure. To do so, simply supply a new <assert /> node with the settings you desire:

 <system.diagnostics>     <assert assertuienabled="false" logfilename="asserts.out" /> </system.diagnostics> 

This example turns off the assert UI and redirects output to a file named asserts.out.




Professional. NET Framework 2.0
Professional .NET Framework 2.0 (Programmer to Programmer)
ISBN: 0764571354
EAN: 2147483647
Year: N/A
Pages: 116
Authors: Joe Duffy

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