A Tracing Primer - Part II (B) [Mike Rousos]

In my introduction to tracing (https://blogs.msdn.com/bclteam/archive/2005/03/15/396431.aspx), I outlined the basics of how to use TraceSources, TraceListeners, and SourceSwitches to trace the flow of an application. I also covered how to configure Whidbey tracing with a configuration file.

In this series of three follow-up articles, I plan to discuss the use of TraceFilters, custom listeners, and a recent change we made to where config-defined listeners create their output files.

This second section (part B) will focus on creating custom listeners.

Custom Listeners

The listeners that are built into the System.Diagnostics library are useful, but one of the truly powerful features of System.Diagnostics tracing is the ability to easily create custom listeners that will output tracing information in whatever way the author deems suitable. At the most basic level, to create a custom listener, the developer need only create a class derived from TraceListener. 

 

There are various methods in TraceListener that either must be overridden, or can be useful to override.

 

At the highest level, the TraceListener type contains three TraceEvent methods, two TraceData methods, and one TraceInformation method. These are the methods that are called by the trace source when it needs to trace through a listener. Any or all of them can be overridden. The signatures are as follows:

  • void TraceEvent(TraceEventCache eventCache, String source, TraceEventType eventType, int id)
  • void TraceEvent(TraceEventCache eventCache, String source, TraceEventType eventType, int id, string message)
  • void TraceEvent(TraceEventCache eventCache, String source, TraceEventType eventType, int id, string format, params object[] args)
  • void TraceData(TraceEventCache eventCache, String source, TraceEventType eventType, int id, object data)
  • void TraceData(TraceEventCache eventCache, String source, TraceEventType eventType, int id, params object[] data)
  • void TraceTransfer(TraceEventCache eventCache, String source, int id, string message, Guid relatedActivityId)

Typically, some of these overloads will call others.

 

These methods, by default, will in turn call WriteHeader, WriteLine, and WriteFooter. 

 

WriteHeader calls the Write method to output the source name, the event type, and the event id. The format of this output, by default, is the same as that seen in the TextWriterTraceListener’s output.

 

WriteFooter, on the other hand, uses the write method to write out all of the optional trace output options that may be enabled on the listener (again, in the same format the TextWriterTraceListener would).

 

This brings us to the lowest level of TraceListener APIs – the Write and WriteLine methods.

  • void Write(string message)

  • void Write(object o)

  • void Write(string message, string category)

  • void Write(object o, string category)

  • void WriteLine(string message)

  • void WriteLine(object o)

  • void WriteLine(string message, string category)

  • void WriteLine(object o, string category)

     

Of those eight methods, six are virtual and by default will simply call through to Write(string message) or WriteLine(string message) respectively. The Write(string) and WriteLine(string) methods are the only two abstract methods in TraceListener. Hence, they must be overwritten.

 

An Example Listener

 

At the most basic level, a custom listener needs only override the Write and WriteLine methods that take strings. These methods will determine where the trace data is written. The other methods will, by default, simply pass the traced data as strings that would appear in a TextWriterTraceListener’s output to these methods. So, the following few lines are actually an entire custom listener:

 

using System;
using System.Diagnostics;

public class BinaryWriterListener : TraceListener
{
public BinaryWriterListener() { }

public override void Write(string message)
{
foreach (char c in message)
{
foreach (byte b in BitConverter.GetBytes(c)) Console.WriteLine(b.ToString("X") + " ");
}
}

public override void WriteLine(string message)
{
Write(message + "\n");
}
}

 

This listener will simply write the same output as the ConsoleTraceListener would (the default behaviors of TraceEvent, WriteHeader, etc. will be used) but whenever it writes a string to the screen, it will instead write the bytes that represent the characters of the tracing output.

To create a listener that writes this information to a file (as the TextWriterTraceListener does) would take only a few extra lines. There would have to be a constructor added that takes the file that the listener should write to and the write method would need to be changed to write to that file. Otherwise, the listener would be the same.

Of course, custom listeners can be used with configuration files as well. The sample config file below demonstrates how the above listener might be used (assuming that it was built into a library called customlistener.dll).

 

<?xml version="1.0" encoding="utf-8"?>
<configuration>
   <system.diagnostics>
      <sources>
            <source name="mySrc" switchName="mySwitch" >
            <listeners>
                  <add name="CustomListener" />
            </listeners>
          </source>
        </sources>
      <switches>
            <add name="mySwitch" value="Information" />
      </switches>
      <sharedListeners>
            <add name="CustomListener" type="CustomListener, customlistener, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null" />
      </sharedListeners>
    </system.diagnostics>
</configuration> 

 

Finally, I mentioned above that you could easily turn my sample listener into one that writes to a file. But, how would you instantiate something like that from a config file? Well, as with all trace listeners, you can add the initializeData attribute to the listener declaration and if that attribute’s value is not an empty string, the listener’s constructor that takes a string as a parameter will be called instead of the default constructor when the listener is instantiated. So, the following line will create a custom listener and will call a constructor that takes a single string parameter instead of the constructor that takes no parameters.

 

 

<add name="CustomListener" type="CustomListener, customlistener, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null" initializeData="out.dat" />

 

A Second Example

 

Although I am not going to go through the sample here, I will mention that there is an entirely different sort of custom listener that I wrote to ship as a sample with the Whidbey documentation. The above sample is great for simply deciding where the trace message is written, but to really change the format in which the data is given (as the XmlWriterTraceListener does), other methods will need to be overridden. For example, the XmlWriterTraceListener overrides the WriteHeader and WriteFooter methods (in addition to the Write and WriteLine methods) so that it can specify how the header information (detailing he source, id, event type, etc) will look (specifically, it is formatted as xml).

In my custom trace listener sample, I go one step beyond this and override the TraceEvent and TraceData methods. This allows me to take control the moment the listener receives a trace from a trace source. My listener is a whimsical ‘audio’ trace listener. I’ve instructed the TraceEvent and TraceData methods to call Console.Beep with varying frequencies and durations depending on the trace’s event type. I also allow the frequencies with which the listener beeps to be configurable by passing in custom attributes when the listener is created.

 

Custom Attributes on a Custom Listener

 

One snippet that I will show from that sample (because it’s important) is the overriding of the GetSupportedAttributes method. By default, a trace listener defined in a config file can only have a name of the listener, its type and (optionally) the initializeData field. If any other attributes are passed in, an exception will be thrown. If, however, your custom listener expects other attributes, it need only specify those in the GetSupportedAttributes method. Here is that method from my Audio Trace Listener sample.

 

// Any custom trace listener that can take custom attributes through a
// configuration file must override this method. The method should return
// an array of strings that represents all of the names that are allowed
// to appear in a configuration file under the definition of that listener
// type. If an attribute appears that is not either on this list or accepted by
// a basic trace listener, then a configuration exception will be thrown when
// the listener is created. Of course, this is only a list of all possible
// attributes, not all required attributes. So, if only some or none of these
// items appear in the configuration file, that will still be all right.
protected override string[] GetSupportedAttributes()
{
return new string[] { "CriticalFrequency", "ErrorFrequency", "WarningFrequency", "InformationFrequency",
"VerboseFrequency", "SuspendFrequency", "ResumeFrequency", "StartFrequency",
"StopFrequency", "TransferFrequency", "ToneLength", "FailFrequency" };
}

 

Once this method has been overridden, it is now acceptable for a config file to declare a listener like this (for example):

 

<add type="Microsoft.Samples.CustomTraceListener.AudioTraceListener, CustomTraceListener" name="AudioListener" traceOutputOptions="Timestamp" ToneLength="100"/>

 

These extra values can be accessed by simply indexing into the attributes by attribute name (for example, Attributes["ToneLength"] will return the ToneLength value as a string).

To find out whether or not an attribute has been defined (since all custom attributes are optional), use the Attributes.ContainsKey method (Attributes.ContainsKey("ToneLength") will return a boolean indicating whether or not the ToneLength attribute has been specified).

One important note about custom listeners: They are not available to the listener until after it has been created. This means that the listener’s constructors will not have access to any of the attributes defined for it except for the intializeData attribute (if any) which will be passed as a parameter.

 

Closing

 

Hopefully these examples give you a good idea of how to implement a custom trace listener. It's only a matter of deriving from the TraceListener type and overriding the correct methods. In fact, this is how all of the listeners included in System.Diagnostics were made.