Click here to Skip to main content
Click here to Skip to main content

Port Writer Trace Listener for .NET Applications

, 12 Aug 2007
Rate this:
Please Sign up or sign in to vote.
A Trace Listener class writing Trace Messages to a UDP port. Also provided is a WinForm application called TraceView to view the Trace Messages sent by the Trace Listener.

Introduction

The .NET Framework 2.0 introduced an enhanced tracing mechanism. The new flexible tracing architecture provides us the ability to create our very own Trace Listener. Other than TraceListener, new classes like TraceSource, TraceSwitch, and TraceFilter give us complete control over application tracing. In this article, I will demonstrate how I created a Trace Listener class, PortWriterTraceListener, that listens to Trace messages and sends the Trace messages to a UDP port. I also created a WinForms application called TraceView which displays the Trace messages sent by the Trace Listener. The PortWriterTraceListener can be used with any .NET application, including ASP.NET. The application and the complete source code are attached with this article.

Background

To create our Trace Listener class, we need to understand the following Framework classes and enumerators. If you already know about these classes and enumerators, you can start reading the next section. Otherwise, click on the class name to go to the Appendix.

Classes:

Enumerators:

PortWriterTraceListener

We will be creating a Trace Listener that sends Trace messages to a UDP port. I have chosen to use the port number 7003, which I believe is not reserved for any protocol. But, a different port can be configured by changing the config file. Why UDP, why not TCP/IP? The UDP protocol is efficient and fast, but less reliable. Tracing is not a mission-critical task, and reliability is not the highest priority criteria.

How many lines of code is required to create a Trace Listener? Just a few lines of code. In fact, there are only two abstract functions in the TraceListener class, Write and Writeline. Following is a functional Trace Listener class that, if used, will log the Trace in the event log.

public class EventLogWriterTraceListener : TraceListener
{
    override public void Write(string Message)
    {
        EventLog.WriteEntry("EventLogWriterTraceListener", 
                            Message, EventLogEntryType.Information, 10001, 0);
    }
    override public void WriteLine(string Message)
    {
        EventLog.WriteEntry("EventLogWriterTraceListener", 
                            Message, EventLogEntryType.Information, 10001, 0);
    }
}

But this will not do. We will create a more sophisticated Trace Listener. Before jumping into the code, let's take a look at a sample config file.

Sample Trace Configuration

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.diagnostics>
        <sources>
            <source name="TraceGenerator" 
              switchName="sourceSwitch" 
              switchType="System.Diagnostics.SourceSwitch">
                <listeners>
                    <add name="PortWriterTraceListener" 
                        type="PerformanceSolutions.Diagnostics.PortWriterTraceListener, 
                              PerfSol.Diagnostics, Version=1.0.0.0, 
                              Culture=neutral, PublicKeyToken=null" 
                        traceOutputOptions="Callstack,LogicalOperationStack,
                                            DateTime,ProcessId,ThreadId,Timestamp"
                        destination="home-dev" port="7003">
                        <filter type="System.Diagnostics.EventTypeFilter" 
                        initializeData="Information"/>
                    </add>
                    <remove name="Default"/>
                </listeners>
            </source>
        </sources>
        <switches>
            <add name="sourceSwitch" value="Information"/>
        </switches>
    </system.diagnostics>
</configuration>

In this config file, we declared a Trace Source called "TraceGenerator". We added our PortWriterTraceListener to the listeners collection, and removed the Default listener. Now, PortWriterTraceListener has two custom attributes called 'destination' and 'port'. These two custom attributes will be used to make the UDP connection. We will visit this config file again, when needed.

To create a Trace Listener that sends Trace messages to a UDP port, we have to do a little bit of Socket programming. We will be using the UdpClient class of the System.Net.Sockets namespace. We just need to do two things, connect to the destination computer, and send data to the port. The following function is used to connect to the UDP port. If _UDPSender is already connected, the function will return true; otherwise, the program will call the port and destination properties to read the port and destination custom attributes passed through the config file and use them to connect to the destination port.

