다음을 통해 공유


New information has been added to this article since publication.
Refer to the Editor's Update below.

CLR Inside Out

Extending System.Diagnostics

Krzysztof Cwalina

Code download available at:CLRInsideOut0604.exe(128 KB)

Contents

Hello TraceSource
Creating a Custom TraceListener
Creating a Custom Switch
Extending Switch
Extending SourceSwitch
Generic SourceSwitch
Creating a Custom TraceSource
Containment
Configuration System Extensibility
Wrapping It Up

The System.Diagnostics namespace in the Microsoft® .NET Framework contains powerful tracing capabilities. This includes the main tracing API: TraceSource. As you will see, the tracing APIs in System.Diagnostics support powerful extensibility. In this column I'll describe advanced extensibility scenarios and various ways to customize the tracing APIs. I'll also discuss API design principles, in particular designing for extensibility with inheritance, containment, and generic types.

Hello TraceSource

Before I dive into customization, let's write a short "Hello World" program instrumented with the TraceSource API.

class Program { static TraceSource source1 = new TraceSource("MyProgram.Source1"); static void Main() { source1.TraceInformation("Main enters"); Console.WriteLine("Hello World"); source1.TraceInformation("Main exists"); } }

Each trace source contains an instance of a SourceSwitch, which decides whether a trace message should be output or not. Each source also contains a set of TraceListener instances that determine to where messages should be output. By default, source switches are not set to pass informational messages, nor do they contain listeners that would pass the messages to the console, so this program just prints "Hello World". To enable tracing, the configuration file in Figure 1 must be added. This configuration file instructs the source switch to output informational messages and adds a trace listener that directs messages to the console. Now the program not only outputs "Hello World", but also traces simple messages when the main method enters and exits. Here's the result:

MyProgram.Source1 Information: 0 : Main enters Hello World MyProgram.Source1 Information: 0 : Main exists

Figure 1 Tracing Configuration

<configuration> <system.diagnostics> <sources> <source name="MyProgram.Source1" switchName="MyProgram.Switch1"> <listeners> <add name="Console" type="System.Diagnostics.ConsoleTraceListener"/> </listeners> </source> </sources> <switches> <add name="MyProgram.Switch1" value="Information"/> </switches> </system.diagnostics> </configuration>

Now that you're familiar with some of the basics, let's start customizing this code.

Creating a Custom TraceListener

I will begin by developing a custom trace listener. Instead of directing trace messages to the console, my trace listener will send some important messages to an e-mail recipient.

Custom trace listeners like this can be implemented by inheriting from the TraceListener class directly. But for historical reasons, this is not very easy. TraceListener contains many interdependent virtual members that can be overridden. And after working with these APIs for several years, I still forget which members need to be overridden in which scenarios. For this reason, I created a helper type called TraceListener2, which makes it much easier to create custom listeners in common scenarios.

I spent some time thinking about a more meaningful name, but settled on TraceListener2. This adheres to a guideline that will appear in an update of the .NET Framework General Reference Design Guidelines for Class Library Developers on MSDN®online. It states that you should "Use a numeric suffix to indicate a new version of an existing API, if the existing name of the API is the only name that makes sense."

In general, however, I find it's better to use a good name that describes the difference between the old type and the new type. Consider using a brand new but meaningful identifier, instead of simply adding a suffix or a prefix.

