.NET Instrumentation Workshop
Home About Workshops Articles Writing Talks Books Contact

6. .NET 3.0/2.0 Trace Source

Trace Listeners gather information passed to them and store the data. In .NET 1.0 and 1.1 the mechanism to generate trace information was to use static members on the Trace or, preferably, the Debug class. However, this information was simply a single line message (possibly concatenated with the detailed message), or a multi line message for asserts. This mechanism is still available in .NET 3.0/2.0. In addition, .NET 3.0/2.0 also provides a class called TraceSource that gives far more information about the process at the time that the message was generated. This class has more scope for customization and for this reason the methods to generate messages are instance methods, which means that you can have several trace sources in your application, each with a different configuration.

6.1 TraceSource

To generate trace messages with Trace or Debug you call static members. These classes delegate their work to a single class called TraceInternal. This means that the trace configuration is global to the application domain. So once you have set the indentation size, that size will be used by all tracing calls, regardless of the class, thread or assembly where the call was made.

In .NET 3.0/2.0 trace sources were introduced to address two issues. The first issue is the amount of tracing data that is generated. TraceInternal will simply pass the message (and if provided, the detailed message) to the trace listeners. TraceSource will add extra information to the message, information like the time, the process ID and thread ID. The other issue is to remove the global aspect of tracing in 1.0 and 1.1 and make it more granular. To do this, the tracing methods are now instance methods, so they must be called on trace source objects, and each trace source object has its own configuration, including its own collection of trace listeners.

The TraceSource class has three methods (each one overloaded) to trace messages: TraceData, TraceEvent and TraceInformation, which will be covered in detail later. Note that all of these methods are conditional on TRACE. That's right, to use the TraceSource class you must define the TRACE symbol. I would have preferred the methods to be conditional on DEBUG but I guess TRACE seemed more logical given the name of the class.

Note also that the constructors do not have the [Conditional] attribute, and neither do the properties, because only methods can have this attribute. Furthermore, the Flush method (which only makes sense if you have generated one or more messages) also does not have the [Conditional] attribute. This means that you are able to create an instance of the TraceSource class in an application that does not have the TRACE symbol defined and you can call Flush on it, but you cannot call any of the object's useful methods. If you want to prevent this object from being created in this situation you have to use conditional compilation. This can make your code rather messy.

The most generic method on TraceSource is TraceEvent, and the most generic overload is this one:

public void TraceEvent (TraceEventType eventType, int id, string format, params object[] args);

The last two parameters are used to create the trace message: format is a format string with WriteLine-like placeholders that are replaced with formatted values from the args parameter. This is marked as params and so C# treats the object array like varargs, (which it isn't, of course). The id parameter is whatever you choose it to be, there is no compulsion that a particular ID is associated with a particular format message. When I first saw this method I thought that it had been created this way so that the EventLogTraceListener would be able to log messages to the event log in a proper way. However, I should have realised that the people who wrote the EventLog class do not really understand how this service works. I say this because the event ID identifies the format string, so that would make the format parameter redundant. In fact, the implementation of EventLogTraceListener does not use the ID in any way that can be considered correct and it merely generates a single string like this:

string message = string.Format(format, args);

EventLogTraceListener then passes this and the ID to the event log as usual with the WriteEvent method. A nasty and lazy implementation.

The first parameter of TraceEvent is an enumeration that gives the severity of the message. This encompasses the severities that are possible with the event log, and it provides seven more:

public enum TraceEventType
{
   Critical    = 0x0001,
   Error       = 0x0002,
   Warning     = 0x0004
   Information = 0x0008,
   Verbose     = 0x0010,
   Start       = 0x0100,
   Stop        = 0x0200,
   Suspend     = 0x0400,
   Resume      = 0x0800,
   Transfer    = 0x1000,
}

Note that this does not have [Flags] and so the values cannot be combined. The first four values refer to the state of the application, indicating a crash, an exception, a warning or just some information message. The Verbose value indicates that the message is a verbose information message. The next four values indicate that some logical operation has started, stopped, has been suspended or resumed. The final value indicates a change in the correlation, which will be explained later.

To test this out create a file (app.cs) and add the following:

#define TRACE

using System;
using System.Diagnostics;

class App
{
   static void Main()
   {
      TraceSource source = new TraceSource("Default Source", SourceLevels.All);
      source.Listeners.Clear();
      source.Listeners.Add(new ConsoleTraceListener());
      source.TraceEvent(TraceEventType.Information, 0, "this is the message");
   }
}