private bool Connect()
{
    try
    {
        if (_UdpSender == null)
        {
            _UdpSender = new UdpClient();
        }
        //Check if already connected

        if (_UdpSender.Client.Connected)
        {
            return true;
        }

        if (port == 0)
        {
            throw new Exception("Port number not set.");
        }

        if (destination == string.Empty)
        {
            throw new Exception("Destination address not set.");
        }
        //If an IP address was passed convert it right here

        if (IPAddress.TryParse(_destination, out _DestHostIP))
            ;
        //Otherwise determine IP Address from the Host Name or Domain Name

        else
        {
            //Assigning only the first IP address from the list

            _DestHostIP = Dns.GetHostAddresses(_destination)[0];
        }
        _UdpSender.EnableBroadcast = true;
        _UdpSender.DontFragment = true;
        _UdpSender.Connect(_DestHostIP, _port);
        return true;
        }
    catch (Exception e)
    {
        LogEvent(e.Message, e.StackTrace);
    }
    return false;
}

Following is the property function for port. We read the value of port in the _port variable from the Attributes collection. Similarly, we read the value of the destination attribute.

public int port
{
    get
    {
        //Find the port attribute.

        foreach (DictionaryEntry de in this.Attributes)
            if (de.Key.ToString().ToLower() == "port")
                _port = int.Parse(de.Value.ToString());
            return _port;
    }
    //If set programatically

    set { _port = value; }
}

When we create a Trace Source object in our application, TraceSource creates a listeners collection from the config file. For each listener in the collection, TraceSource will call the GetSupportedAttributes method of the listener to build the custom attributes Dictionary. In the PortWriterTraceListener class, I had to override the GetSupportedAttributes method to let the TraceSource know what custom attributes are there for this class.

protected override string[] GetSupportedAttributes()
{
    return new string[] { "destination", "port" };
}

Once the UdpClient is connected, the following Send method will be used to send Trace messages to the UDP port.

private void Send(string TraceMessage)
{
    try
    {
        Byte[] sendBytes = Encoding.ASCII.GetBytes(TraceMessage);
        _UdpSender.Send(sendBytes, sendBytes.Length);
    }
    catch (SocketException se)
    {
        if (se.ErrorCode == 10040)
        {
            Send(TruncateMessage(TraceMessage));
        }
        LogEvent(se.Message + " ErrorCode: " + se.ErrorCode.ToString(), se.StackTrace);
    }
    catch (Exception e)
    {
        LogEvent(e.Message, e.StackTrace);
    }
}

If you notice, in the Send method, I am checking for the exception error code 10040, which occurs when the message size exceeds the limit that the Windows socket can handle. We are going to truncate the Trace message in this situation. Also, we will not raise errors, but log them in the event log. Raising errors can cause the actual application to fail, which is undesirable.

Microsoft has a recommended format for Trace messages. At least, they follow that format for the Trace Listener classes that come with the .NET framework. This format has a header section, a message section, and a footer section, like: <Header><Message><Footer>. To do this, TraceListener has two methods: WriteHeader and WriteFooter. Unfortunately, these two methods are private methods. So, we cannot override them, but we will follow the format. For our purpose, every message will be of the following XML format:

  <TraceMessage>
    <Header>
      <Source></Source>
      <EventType></EventType>
      <MessageId></MessageId>
    </Header>
    <Message><![CDATA[]]></Message>
    <Footer>
      <Callstack><![CDATA[]]></Callstack>
      <DateTime></DateTime>
      <LogicalOperationStack></LogicalOperationStack>
      <ProcessId></ProcessId>
      <ThreadId></ThreadId>
      <Timestamp></Timestamp>
    </Footer>
  </TraceMessage>

To send Trace messages to a Trace Listener, there are four methods available in the TraceSource class: TraceEvent, TraceData, TraceInformation, and TraceTransfer. We will be most interested in the TraceEvent and TraceData methods, and implement them in our PortWriterTraceListener class. The TraceInformation method internally calls the TraceEvent method with the EventType parameter as TraceEventType.Information. The TraceTransfer method is used for correlating Trace messages. If none of these methods are implemented, the TraceEvent method will call the Write method of the Trace Listener class.

We will override TraceEvent, TraceData, Write, and WriteLine in the PortWriterTraceListener class. The TraceEvent method has three flavors. Here, I have shown one implementation. The other two implementations are very similar.