[Editor's Update - 4/26/2006: Figure 2 has been updated.] The full source of TraceListener2 is in the TraceListener2.cs file in the code download.

Figure 2 TraceListener2

public abstract class TraceListener2 : TraceListener { protected TraceListener2(string name): base(name) {} protected abstract void TraceEventCore( TraceEventCache eventCache, string source, TraceEventType eventType, int id, string message); protected virtual string FormatData(object[] data) { StringBuilder strData = new StringBuilder(); for (int i = 0; i < data.Length; i++) { if (i >= 1) strData.Append("|"); strData.Append(data[i].ToString()); } return strData.ToString(); } protected void TraceDataCore(TraceEventCache eventCache, string source, TraceEventType eventType, int id, params object[] data) { if (Filter != null && !Filter.ShouldTrace(eventCache, source, eventType, id, null, null, null, data)) return; TraceEventCore(eventCache, source, eventType, id, FormatData(data)); } public sealed override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string message) { if (Filter != null && !Filter.ShouldTrace(eventCache, source, eventType, id, message,null,null,null)) return; TraceEventCore(eventCache, source, eventType, id, message); } public sealed override void Write(string message) { if (Filter != null && !Filter.ShouldTrace(null, "Trace", TraceEventType.Information, 0, message, null, null, null)) return; TraceEventCore(null, "Trace", TraceEventType.Information, 0, message); } public sealed override int GetHashCode() { return base.GetHashCode(); } public sealed override string Name { get { return base.Name; } set { base.Name = value; } } ... }

As you can see, this class has only one abstract member, making it very clear what must be overridden. Also, most of the virtual members of the TraceListener base class are overridden and sealed to indicate that they don't need to be overridden. (Note that they don't need to be overridden in common scenarios. There are still less common scenarios in which it might be useful, but as I mentioned before, TraceListener2 is designed as a helper base class for common scenarios.)

There are several design guidelines that I took into account when trying to improve TraceListener. First, define top usage scenarios for each major feature area. Make only the longest overload virtual if extensibility is required (shorter overloads should simply call through to a longer overload). And, finally, favor protected accessibility over public accessibility for virtual members (public members should provide extensibility, if required, by calling into a protected virtual member).

So I defined the top scenario as implementing a custom listener to write simple events to a simple event store. I limited the number of virtual members. And I made all the remaining extensibility points protected.

Creating a custom listener for simple scenarios is now trivial. The only thing required is to implement the TraceEventCore abstract method. Note that Visual Studio® 2005 provides a great new feature to make this even easier. Just create an empty class that inherits from an abstract class, then right-click on the name of the abstract class and select "Implement Abstract Class", as you see in Figure 3.

Figure 3 Implement Abstract Class

Figure 3** Implement Abstract Class **

The editor will generate default implementations for all abstract members, like this:

public class EmailListener : TraceListener2 { protected override void TraceEventCore (TraceEventCache eventCache, string source, TraceEventType eventType, int id, params object[] data) throw new System.Exception ("The method or operation is not...") } }

The only task remaining is to add constructors and fill in the body of the autogenerated (overridden) method. The example that is shown in Figure 4 represents a custom listener responsible for sending all critical events via e-mail.

Figure 4 Sending Events by E-Mail

public class EmailListener1 : TraceListener2 { public EmailListener1(string name) : base(name) {} public EMailListener1() : this("EMailListener") { } protected override void TraceEventCore(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string message) { if (eventType != TraceEventType.Critical) return; MailMessage emailMessage = new MailMessage("kcwalina@microsoft.com", "kcwalina@microsoft.com"); emailMessage.Body = message; emailMessage.Subject = "Critical Error"; SmtpClient client = new SmtpClient(); client.Send(emailMessage); } }

To use the listener, you need to configure the SMTP settings in the configuration file. Something like the following will do the trick; just fill in the values for host, username, and password:

<configuration> <system.net> <mailSettings> <smtp deliveryMethod="network"> <network host="..." defaultCredentials="false" userName="..." password="..." /> </smtp> </mailSettings> </system.net> </configuration>

You can now add the listener to your trace source, as follows:

<listeners> <add name="Email" type="Microsoft.Samples.Tracing.EMailListener,Extensions"/> <add name="Console" type="System.Diagnostics.ConsoleTraceListener"/> </listeners>

Next, try to output some critical trace messages, like so:

static void Main() { source1.TraceInformation("Main enters"); Console.WriteLine("Hello World"); source1.TraceEvent(TraceEventType.Critical, 0, "Main exists"); }

Creating a Custom Switch

Creating custom listeners is a relatively common scenario and is well documented. The same is true for custom switches, but to a lesser degree. The Framework has a base switch class called System.Diagnostics.Switch and three subclasses, BooleanSwitch, TraceSwitch, and SourceSwitch.