Note that the TRACE symbol must be defined. The code creates a trace source called Default Source and it will be used to trace all message types. The second parameter sets the Switch property which can be changed during execution. The Switch value determines whether the trace source will pass the message to the trace listeners. This is, if you like the first level of filtering that is possible with trace sources.

The values of SourceLevels are shown here:

[Flags]
public enum SourceLevels
{
   Off             = 0x0000,
   Critical        = 0x0001,
   Error           = 0x0003,
   Warning         = 0x0007,
   Information     = 0x000f,
   Verbose         = 0x001f,
   ActivityTracing = 0xff00,
   All             = 0xffff
}

Notice that it is marked with [Flags] which suggests that the values should be treated as bit fields, that is, they can be combined. However, this is nonsense because Error contains the bit of Critical (is an error critical?); Warning contains the bits of Error and Critical; Information contains the bits of Warning, Error and Critical; and Verbose contains the bits of them all! So ignore [Flags] and treat the values as if they are appended with AndMoreSevere, that is treat SourceLevel.Warning as being WarningAndMoreSevere (that is, Warning, Error and Critical). When you log an event the class will perform a bit comparison of the Switch value with the type of the event being reported and if the appropriate bits are set then the event is logged. Note that to be able to report Start, Stop, Suspend, Resume or Transfer messages the trace source must be set to ActivityTracing (or All).

The next line of the sample code adds a trace listener to the Listeners collection of the source. Be aware that you will have an instance of DefaultTraceListener by default. Note the warnings I gave earlier in this workshop that DefaultTraceListener has some nasty side effects that in most cases you do not want. This is why I added the line to clear the Listeners collection so that you only get a DefaultTraceListener instance if you explicitly add one.

Finally, the code logs an information message to the source. Compile this code and run it, the results should be:

Default Source Information: 0 : this is the message

Now change the code to use a DelimitedListTraceListener:

source.Listeners.Add(new DelimitedListTraceListener("app.log"));
source.TraceEvent(TraceEventType.Information, 0, "this is the message");
source.Flush();

Since the delimited list will be sent to a file you have to ensure that the trace listener is flushed, hence the last line. Compile and run this code. You should get something like this (type app.log):

"Default Source";Information;0;"this is the message";;;;;;;

The default delimiter is a semicolon, but you can change this by using a constructor that takes a string as the second parameter or by changing the Delimiter property. Notice all of those semicolons at the end: clearly there are values that the trace listener is printing out that have null values.

6.2 Trace Options

The extra values shown by the delimiter list in the last section are generated by the trace listener rather than the trace source. These values are generated by the trace listener if it is instructed to do so through the listener's TraceOutputOptions property which is a combination of values from TraceOptions enumeration:

[Flags]
public enum TraceOptions
{
   None                  = 0x00,
   LogicalOperationStack = 0x01,
   DateTime              = 0x02,
   Timestamp             = 0x04
   ProcessId             = 0x08,
   ThreadId              = 0x10,
   Callstack             = 0x20,
}

This enumeration has the [Flags] attribute, so you can combine them. The values you are most likely to use are DateTime and ThreadId, so that you have a record of when the event was generated and which thread generated the event (assuming you have a multi threaded app). If you have multiple processes in your application then you can use ProcessId, however, note that to make this useful the different processes will have to log to a shared resource, which effectively means the event log or the output debug stream. (Files are not shareable between processes.) The Timestamp value is a long value that represents the current tick count of the system clock. This is useful to order events.

The two stack values are interesting. As the name suggests, Callstack provides the current call stack, returned by calling Environment.StackTrace. In fact the implementation is rather dumb. If you log from the Main method you'll get something like this:

"Default Source";Information;0;"this is the message";;;;;;;
   " at System.Environment.GetStackTrace(Exception e, Boolean needFileInfo)
   at System.Environment.get_StackTrace()
   at System.Diagnostics.TraceEventCache.get_Callstack()
   at System.Diagnostics.DelimitedListTraceListener.WriteFooter(TraceEventCache eventCache)
   at System.Diagnostics.DelimitedListTraceListener.TraceEvent(
      TraceEventCache eventCache, String source, TraceEventType eventType, Int32 id, String message)
   at System.Diagnostics.TraceSource.TraceEvent(TraceEventType eventType, Int32 id, String message)
   at App.Main()"

All I am interested in is where the TraceEvent method was called, I am not interested in the entire stack! If I call TraceEvent deep in my call stack then it would be easy to lose where the TraceEvent method was called. Furthermore, the string contains newline characters and hence this messes the formatting of the messages if ythey are logged to the event log, or to a file. For these reasons I regard this option as being less than useful.

The other stack option is LogicalOperationStack. This logical stack is managed by the correlation manager, which is accessed through the Trace class.

To test out these values, change the code from the last section to these lines:

TraceListener tl = new ConsoleTraceListener();
dl.TraceOutputOptions = (TraceOptions)0x1e;
source.Listeners.Add(tl);
source.TraceEvent(TraceEventType.Information, 0, "this is the message");
source.Flush();

This will print the trace messages to the console. The trace options are: DateTime, Timestamp, ProcessId and ThreadId. Compile and run this. You'll get a result like this:

Default Source Information: 0 : this is the message
   ProcessId=1440
   ThreadId=1
   DateTime=2006-07-01T15:19:43.1325056Z
   Timestamp=1262251704995

Notice that the extra trace options are given after the main trace message; this is because they form the footer of the traced event.

The logical operation stack is created by starting a new logical operation. To do this you access the correlation manager through the Trace class. Make following changes:

TraceListener tl = new ConsoleTraceListener();
dl.TraceOutputOptions = (TraceOptions)0x1;
source.Listeners.Add(tl);
Trace.CorrelationManager.StartLogicalOperation("operation one");
source.TraceEvent(TraceEventType.Information, 0, "this is the message");
Trace.CorrelationManager.StopLogicalOperation();
source.Flush();

Here, you just want to trace the logical operations, and you tell the correlation manager to start operation one before tracing the message, and stop the logical operation after the trace. Compile the code and run it. The result will be:

Default Source Information: 0 : this is the message
   LogicalOperationStack=operation one

Logical operations are nested. To see how this works, change the code so that the source is a static member:

static TraceSource source;
static void Main()
{
   source = new TraceSource("Default Source", SourceLevels.All);
   source.Listeners.Clear();

Next, add two static methods:

static void MethodOne()
{
   Trace.CorrelationManager.StartLogicalOperation("operation two");
   MethodTwo();
   Trace.CorrelationManager.StopLogicalOperation();
}
static void MethodTwo()
{
   Trace.CorrelationManager.StartLogicalOperation("operation three");
   source.TraceEvent(TraceEventType.Information, 0, "this is the message");
   Trace.CorrelationManager.StopLogicalOperation();
}

Finally, change Main so that it calls MethodOne:

Trace.CorrelationManager.StartLogicalOperation("operation one");
MethodOne();
Trace.CorrelationManager.StopLogicalOperation();
source.Flush();

Compile and run this code. The result should be something like this:

Default Source Information: 0 : this is the message
   LogicalOperationStack=operation three, operation two, operation one

Notice how the logical stack is in reverse order, that is, the string from last call to StartLogicalOperation appears first. If you call StartLogicalOperation without a string then a GUID will be generated and the value of that GUID will be printed instead. The correlation manager maintains a thread based logical operation stack, so each thread in the process that logs an event with this trace source will show its own logical call stack. Note that the parameter of StartLogicalOperation is an object, so you can provide any type of object, but be aware that the footer calls ToString on this object, so this method must return something meaningful.

6.3 Tracing Methods

There are three methods on TraceSource to trace data: TraceEvent, TraceData and TraceInformation. TraceEvent has already been covered. TraceData has two overloads and both take a TraceEventType and an ID. The difference is that one overload takes an object and the other takes an array of object (marked as params). Again, this looks as if it could be useful for passing data to the event log. However, this is not the case. TraceData calls a similarly named method on each trace listener and the implementation of the trace listeners in the framework will call ToString on the object. If the method is called with an array of objects then a comma separated list will be created by calling ToString on each object. The EventLogTraceListener class behaves ;like this too, so yet again, the writers of this class have failed to create a method that works correctly.

TraceInformation takes either a string, or a format string and a params array of objects that will be placed in the format string. This method merely calls TraceEvent passing the string (or the formatted string) and an ID of 0 and a message type of TraceEventType.Information.

The Trace has three static methods not covered so far: TraceInformation, TraceWarning and TraceError. These methods will generate information, warning and error messages by calling TraceEvent on all the trace listeners in the application domain's Listeners collection. Since a trace source is not used it means that there is no Switch settings and hence there is no check on the event's type to determine if it should be traced.

6.4 Configuration

So far I have showed how you can instantiate trace sources. They become far more useful when they are instantiated through entries in a configuration file.

To test out basic configuration create the following application (app.cs) and compile it:

#define TRACE
using System;
using System.Diagnostics;

class App
{
   static TraceSource trace =
      new TraceSource("TestSource", SourceLevels.All);
   static void Main()
   {
      TraceListener tl = new ConsoleTraceListener();
      trace.Listeners.Add(tl);
      trace.TraceEvent(TraceEventType.Warning, 0, "Test Message");
   }
}

Compile and run this code. You should find that the message will appear on the command line.

The SourceLevels.All value indicates that the source will log events of all severities. Remove the SourceLevels parameter from the constructor of the static member, so that the TraceSource  constructor with just one string parameter is called: this is the name of the source, TestSource.

static TraceSource trace = new TraceSource("TestSource");

Compile and run this application. This time the message will not appear on the command line because the Switch property of the TraceSource is not being set. Create a configuration file (app.exe.config):

<configuration>
   <system.diagnostics>
      <switches>
         <add name="TestSource" value="All"/>
      </switches>
   </system.diagnostics>
</configuration>

Notice that this entry has the same name as the trace object. The value is the string value of a SourceLevels member. Run the application. This time you'll see the message. To show that it is this switch value that is controlling whether the event is logged, change the value to Off and run the application again. This time the message won't be logged. Change the value back to All.

Now remove the code that adds the ConsoleTraceListener to the trace source so that Main now looks like this:

static void Main()
{
   trace.TraceEvent(TraceEventType.Warning, 0, "Test Message");
}

Compile the code.

You now need to change the configuration file so that the trace listener is specified. To do this, add the following to the <system.diagnostics> element:

<sources>
   <source name="TestSource">
      <listeners>
         <add name="Console" type="System.Diagnostics.ConsoleTraceListener"/>
      </listeners>
   </source>
</sources>

The <source> entry refers to our TraceSource object using its name. Run the application again to confirm that it still logs a message to the command line.

As you can see, each source has its own <listeners> collection. This might work well with how you've designed you application. On the other hand, you may decide that you would prefer several sources to share the same trace listener. To do this you need to add a <sharedListeners> collection under the <system.diagnostics> element:

<sharedListeners>
   <add name="Console" type="System.Diagnostics.ConsoleTraceListener"/>
</sharedListeners><

The name is important, because it is referred to in the <listeners> collection of the source. Remove the type attribute, so that the source looks like this:

<source name="TestSource">
   <listeners>
      <add name="Console"/>
   </listeners>
</source>

Run the application again and confirm that the message appears on the command line, showing that the source uses the shared listener.

In this application the trace source Switch property is initialized with the value of the <switch> element that has the same name as the <source> entry and the Name property of the TraceSource object in the application. It is worth pointing out that the <source> element has an attribute called switchName, so that you can supply the source level through a switch other than one with the same name as the trace source, like this:

<source name="TestSource" switchName="level">
   <listeners>

You can now change the switch:

<switches>
   <add name="level" value="All"/>
</switches>
 
You can provide a switchType attribute to the <source> entry if you want to use a class other than SourceSwitch. The only suitable class provided by the framework is SourceSwitch itself, and if you want to provide a replacement then be aware that this class will be used to initialize the TraceSource.Switch property and therefore it must be a class derived from SourceSwitch and it must have a constructor with a string parameter that will be passed to the base class to initialize the DisplayName property.

Keep the sample code, it will be used in the next section.

6.5 Attributes

If you read the documentation for the <source> element you'll see that it mentions an attribute called extraAttribute. In fact this is not necessarily the name of the attribute, it is just a placeholder. A class derived from TraceSource can support any number of attributes. To do this you need to derive from TraceSource and provide the names of the additional attributes through the protected GetSupportedAttributes method.

This sample uses the files from the last section as a starting point (app.cs and app.exe.config). Add a new class to the process:

class RestrictedTraceSource : TraceSource
{
   public RestrictedTraceSource(string name)
      : base(name)
   {
   }
   protected override string[] GetSupportedAttributes()
   {
      return new string[] { "ignore", "Ignore" };
   }
   public new void TraceEvent(TraceEventType type, int id, string msg)
   {
      string ignore = null;
      if (Attributes.ContainsKey("ignore"))
      {
         ignore = Attributes["ignore"];
      }
      else if (Attributes.ContainsKey("Ignore"))
      {
         ignore = Attributes["Ignore"];
      }
      else
      {
         base.TraceEvent(type, id, msg);
         return;
      }
      int minimum = Int32.Parse(ignore);
      if (id <= minimum)
      {
         base.TraceEvent(type, id, msg);
      }
   }
}

This class indicates that it supports an attribute called ignore (with two alternate spellings). This is used in the TraceEvent method that hides the inherited version. TraceEvent treats this attribute as a number and will only trace a message if the ID is less than or equal to the ignore attribute. Change the code so that the new trace source is used, and change the ID to 10:

static RestrictedTraceSource trace = new RestrictedTraceSource("TestSource");
static void Main()
{
   trace.TraceEvent(TraceEventType.Warning, 10, "Test Message");
}

Compile and run this code. Since the configuration file does not have the ignore attribute the message will be generated. Now change the configuration file to ignore messages over 5:

<source name="TestSource" switchName="level" ignore="5">

This time you'll find that the message is not generated. change the ignore value to 20 and run the application again. The message will be generated.

You can use attributes for whatever use you choose, however, note that there is a better mechanism available to filter messages.

6.6 Filters

Trace listeners can use filters to determine if a message should be traced. A filter derives from the abstract TraceFilter class and provides an implementation of the ShouldTrace method:

public abstract bool ShouldTrace (
   TraceEventCache cache, string source, TraceEventType eventType,
   int id, string formatOrMessage, Object[] args, Object data1, Object[] data);

The following table is a summary of these parameters:

Parameter Description
TraceEventCache cache Information about the event message, this contains the information used in the footer, like the time and date, thread and process ID.
string source The Name property of the source
TraceEventType eventType The 'severity' of the message
int id The (arbitrary) identifier for the message
string formatOrMessage The message, or the format string with placeholders, (depending on the trace message called)
Object[] args The data to be inserted into the format string placeholders
Object data1 A trace data object
Object[] data An array of trace data objects

There are so many parameters because there are so many different ways to log a message. TraceSource has TraceEvent and TraceData, and there are various overloads of these methods. ShouldTrace is called when any of these overloads is called and the parameters that are absent are passed as null values.

A simple implementation of TraceFilter is provided by the framework. This is called SourceFilter. The class is initialized with the name of a source (which is also accessible through its Source property) and ShouldTrace will return true only if the message is generated by the specified source. This means that if you have multiple trace sources you can indicate that a particular listener will only handle the messages from the specified source. The <filter> child element is available for listeners in the <listeners> collection for Trace and Debug (ie the global <listeners> collection under <trace>), the <sharedListeners> collection and the <listeners> collection for each source (although in this last case SourceFilter seems unnecessary).

Create a file (app.cs) and give it the following code:

#define TRACE
using System;
using System.Diagnostics;

class App
{
   static TraceSource traceOne = new TraceSource("SourceOne");
   static TraceSource traceTwo = new TraceSource("SourceTwo");
   static TraceSource traceThree = new TraceSource("SourceThree");
   static void Main()
   {
      traceOne.TraceEvent(TraceEventType.Information, 1, "Test Message One");
      Two();
   }
   static void Two()
   {
      Three();
      traceTwo.TraceEvent(TraceEventType.Information, 1, "Test Message Two");
   }
   static void Three()
   {
      traceThree.TraceEvent(TraceEventType.Information, 1, "Test Message Three");
   }
}

This creates three trace sources which are used to trace messages in the application. Now create a configuration file (app.exe.config):

<configuration>
   <system.diagnostics>
      <sources>
         <source name="SourceOne">
            <listeners>
               <add name="Console"/>
            </listeners>
         </source>
         <source name="SourceTwo">
            <listeners>
               <add name="Console"/>
            </listeners>
         </source>
         <source name="SourceThree">
            <listeners>
               <add name="Console"/>
            </listeners>
         </source>
      </sources>
      <switches>
         <add name="SourceOne" value="All"/>
         <add name="SourceTwo" value="All"/>
         <add name="SourceThree" value="All"/>
      </switches>
      <sharedListeners>
         <add name="Console" type="System.Diagnostics.ConsoleTraceListener">
            <filter type="System.Diagnostics.SourceFilter" initializeData="SourceOne" />
         </add>
      </sharedListeners>
   </system.diagnostics>
</configuration>

Compile the code and then run it. You'll see that you get only one message, the message is generated by SourceOne in Main. The reason is that the filter for the shared listener only allows messages from SourceOne. Change this entry in the configuration file to SourceTwo and repeat the test. Finally change the entry to SourceThree and repeat the test. Each time you'll see that only the message from the specified source will be generated.

This raises the question: how do you trace from two out of the three sources? Try adding a second <filter> to the shared listener, one each for SourceTwo and SourceThree. Run the code. You should find that a ConfigurationErrorsException will be thrown. The reason is that you can only have one filter for each listener. You have three options. First, for this particular example you can use the <switches> collection to have the same effect, in this case remove the <filter> entry and edit the <switches> to look like this:

<switches>
   <add name="SourceOne" value="Off"/>
   <add name="SourceTwo" value="All"/>
   <add name="SourceThree" value="All"/>
</switches>

Run the application to confirm that you get the results you expected.

However, this is cheating somewhat because it does not use filters. The problem is that a trace listener can only have one filter, and because of the way that SourceFilter works if you have a shared listener then the filter only allows message from one source. This is your second option. To filter out just one of the three you need to use a trace listener for each source. Return the switches back to how they were before and change the sources to this:

<source name="SourceOne">
   <listeners>
      <add name="Console" type="System.Diagnostics.ConsoleTraceListener">
         <filter type="System.Diagnostics.SourceFilter"
                 initializeData="SourceOne" />
      </add>
   </listeners>
</source>
<source name="SourceTwo">
   <listeners>
      <add name="Console" type="System.Diagnostics.ConsoleTraceListener">
         <filter type="System.Diagnostics.SourceFilter"
                 initializeData="SourceTwo" />
      </add>
   </listeners>
</source>
<source name="SourceThree">
   <listeners>
      <add name="Console" type="System.Diagnostics.ConsoleTraceListener">
         <filter type="System.Diagnostics.SourceFilter"
                 initializeData="SourceThree" />
      </add>
   </listeners>
</source>

This appears obvious, the filter for SourceOne says that only messages for SourceOne will generated, the same for SourceTwo and SourceThree. Run the application and confirm that all three messages are generated. Now change the filter for SourceOne so that only messages generated for source doesNotExist will be generated. Note that it does not matter that they all have the same name.

I would prefer to use a shared listener and use a filter that takes more than one source. Your third option, and the best of the three, is to write your own filter. Add the following code to the process:

class MultipleSourceFilter : TraceFilter
{
   string[] sources;
   public MultipleSourceFilter(string s)
   {
      sources = s.Split(new char[]{ ',', ' ' });
   }
   public override bool ShouldTrace(
      TraceEventCache cache, string source, TraceEventType eventType,
      int id, string formatOrMessage, Object[] args, Object data1, Object[] data)
   {
      foreach (string s in sources)
      {
         if (s == source) return true;
      }
      return false;
   }
}

This takes a comma separated list of sources. ShouldTrace checks this list to see if it contains the source. Return the configuration file back to the text where the sources all use the shared listener Console. Now indicate that you want the shared listener to use the custom filter:

<sharedListeners>
   <add name="Console" type="System.Diagnostics.ConsoleTraceListener">
      <filter type="MultipleSourceFilter,app" initializeData="SourceTwo, SourceThree" />
   </add>
</sharedListeners>

Compile the application and then run it. This time you'll find that messages from SourceTwo and SourceThree will be generated.

6.7 Work In Progress

I regard tracing in .NET as work in progress: what we have got is fine, but it could be so much better. The frustrating thing is that Microsoft had a wonderful tracing system in Visual Studio 7.0 and yet the .NET team seemed to have 're-invented the wheel' and come up with an inferior copy. Of course, I am referring to ATL tracing. .NET tracing has some features that ATL doesn't - trace listeners and customizable filters are the two best features of .NET tracing - but the .NET diagnostics designer could have made tracing so much better if they took a look at how ATL does the same thing.

With ATL tracing in mind, here's a wish list for .NET tracing:

  • Tracing on an assembly basis. The footer does not contain assembly information, and thus TraceFilter.ShouldTrace is not passed assembly information. To trace on a per-assembly basis you need to create a distinct trace object in every assembly and name it appropriately.
  • Stack dump sucks. What would be far more useful is the name of the method and the source code line.
  • Dynamic configuration. Yes, you can call Trace.Refresh, but why should you have to? Why can't the tracing system monitor the configuration file on your behalf and update the tracing configuration accordingly?

It would be nice if features like these appeared in the next version of the framework.

I hope that you enjoy this tutorial and value the knowledge that you will gain from it. I am always pleased to hear from people who use this tutorial (contact me). If you find this tutorial useful then please also email your comments to mvpga@microsoft.com.

Errata

If you see an error on this page, please contact me and I will fix the problem.

Page Seven

This page is (c) 2007 Richard Grimes, all rights reserved