override public void TraceEvent(TraceEventCache eventCache, 
         string source, TraceEventType eventType, int id, string message)
{
    _MessageSource = source;
    _EventType = eventType.ToString();
    _MessageId = id.ToString();
    _TraceMessage = message;
    SetTraceOutputOptionsFromEventCache(eventCache);
    string XmlMessage = FormatMessageToXml();
    Connect();
    Send(XmlMessage);
}

The eventCache parameter contains process specific information like callstack, datetime, process ID, Thread ID etc. These are part of the Trace Output Options. In the config file's TraceOutputOptions attribute, we can mention which output options the application is interested to capture. For example, in the config file in Sample Trace Configuration, I have added TraceOutputOptions, Callstack, LogicalOperationStack, DateTime, ProcessId, ThreadId, and Timestamp. The SetTraceOutputOptionsFromEventCache method is used to determine what output options to send with the message.

private void SetTraceOutputOptionsFromEventCache(TraceEventCache eventCache)
{
    if (this.TraceOutputOptions == TraceOptions.None)
    {
        return;
    }
    if ((this.TraceOutputOptions & TraceOptions.Callstack) == TraceOptions.Callstack)
    {
        _Callstack = eventCache.Callstack;
    }
    if ((this.TraceOutputOptions & TraceOptions.DateTime) == TraceOptions.DateTime)
    {
        DateTime CurrentDateTime = eventCache.DateTime.ToLocalTime();
        _MessageDateTime = CurrentDateTime.Month.ToString() + "-"
        + CurrentDateTime.Day.ToString() + "-"
        + CurrentDateTime.Year.ToString() + " "
        + CurrentDateTime.Hour.ToString() + ":"
        + CurrentDateTime.Minute.ToString() + ":"
        + CurrentDateTime.Second.ToString() + ":"
        + CurrentDateTime.Millisecond.ToString();
    }
    if ((this.TraceOutputOptions & TraceOptions.LogicalOperationStack) == 
                                       TraceOptions.LogicalOperationStack)
    {
        object[] objs = eventCache.LogicalOperationStack.ToArray();
    }
    if ((this.TraceOutputOptions & TraceOptions.ProcessId) == TraceOptions.ProcessId)
    {
        _ProcessId = eventCache.ProcessId.ToString();
    }
    if ((this.TraceOutputOptions & TraceOptions.ThreadId) == TraceOptions.ThreadId)
    {
        _ThreadId = eventCache.ThreadId;
    }
    if ((this.TraceOutputOptions & TraceOptions.Timestamp) == TraceOptions.Timestamp)
    {
        _Timestamp = eventCache.Timestamp.ToString();
    }
    return;
}

At this point, we have all the values required for the Trace message. We call the FormatMessageToXml() function to format the message to XML. Once we have the XML, we call Connect, and then Send to send the Trace message to the UDP port. That's all for the TraceEvent method.

The TraceData method is same as the TraceEvent method except that the message is passed in the form of an object. Following is an implementation of the TraceData method.

override public void TraceData(TraceEventCache eventCache, 
         string source, TraceEventType eventType, int id, object data)
{
    _MessageSource = source;
    _EventType = eventType.ToString();
    _MessageId = id.ToString();
    _TraceMessage = data.ToString();
    SetTraceOutputOptionsFromEventCache(eventCache);
    string XmlMessage = FormatMessageToXml();
    Connect();
    Send(XmlMessage);
}

The Write method takes either one or two parameters. Following is an implementation of the Write method.

override public void Write(string Message)
{
    _TraceMessage = Message;
    SetTraceOutputOptions();
    string XmlMessage = FormatMessageToXml();
    Connect();
    Send(XmlMessage);
}

Since the Write method does not provide Source or EventType, those fields remain blank in the formatted XML Trace messages. Also, the TraceEventCache object is not available in the Write method. The output options are determined in the following function using the Environment, Datetime, Process and Thread objects.