The first two concrete switches are intended to be used as standalone types, and creating a custom standalone switch is relatively straightforward. SourceSwitch is intended to be used with TraceSource classes, and implementing a custom SourceSwitch is a bit trickier. I'll discuss this a little later.

Extending Switch

TraceSource uses SourceSwitch, which was added to the .NET Framework 2.0, to decide which messages should be traced. The Trace and TraceSwitch classes, which were present in the first version of the Framework, are still available. Since they are simpler, I will use them first to illustrate how to create custom switches.

Let's get started by creating a custom switch that is better suited than TraceSwitch to trace execution flow (method entry and exit). For example, if I used the Trace and TraceSwitch classes to trace execution flow I would have to write something like this:

static TraceSwitch trace = new TraceSwitch("flowSwitch"); ... void Main(){ if(trace.TraceInformation){ Trace.WriteLine("Entering method Foo"); } ... if(trace.TraceInformation){ Trace.WriteLine("Exiting method Foo"); } }

But this leaves some limitations. What if I want to enable either of these messages independently? In other words, what if I want to output only the exit message?

I would probably want a custom switch with custom levels corresponding to exit and enter events. It would look like this:

static FlowSwitch trace = new FlowSwitch("flowSwitch"); ... void Main(){ if(trace.Entering){ Trace.WriteLine("Entering method Foo"); } ... if(trace.Exiting){ Trace.WriteLine("Exiting method Foo"); } }

Such a FlowSwitch can actually have four states: one indicates that events should be traced when a method enters, another when a method exists, and the third and fourth are combinations of the core values to trace both or none of the event types. The four states of a FlowSwitch are represented by the FlowSwitchSettings enumeration, as shown here:

[Flags] public enum FlowSwitchSettings { None = 0, Entering = 32, Exiting = 64, Both = Entering | Exiting }

Values representing combinations of flags are very convenient and providing such values is recommended by the design guidelines.

Note that the enum is named using a plural noun phrase and has the FlagsAttribute applied. This follows two fundamental flags enum design guidelines: apply the System.FlagsAttribute to flag enums but not to simple enums, and use a plural type name for an enumeration with bit fields as values (also called a flags enum).

The FlowSwitch uses this enum for the Level property, which is set and changed by the OnValueChanged method. The method is called after the protected Value property is set to the value stored in the configuration file, which typically happens when the switch is instantiated. The value can be refreshed later during execution of the program by calling the Trace.Refresh method (new in the .NET Framework 2.0), which re-reads the configuration file. I also added two helper properties (Entering and Exiting), to avoid having to do complex bitwise operations in common usage scenarios (see Figure 5).

Figure 5 FlowSwitch Class

public class FlowSwitch : Switch { public FlowSwitch(string name) : base(name,"") {} public FlowSwitchSettings Level { get { return (FlowSwitchSettings)this.SwitchSetting; } set { this.SwitchSetting = (int)value; } } public bool Entering { get { return (Level & FlowSwitchSettings.Entering) == FlowSwitchSettings.Entering; } } public bool Exiting { get { return (Level & FlowSwitchSettings.Exiting) == FlowSwitchSettings.Exiting; } } protected sealed override void OnValueChanged() { SwitchSetting = (int)Enum.Parse(typeof(FlowSwitchSettings), Value); } }

TraceSwitch and BooleanSwitch (which are included in the .NET Framework) are two other examples of concrete implementations of the Switch class.

Extending SourceSwitch

As you can see, extending the Switch class is relatively simple. Extending the SourceSwitch is both more interesting and more challenging. As I mentioned before, the switch is used with the main tracing entry point called TraceSource. TraceSource relies on two enums: one is used to specify the type of the event being traced and the second specifies the types of events that the given source should trace. The challenge in implementing custom source switches stems from the fact that the enums need to be specially designed to interoperate properly when used inside a TraceSource.

For example, in the .NET Framework, TraceSource uses a SourceLevels flags enum to represent the switch setting and a TraceEventType simple (non-flag) enum to represent the type of the event, as shown in the following:

