A Guide to Controlling Trace Output
As a summary to the vital concepts you have been reading about, Table 8 collects reference links that you may need. Don't remember the difference between a TraceSwitch and a TraceSource? Check this table. Need to know how to define something in the configuration file? Check this table!
Table 8: Key .NET Diagnostic Concepts: This table provides a brief description, plus links on the left to passages in the .NET Framework Developers Guide, and references on the right to the formal documentation.
Author's Note: Though I have not used it in the app.config file, for completeness the fifth and last component of controlling what you log here: set each Listener to the appropriate output level. Recall that you could set the level of the TraceSwitch for a TraceSource to control the amount of output to all TraceListeners operating under the auspices of that TraceSource. You can further refine that by setting each TraceListener's output individually with a child <filter> element. You will see an example of this in the next section as well. |
Interested in designing your own TraceListeners? Take a look at the source code for the AlignedTextWriterTraceListener referenced in this project, or the TextBoxTraceListener discussed in the next section. A couple other tutorials specifically on customization are also available:
CLR Inside Out: Customized Tracing and
Custom Trace Listener.
Build a Trace Output Comparison Sandbox
 | |
Figure 4. Diagnostic Sandbox: This is the UI for the TraceSourceGuiApp that lets you experiment with any combination of source levels, trace options, and trace listeners. |
It helps when you can actually compare the output of each TraceListener, using identical diagnostic output. The next application provides a sandbox tool so you may do this yourself. You'll also see snapshots of each TraceListener's output.
Figure 4 shows the UI for the tool. The top half enumerates all source levels, trace options, and trace listeners. The bottom half contains a large TextBox used for user feedback; it's initially loaded with a dump of the main TraceSource used by the application. You can get a current snapshot at any time by pressing the Dump button.
All the radio buttons and check boxes are populated dynamically. The first two group boxes will probably not change much over time, but they may when a new release of the framework comes out. The third group box—the trace listeners—is populated based on the contents of the
app.config file and/or any TraceListeners that you create programmatically. Each TraceListener gets a check box in the trace listener group, but its configuration will also be listed in the output area. This configuration reflects the initial configuration from the
app.config file, plus any changes you have made programmatically before the form actually opens.
Given the preliminaries you learned in the earlier parts of this article, the user interface of this tool should be reasonably evident. If you want to refresh yourself:
- Source Levels—see Table 4
- Trace Options—see Table 6 (the shaded rows)
- Trace Listeners—see Table 5 (except for the TextBox).
 | |
Figure 5. TraceListener Output: Each TraceListener includes a specification for where its output goes. The file names are detailed in the application's configuration file. If you want to change one, you can edit the configuration file without needing to recompile the application. |
All the TraceListeners except the TextBox type are detailed in Table 5. The TextBox TraceListener exists in the sandbox project only; it allows you to send diagnostic output directly to the text box of this application.
Working with the application is simple: select a source level, any number of trace options, and any number of trace listeners, then press Go. The application then exercises each of the common trace event types (see Table 2) for all TraceListeners you have selected. The output of each trace listener goes to a different destination, as shown in
Figure 5. Note that output from the Console TraceListener will not be available unless you are running inside Visual Studio, because the application is a graphical rather than a console application.
Table 9 shows the output from each TraceListener responding to the same trace events with the same output settings. You can reproduce equivalent results by selecting
All for the source level;
DateTime,
ProcessId, and
ThreadId for trace options (my preferred combination); and all the trace listeners listed. The more expansive formats have linked figures showing more complete samples. Note that there is no requirement for all TraceListeners to use either the same source level or the same output options; the TraceSourceGuiApp just happens to work that way for easier "apples to apples" comparisons.
Table 9. TraceListener output samples: Thumbnail samples of all TraceListeners are shown here; refer to the individual figure references indicated for larger samples of the more expansive formats.
TextWriter and Console output sample (see Figure 6):
 |
DelimitedList output sample:
 |
AlignedText and TextBox output sample: |
XmlWriter output sample (see Figure 7):
 |
Event Log output sample:
 |
 | |
Figure 6: Full TextWriter and Console Output Sample: Selected trace options are output one per line. |
|
 | |
Figure 7: Full XmlWriter Output Sample: Your diagnostic message maps to the <ApplicationData> element, and your selected trace options are emitted as XML attributes of the <Execution> element. |
|
|
 | |
Figure 8: Application configuration file: The highlighted items are: (1) the two TraceSource names, which you use to reference the TraceSources from your code; (2) the output files passed to constructors of a couple TraceListeners; and (3) a filter that acts as a single-listener-specific switch. |
For brevity, this article won't walk through the
TraceSourceGuiApp code, but it will discuss the key points. First, take a look at the configuration file (see
Figure 8). For educational purposes, the application uses two TraceSources. The previous application used one in the main program and another in an external library. Here, the main program uses both. Each uses the same set of seven listeners: six are defined in the configuration file as shown here and the remaining TextBox listener is defined programmatically.
Most TraceListeners have a variety of constructors, but the configuration file does not support all of them—it supports only those that take string arguments. As the configuration sample shows, you pass the string arguments to the constructors in the
initializeData attribute. Typically, this will be a file name. That means that you can't initialize a constructor that uses a Stream, a TextBox, or any other object, in the configuration file. That does not necessarily prevent you from
instantiating all your listeners in the configuration file. You could define a listener here using some dummy string arguments, and then update it during your program initialization to configure it completely.
Another new feature introduced here is the
<filter> child attached to most of the listeners. It is not strictly necessary, because it's manipulated in the code, but including it here illustrates where it would be used. The combination of the source level of the switch and the source level of the filter determines the trace listener's ultimate output.
The earlier discussion of the TraceSourceDemoApp application mentioned that you should generally use shared listeners rather than private listeners if you want seamless integration for your separate modules. I stated that shared listeners use the same instance of a listener. That is only
partly true. (I suspect it is a bug and
should have been completely true.) Using a shared listener lets all consumers write to a single file. That's true. Using a shared listener lets you set the TraceOutputOptions once and everyone gets access to them. That's true. But then there's the common indentation issue. When different TraceSources are in the
same file, such as in this TraceSourceGuiApp, they have common indentation. When they are in
different modules, such as in the TraceSourceDemoApp, they do
not have common indentation. As I was designing my custom AlignedTextWriterTraceListener I discovered the indentation was corrupted, so I had to build in a fix to make the indentation global. Table 10 summarizes the differences between shared and local (or private) TraceListeners.
Table 10: Shared vs. Local Listener Summary: The table summarizes the differences between shared and local listeners.
Shared Listener |
Local (Private) Listener |
Common TraceOutputOptions |
Independent TraceOutputOptions |
Common output file |
Separate output files |
Independent indentation control (except for AlignedTextWriterTraceListener) |
Independent indentation control |
The configuration file connects to your program by the names of the
<source> elements as you have seen. In the TraceSourceGuiApp constructor you see where these TraceSources are initialized with names corresponding to the configuration names. This application is again using StructuredTraceSources instead of TraceSources to take advantage of the extra program flow tracing methods they offer. The constructor also builds a list of its TraceSources so that when a user chooses the settings, they may be applied to both TraceSources. The final statement sets up the TraceListener that is used to channel output into the output pane of the application. This TraceListener is defined in
InitializeTextBoxListening, specifying the TextBox it will send output to and a name to reference it. The second line programmatically turns the listener off via its
Filter property, the programmatic corollary to how you saw it done in the configuration file. The final line adds this listener to both TraceSources—just as in the configuration file, all the other listeners were added to both TraceSources.
public TraceSourceGuiForm()
{
InitializeComponent();
AlignedTextWriterTraceListener.SourceNameLength = 16;
tracer = new StructuredTraceSource("TraceSource_Main");
tracer2 = new StructuredTraceSource("TraceSource_Secondary");
sources = new TraceSource[] { tracer, tracer2 };
InitializeTextBoxListening();
}
private void InitializeTextBoxListening()
{
TextBoxTraceListener l = new TextBoxTraceListener(
diagTextBox, "TextBox");
l.Filter = new EventTypeFilter(SourceLevels.Off);
foreach (TraceSource source in sources)
{ source.Listeners.Add(l); }
}
Pressing the Go button invokes the event handler shown below is invoked. This method first sets the source level for both TraceSources based on the user's selected settings. Next, it sets the
TraceOutputOptions of each listener to the set of options the user chose. That method walks through the list of listeners, setting each one either on or off depending on the checked choices. The
ExerciseTraces method simply invokes a series of
TraceEvents. Finally, the method flushes the output so all listeners will write to their respective media for you to view.
private void goButton_Click(object sender, EventArgs e)
{
// Set the user's selected level.
foreach (TraceSource source in sources)
{ source.Switch.Level = GetSourceLevel(sourceLevelGroupBox); }
// Set the user's selected output options.
SetSelectedOptionsForListeners(
BuildTraceOptionsList(traceOptionsGroupBox));
ExerciseTraces();
foreach (TraceSource source in sources)
{ source.Flush(); }
}