private void SetTraceOutputOptions()
{
    try
    {
        if (this.TraceOutputOptions == TraceOptions.None)
        {
            return;
        }
        if ((this.TraceOutputOptions & TraceOptions.Callstack) == TraceOptions.Callstack)
        {
            _Callstack = Environment.StackTrace;
        }
        if ((this.TraceOutputOptions & TraceOptions.DateTime) == TraceOptions.DateTime)
        {
            DateTime CurrentDateTime = DateTime.Now.ToLocalTime();
            _MessageDateTime = CurrentDateTime.Month.ToString() + "-"
                            + CurrentDateTime.Day.ToString() + "-"
                            + CurrentDateTime.Year.ToString() + " "
                            + CurrentDateTime.Hour.ToString() + ":"
                            + CurrentDateTime.Minute.ToString() + ":"
                            + CurrentDateTime.Second.ToString() + ":"
                            + CurrentDateTime.Millisecond.ToString();
        }
        if ((this.TraceOutputOptions & TraceOptions.LogicalOperationStack) == 
                                       TraceOptions.LogicalOperationStack)
        {
            _LogicalOperationStack = "";
        }
        if ((this.TraceOutputOptions & TraceOptions.ProcessId) == TraceOptions.ProcessId)
        {
            _ProcessId = Process.GetCurrentProcess().Id.ToString();
        }
        if ((this.TraceOutputOptions & TraceOptions.ThreadId) == TraceOptions.ThreadId)
        {
            _ThreadId = Thread.CurrentThread.ManagedThreadId.ToString();
        }
        if ((this.TraceOutputOptions & TraceOptions.Timestamp) == TraceOptions.Timestamp)
        {
            _Timestamp = System.Diagnostics.Stopwatch.GetTimestamp().ToString();
        }
    }
    catch (Exception ex)
    {
        LogEvent(ex.Message,ex.StackTrace);
    }
    return;
}

The other implementations of the Write method are very similar and don't need further discussion. The WriteLine method doesn't have any special meaning in PortWriterTraceListener as Trace messages will always be formatted to XML. For this reason, in the WriteLine method, we simply call the corresponding Write method.

We are now ready to compile our code and create the DLL. Our Trace Listener is ready to use.

How to Use the PortWriterTraceListener

First, we will see how to use the PortWriterTraceListener in a managed executable. Create a Console based application or WinForms application, and add the following function:

private void GenerateTraceMessage()
{
    TraceSource mySource = new TraceSource("TraceGenerator");
    mySource.TraceEvent(TraceEventType.Critical, 1001, "Critical Message");
    mySource.TraceEvent(TraceEventType.Error, 1002, "Error Message");
    mySource.TraceEvent(TraceEventType.Warning, 1003, "Warning Message");
    mySource.TraceEvent(TraceEventType.Information, 1004, "Critical Message");
}

Now, replace the application config file with the one I mentioned in Sample Trace Configuration. Calling this function in your program will send four Trace messages to the PortWriterTraceListener.

To use the PortWriterTraceListener in an ASP.NET application, add the following in the web.config file:

    <system.web>
        <trace writeToDiagnosticsTrace="true"/>
        <customErrors mode="Off"/>
    </system.web>
    <system.diagnostics>
        <trace autoflush="true" indentsize="4">
            <listeners>
                <add name="PortListener" 
                     type="PerformanceSolutions.Diagnostics.PortWriterTraceListener, 
                           PerfSol.Diagnostics, Version=1.0.0.0, 
                     Culture=neutral, PublicKeyToken=null" 
                     initializeData="TestListener" 
                     destination="home-dev" port="7003" 
                     traceOutputOptions=" DateTime,ProcessId,ThreadId,Timestamp"/>
                <remove name="Default" />
            </listeners>
        </trace>
    </system.diagnostics>

There are two things to notice in this config file. I used writeToDiagnosticsTrace="true" to redirect trace messages to the System.Diagnostics trace, and in the listeners collection, I added the PortWriterTraceListener listener.

Now, create an aspx page with the page directive Trace=true, and use Trace.Write to send some messages to the PortWriterTraceListener. If you have a .NET component that you are using in the aspx page, you can send the Trace messages from that component to PortWriterTraceListener. For this, you have to include the configuration setting used in the Sample Trace Configuration in the system.diagnostics section of the web.config. The nice thing about ASP.NET 2.0 is that Trace is integrated. Whatever Trace message we send from the component will show up in the right sequence along with the ASP.NET trace.

TraceView

