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
="1.0"="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();
}
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 (IPAddress.TryParse(_destination, out _DestHostIP))
;
else
{
_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
{
foreach (DictionaryEntry de in this.Attributes)
if (de.Key.ToString().ToLower() == "port")
_port = int.Parse(de.Value.ToString());
return _port;
}
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 TraceSource
s 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.