TraceSource diagnostics = new TraceSource("MyApplication"); // diagnostics.Switch.Level = SourceLevels.Error; diagnostics.TraceEvent(TraceEventType.Error, 0, "File not found");

For the custom flow source switch, I will reuse the FlowSwitchSettings enum from the previous section to represent the switch setting and add a new enum, FlowEventType, to represent the type of an event. This flow switch can control two types of events: Entering and Exiting. Therefore, the enum will have two values. Note that this enum is not a flags enum. Each event can be either an entering or an exiting event, but not both:

public enum FlowEventType { Entering = 32, Exiting = 64 }

As you might have noticed, this enum's name (FlowEventType) is a singular noun phrase so as to conform to the basic enum naming guidelines: use a singular type name for an enumeration unless its values are bit fields.

Unlike FlowSwitch, FlowSourceSwitch (and all SourceSwitches) does not need helper properties (Entering and Exiting). Instead, it contains a ShouldTrace method that will compare the event type to the switch setting to determine whether or not the event should be traced (see Figure 6).

Figure 6 To Trace or Not to Trace

public class FlowSourceSwitch1 : SourceSwitch { public FlowSourceSwitch1(string name) : base(name) {} public new FlowSwitchSettings Level { get { return (FlowSwitchSettings)base.Level; } set { base.Level = (SourceLevels)value; } } public bool ShouldTrace(FlowEventType eventType) { return ((int)eventType & (int)Level) != 0; } protected sealed override void OnValueChanged() { SwitchSetting = (int)Enum.Parse(typeof(FlowSwitchSettings), Value); } }

The ShouldTrace method is probably the most interesting member of a source switch. As you might recall, I mentioned that the integer values of the two enums used by trace sources need to be chosen carefully. The reason is the implementation of the ShouldTrace method. It performs a bitwise comparison of the integer values to determine whether an event should be traced. Because the SourceSwitch.ShouldTrace method is nonvirtual, you cannot change how it compares the values. You can only change, or rather select, the values carefully to customize how the method works. Note that the method is nonvirtual to maximize performance (it allows for inlining). Performance of this method is critical to ensure the overhead of tracing can approach zero when a switch is turned off and the switch check is hoisted out. For example, the following if check can get inlined, and if the Boolean expression evaluates to false, the cost of the whole statement will be negligible:

if(source.Switch.ShouldTrace(TraceEvent.Information)) source.TraceEvent(TraceEvent.Information,...);

In fact, you don't absolutely have to add the ShouldTrace method to a custom switch—after all, the ShouldTrace method is just an overload and is not called by trace sources. TraceSource will call the base type's ShouldTrace(TraceEventType) method anyway. The overload is useful for scenarios in which the caller wants to move the switch check out of the TraceEvent call to improve performance.

Later in this article, I will show how to hook up such a custom SourceSwitch to a TraceSource. But first let's see if we can do better than the painstakingly handcrafted FlowSourceSwitch.

Generic SourceSwitch

Customization, or extensibility, can be provided through several mechanisms including abstractions, events, and callbacks. The .NET Framework 2.0 added a feature called Generics, which is a very powerful extensibility mechanism. Figure 7 shows how generics can be used to provide a switch that can be tweaked to support custom settings.

Figure 7 Switch that Supports Custom Settings

public class SourceSwitch<TEventType,TSetting> : SourceSwitch { TSetting enumLevel; static Type settingEnumType = typeof(TSetting); public SourceSwitch(string name) : base(name) {} public new TSetting Level { get { return enumLevel; } set { enumLevel = value; SwitchSetting = Convert.ToInt32(enumLevel); } } public bool ShouldTrace(TEventType eventType) { int eventTypeValue = Convert.ToInt32(eventType); return (eventTypeValue & SwitchSetting) != 0; } protected override void OnValueChanged() { Level = (TSetting)Enum.Parse(settingEnumType, this.Value); } }

This generic type has two type parameters, TEventType and TSetting, that are used in order to specify the enums representing the event type and the switch setting. This switch setting can be used in the following manner, which makes it so much simpler to create custom switches:

public class FlowSourceSwitch : SourceSwitch<FlowEventType,FlowSwitchSetting> { public FlowSourceSwitch(string name) : base(name) {} }

Generics support a feature called constraints. The feature allows type parameters to be constrained to only certain types. For example, it's possible to specify that the type argument passed to TSetting needs to be derived from, for example, IComparable. In this particular example of the Switch<TEventType,TSetting>, it would be nice to constrain both of the type parameters to types that inherit from System.Enum. Unfortunately, such constraint is not supported by the .NET Framework 2.0.

This restriction can be partially mitigated by choosing descriptive type parameter names. In naming the type parameters I followed the guideline to use descriptive names, unless a single-letter name is completely self-explanatory and a descriptive name would not add value.

Creating a Custom TraceSource

So how do you integrate the new switch with a TraceSource? Let's look at two approaches. In the first one, I will inherit from TraceSource and in the second, I will use containment. First, inherit from TraceSource to customize the default switch type:

public class FlowTraceSource1 : TraceSource { public FlowTraceSource1(string name) : base(name) { base.Switch = new FlowSourceSwitch(name); } public new FlowSourceSwitch Switch { get { return (FlowSourceSwitch)base.Switch; } set { base.Switch = value; } } }

If the source is configured using the configuration system, the switch type needs to be specified in the source configuration file:

<source name="FlowTracing" switchName="FlowTracing" switchType="Microsoft.Samples.Tracing.FlowSourceSwitch,Extensions" > <listeners> ... </listeners> </source>

Now the source can be used as shown in this example:

static readonly FlowTraceSource1 flow1 = new FlowTraceSource1("FlowTracing"); flow1.TraceEvent((TraceEventType)FlowEventType.Entering, 0, "Method1"); flow1.TraceEvent((TraceEventType)FlowEventType.Exiting, 0, "Method1");

Casting the event type argument is very inconvenient. The problem can be addressed by adding (to the TraceEvent methods) overloads that take FlowEventType, as shown here:

public class FlowTraceSource2 : TraceSource { ... public void TraceEvent(FlowEventType eventType, int id, string format, params object[] args) { int eventTypeValue = Convert.ToInt32(eventType); base.TraceEvent((TraceEventType)eventTypeValue, id, format, args); } public void TraceEvent(FlowEventType eventType, int id, string format) { int eventTypeValue = Convert.ToInt32(eventType); base.TraceEvent((TraceEventType)eventTypeValue, id, format); } }

But there is a problem with this design. FlowTraceSource pretends to be just an extended TraceSource. FlowTraceSource contains all the members inherited from TraceSource and in fact can be cast to TraceSource and used in place of it.

The problem is that FlowTraceSource is not really a TraceSource. For one thing, it's not designed to trace regular TraceSource events, like TraceEvent.Information, as the FlowSourceSwitch is not really designed to control such events. In cases where a subclass cannot be really used in place of its base class, inheritance should not be used. Containment may be a better approach.

Containment

Extensibility through containment (wrapping) means that a custom type uses another type in its implementation instead of inheriting from the type. In this example, FlowTraceSource3 uses TraceSource in its implementation (see Figure 8).

Figure 8 Wrapping a Type

public class FlowTraceSource3 { TraceSource source; public FlowTraceSource3(string name) { source = new TraceSource(name); source.Switch = new FlowSourceSwitch(name); } public FlowSourceSwitch Switch { get { return (FlowSourceSwitch)source.Switch; } set { source.Switch = value; } } public void TraceEvent(FlowEventType eventType, int id, string format, params object[] args) { int eventTypeValue = Convert.ToInt32(eventType); source.TraceEvent((TraceEventType)eventTypeValue, id, format, args); } public void TraceEvent(FlowEventType eventType, int id, string format) { int eventTypeValue = Convert.ToInt32(eventType); source.TraceEvent((TraceEventType)eventTypeValue, id, format); } public TraceListenerCollection Listeners { get { return source.Listeners; } } public void Flush() { source.Flush(); } public void Close() { source.Close(); } }

This design seems much simpler and cleaner. FlowTraceSource3 does not pretend to be a substitute for TraceSource, but by using TraceSource internally, FlowTraceSource3 gets many of the useful properties of TraceSource. For example, FlowTraceSource3 can be configured using the configuration system—it reuses TraceSource's listeners collection with its synchronization facilities, and many others.

Like the generic Switch we talked about before, it's possible to create a generic trace source. The type has two type parameters used to specify the type of the switch and the type of the event type enum. Again, the event type cannot be constrained, but the switch can be (see Figure 9). This generic type can be used as follows:

public class FlowTraceSource : TraceSource<FlowSourceSwitch, FlowEventType> { public FlowTraceSource(string name) : base(name) { } }

Figure 9 Generic TraceSource

public class TraceSource<TSwitch, TEventType> where TSwitch : SourceSwitch { TraceSource source; public TraceSource(string name) { source = new TraceSource(name); Type st = typeof(TSwitch); object s = st.GetConstructor( new Type[] { typeof(string) }).Invoke(new object[] { name }); this.Switch = (TSwitch)s; } public TSwitch Switch { get { return (TSwitch)source.Switch; } set { source.Switch = value; } } public void TraceEvent(TEventType eventType, int id, string format, params object[] args) { int eventTypeValue = Convert.ToInt32(eventType); source.TraceEvent((TraceEventType)eventTypeValue, id, format, args); } public void TraceEvent(TEventType eventType, int id, string format) { int eventTypeValue = Convert.ToInt32(eventType); source.TraceEvent((TraceEventType)eventTypeValue, id, format); } public void Flush() { source.Flush(); } public void Close() { source.Close(); } }

Configuration System Extensibility

Let's go back to the e-mail listener. If you remember, I hardcoded the recipient e-mail address, subject, and so forth. But there is a better way to do this. You can enable the listener to be configurable using the configuration system and it can be configured using the System.Diagnostics section of the configuration file. To do this, you need to override the GetSupportedAttributes method and then use the Attributes dictionary instead of the hardcoded values when sending e-mail messages. Figure 10 shows the code used to do this. Now the listener can be configured the following way:

<sharedListeners> <add name="Email" type="Microsoft.Samples.Tracing.CriticalMailListener,Extensions" Recipients="kcwalina@microsoft.com" Subject="Critical Error from TraceListener" From="kcwalina@microsoft.com " /> </sharedListeners>

Figure 10 Configurable E-Mail Listener

public class CriticalMailListener : TraceListener2 { public CriticalMailListener() : base("CriticalMailListener") { } public CriticalMailListener(string name) : base(name) { } protected override string[] GetSupportedAttributes() { return new string[] { "Receipients", "Subject", "From" }; } protected override void TraceEventCore( TraceEventCache eventCache, string source, TraceEventType eventType, int id, string message) { if (eventType != TraceEventType.Critical) return; MailMessage emailMessage = new MailMessage( Attributes["From"], Attributes["Recipients"]); emailMessage.Body = message; emailMessage.Subject = Attributes["Subject"]; SmtpClient client = new SmtpClient(); client.Send(emailMessage); } }

Wrapping It Up

As you can see, the tracing APIs in System.Diagnostics support powerful extensibility. I hope you will find some time to experiment with these APIs on your own. If you create some interesting extensions, I would love to hear about them. Remember, you can read more about the Tracing APIs in my blog at Tracing APIs in .NET Framework 2.0. You can also read the design guidelines now in Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries by Krzysztof Cwalina, Brad Abrams (Addison-Wesley, 2005).

Send your questions and comments to  clrinout@microsoft.com.

Krzysztof Cwalina is a program manager on the CLR team at Microsoft. He started his career at Microsoft designing APIs for the first release of the .NET Framework. Currently, he is leading the effort to develop, promote, and apply the design guidelines to the .NET Framework and WinFx. He is coauthor of Framework Design Guidelines (Addison-Wesley, 2005). Reach him at his blog at blogs.msdn.com/kcwalina.