So far, I have showed you how to send Trace messages to the PortWriterTraceListener. But, how can we see that PortWriterTraceListener is working? I have created a WinForms application called TraceView to view the Trace sent to PortWriterTraceListener. The TraceView simply listens to the UDP port where PortWriterTraceListener sends the Trace messages and displays the messages in a ListView control. There are many useful features in TraceView, like logging Trace to a file, saving Trace to a file, search messages, filter messages, and copy messages to the Clipboard. Make sure you have chosen the 'Do not filter' option in the Filter menu to see the Trace messages sent from the ASP.NET application.

Instruction for the Demo

Follow the steps below to use the demo application:

  • Unzip the files from PortWriterDemo.zip to a folder
  • Open the config file TraceGenerator.exe.config in Notepad, change the destination to your computer name, and save
  • Double click on TraceView.exe to run TraceView
  • Go to the Capture menu and click on 'Start Capture'
  • Double click on TraceGenerator.exe to run TraceGenerator
  • Click on the 'Send Messages' button

You should see the Trace messages in the TraceView window.

Conclusion

PortWriterTraceListener and TraceView make a complete working tracing solution for any .NET application. The TraceView application can be run in a separate computer than the one where the application is running. This is useful for web based applications where the web server and the development computer are two different machines. Other than display messages, TraceView has the ability to save and log messages in CSV or XML format. You can also open the saved messages in TraceView to search or filter saved messages. In a future version, I will add logging messages to SQL Server. Enjoy tracing.

Appendix

Classes:

  • TraceListener: Tracing in .NET 2.0 is extensible. Every Trace message goes through at least one Trace Listener. The Trace Listener can be used programmatically, or by configuring the application's config file. Any custom Trace Listener needs to be inherited from the base class, TraceListener.
  • TraceListenerCollection: More than one Trace Listener can be used in an application. We can add Trace Listeners in the TraceListenerCollection programmatically, or through the config file. You might ask why I am going to send a Trace message to more than one Trace Listener? The idea is you may want to send Trace messages to different Trace Listeners in the collection depending on their event type. This is possible by using the TraceFilter.
  • TraceSource: No more sending messages using the old Trace class. The programmer will create a TraceSource object to send the Trace messages to the Trace Listener. A TraceSource is associated with a Source object, either created programmatically, or through the configuration file. This gives us the flexibility to use more than one source in the same application. For example, we can use different TraceSources in different assemblies of the application to differentiate or categorize the Trace messages. Each of these sources is independently configurable, completely. For example, I may choose to send only warning messages from one assembly but error messages from another.
  • TraceSwitch: The TraceSwitch is used to control the levels of Trace messages sent from a TraceSource. If we want to send only the warning and error messages, we will be choosing a switch for the TraceSource that has the value 'Warning'.
  • TraceFilter: The TraceFilter is associated with the Trace Listener. It gives an extra layer for the Trace Listener to decide whether a Trace message from a Trace source will be accepted or not. The TraceFilter is useful when we like to use different Trace Listeners for different types of Trace events coming from the same Trace source. The Trace switch determines what levels (Warning, Error, Critical, Information) of Trace messages will be sent via a Trace source whereas a Trace filter determines to which Trace Listener these messages will be channelized.
  • TraceEventCache: The TraceEventCache provides thread and process specific data for a Trace message. It is used as a parameter in different tracing methods in the TraceSource class and is useful to identify the origin of the Trace messages.
  • Trace: Old vanilla Trace class still works.

Enumerators:

  • TraceEventType: The TraceEventType enumerator identifies a Trace message with an event like warning, error, critical etc.
  • TraceLevel: The TraceLevel enumerator determines which levels of messages will be sent via a Trace source and used with the Trace switch.
  • TraceOptions: Some Trace data in the Trace message can be optional. To determine what optional data in the Trace message will be sent from the Trace source, the TraceOptions enumeration can be used. The examples of optional data are Callstack, Datetime, LogicalOperationStack, ProcessId, ThreadId, and Timestamp.

Reference

History

  • 12th Aug '07 - Original version.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Himadri Chakrabarti
Web Developer
United States United States
No Biography provided

Comments and Discussions

 
GeneralWell written, most comprehensive. Now about those dates Pinmemberrobvon7-Nov-07 9:05 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140709.1 | Last Updated 12 Aug 2007
Article Copyright 2007 by Himadri Chakrabarti
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid