Instrumenting an application with tracing has become increasingly sophisticated as the .NET framework has matured. Find out how to use tracing in your applications, how to fine-tune tracing to your needs with custom listeners, and how to gain field-level and robust formatting control over the output.
by Michael Sorens
Mar 14, 2008
Page 3 of 6
Connecting Diagnostics Between Your Application and External Libraries
The SomeCalc method called inside ProcessLevel2 is a static method defined in the SomeLibClass shown next. (Whether it is a static or an instance method is unimportant.) This small class is contained in a separate library defined in a separate Visual Studio project called TraceSourceDemoLibrary, which generates a DLL referenced in the current project. (The TraceSourceDemoLibrary could just as easily have been in a separate solution.) The point is that the coupling between the current project and the library is minimal. Here is the entire class:
public class SomeLibClass
private static StructuredTraceSource tracer
= new StructuredTraceSource("DemoSecond");
public static void SomeCalc()
tracer.TraceInformation("stuff in a library module");
There are three ways the SomeLibClass differs from the TraceSourceDemoApp project:
Both classes define a static StructuredTraceSource, but where the TraceSourceDemoApp project referenced the "DemoMain" TraceSource, the TraceSourceDemoLibrary references the "DemoSecond" TraceSource.
The SomeLibClass code declares and initializes the tracer variable in one statement, while the main application initialized it inside a static constructor. Why the difference? In this case the application doesn't have to execute any statements before instantiating the StructuredTraceSource.
This library uses a different form of the StructuredTraceSource constructor, one that does not include TraceOptions. The main application included some settings of the listener both before instantiating the StructuredTraceSource and during instantiation, while this one does not. That's because by the time the SomeCalc method gets called (and therefore this class is first accessed, triggering static initialization), the demo application has already output a number of diagnostic lines. If this library class were to set trace output options in mid-stream, nothing would prevent those options from being different than those used by the main program—which is generally undesirable: all diagnostic output from an entire application should be consistent for ease of interpreting the results. Figure 3 clarifies this point. The figure shows diagnostic output from multiple sessions of this TraceSourceDemoApp, highlighting the point where the main application connects to a TraceSource and where the library connects to a TraceSource.
Figure 3. Diagnostic Output from an AlignedTextWriterTraceListener: All columns are aligned, irrespective of indentation and irrespective of source library.
In Figure 3, you can see that the library trace output integrates seamlessly in terms of indentation, because the constructor does not specify any output options; instead it just continues with those already established by the main application. This provides two subtle but important advantages:
The library needs to know nothing a priori about the application that will reference it.
The library does not affect output from the main application because it does not set any output options.
When you design library tracing this way, an application designer does not have to worry about side effects from any libraries used, and the library designer doesn't have to worry about which applications the library code might plug into.
The application and the library are connected, of course, but only by very loose coupling—the single argument to the StructuredTraceSource constructor that references a TraceSource from the app.config file (see Listing 1).
The <system.diagnostics> element in Listing 1 contains three children: <sources>, <switches>, and <sharedListeners>. Though they all appear as children of the <system.diagnostics> element, switches and shared listeners are subordinate to sources. A source has a name attribute you use to reference the source in your code; a switchName attribute, with which you reference a particular switch from the <switches> element; and a <listener> child element, with which you reference one or more listeners from the <sharedListeners> element. The first <source> element shown has the name "DemoMain," which is the name passed in the StructuredTraceSource constructor of the demo application. Similarly, the second <source> element has the name "DemoSecond," which is used in the StructuredTraceSource constructor of the library class. You can see this both by reviewing the code discussed earlier, and by observing the highlighted CREATE entries in Figure 3.
So, putting all this together, when you create an instrumented library, the name of its TraceSource needs to be part of its advertised API. Anyone (including yourself) who wishes to be a consumer of that library would then need only add a <source> element to the main applications app.config file to enable diagnostic output from that library.
This does not imply that there is a one-to-one mapping between libraries and TraceSources. The main demo application uses a single TraceSource. But if you have a large program and you want to be able to see diagnostic output from one portion of code but not from another, simply create two TraceSources. Or more, if you want even finer resolution over what will reach your diagnostic streams. Similarly, you may have zero or more TraceSources associated with each library that you create. This is the first component of controlling what you log: create a separate TraceSource for each "channel" of output that you want to control.
To turn on output for a particular TraceSource, you set the value of the associated TraceSwitch in the app.config file. Note that app.config is relevant primarily within the confines of Visual Studio. Once you have compiled your program and you run it independent of Visual Studio, the app.config file gets a new name, which is your executable with a ".config" suffix appended; in this case, TraceSourceDemoApp.exe.config. Everything discussed about app.config in this article also applies to your xxx.exe.config file, with the further advantage that no recompilation is required.
The demo application works exclusively with the TraceEventType of Information, but in the next section you will see others in play. Take a look at the hierarchy of Table 4 (ignoring the ActivityTracing level). If you set the TraceSwitch to All, as you have seen, the application outputs Information events. If you set the TraceSwitch level to Verbose or Information, the application will still output all the Information event messages. But in this demo application Warning, Error, and Critical are just synonyms for Off because an Information event does not rise to the urgency required by any of those levels.
Table 4: Source Levels: Each source level accommodates one or more trace event types (adapted from SourceLevels Enumeration).
Trace Event Types
Stop, Start, Suspend, Transfer, Resume
Critical, Error, Warning
Critical, Error, Warning, Information
Critical, Error, Warning, Information, Verbose
This is the second component of controlling what you log: set each TraceSwitch to an appropriate level. You could, for example, set all your switches to Error as a default, but then set the switch for the new module that you are debugging to Verbose.