Introduction
This article is about a Trace client to visualize and filter Trace messages, and a TCP Trace listener.
There are many Trace listeners out there. But, there are only a few Trace clients (receivers, applications, etc.). Like that one I've used in a company I worked for once.
There is another important point: this is the first release of the Trace client application! It's not ready yet, but it does everything I need to start coding a larger application. Check out for updates at my blog and look also at the ToDo section.
Background
The Trace client is written with WPF - it's the simplest and the best way to build GUIs :-) To receive Trace messages, I've implemented a TCP Trace listener. This is the best way to send large messages to another application (on the same machine or a remote one).
Each Trace listener opens a TCP socket to listen to. The Trace client connects to that socket and displays the Trace messages. If you like, you could use any Telnet application to do the same - in the next version, I'll add support for text messages; as for now, the Trace listener sends XML messages.
Using the Code
First of all: how to use it!
Trace Listener
As it is "only" a Trace listener, you use tracing as what Microsoft thinks tracing is about. Just add the Trace listener to your config file:
<system.diagnostics>
<trace>
<listeners>
<add name="tcp" type="TraceClient.TcpTraceListener, TraceListener" port="666" />
</listeners>
</trace>
</system.diagnostics>
As the TCP Trace listener opens a TCP socket, it only needs to know which port it should listen to. This can be configured through the "port
" attribute. If you are in trouble when there is more than one application domain, you can use this syntax:
port="AppDomainName:Port;DefaultPort"
e.g.: port="ServerAppDomain:666;ClientAppDomain:667;668"
This sample shows the configuration for a "Server" AppDomain, sending on port 666, a "Client" AppDomain, sending on port 667, and the default port on 668. I need such AppDomains, e.g., when my application is able to host a server and a client in the same process - very useful for single user installations.
Trace Client
On the Trace client side, you have to change the config file too. It's a simple array of address/port entries. This is a ToDo: Add a configuration dialog.
<userSettings>
<TraceClient.Properties.Settings>
<setting name="TraceSources" serializeAs="String">
<value>127.0.0.1:666;127.0.0.1:667;127.0.0.1:668</value>
</setting>
</TraceClient.Properties.Settings>
</userSettings>
That's it...
Points of Interest
TraceListener
I don't want to explain the Trace listener in detail because there are many good articles about Trace listeners. E.g.: A TraceListener that sends messages via UDP, and Port Writer Trace Listener for .NET Applications. Those Trace listeners send Trace messages through UDP. And by the way, this article is inspired by the Port Writer Trace Listener article.
Why TCP? Well, with UPD, you can only send small packages (your sysadmin knows more). So I needed a alternative.
First, I'll read the config.
protected override string[] GetSupportedAttributes()
{
return new string[] { "port" };
}
private int GetPort()
{
string port = this.Attributes["port"];
if (string.IsNullOrEmpty(port))
{
throw new ArgumentNullException("port", "port is not set in your config.");
}
int result = -1;
return result;
}
When a TraceRequest comes in, a TraceMessage is created and passed to the Send
Method.
private StringBuilder writeBuffer = new StringBuilder();
public override void Write(string message)
{
writeBuffer.Append(message);
}
public override void WriteLine(string message)
{
writeBuffer.Append(message);
Send(new TraceMessage(writeBuffer.ToString()));
writeBuffer = new StringBuilder();
}
public override void TraceEvent(TraceEventCache eventCache,
string source, TraceEventType eventType, int id, string message)
{
Send(new TraceMessage(eventCache, source, eventType, id, message));
}
public override void TraceData(TraceEventCache eventCache,
string source, TraceEventType eventType, int id, object data)
{
Send(new TraceMessage(eventCache, source, eventType, id, data));
}
The writeBuffer
is used to cache Write
method calls.
The Send
method, first of all, does a call to CreateSocket
. This method opens a TCP listener. But only once! If we have trouble there, no TraceMessages will be send. Most of the troubles are related to used ports, so we don't want to try opening a port over and over again.
The reason why Send
creates the socket and not the constructer is the fact that GetSupportedAttributes()
is called later! So, there must be another entry point.
private static TcpListener listener = null;
private void CreateSocket()
{
lock (typeof(TcpTraceListener))
{
if (initialized) return;
initialized = true;
try
{
listener = new System.Net.Sockets.TcpListener(IPAddress.Any, GetPort());
listener.Start();
listener.BeginAcceptSocket(new AsyncCallback(AcceptSocket), null);
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
Debug.Fail("Unable to create TcpListener", ex.ToString());
}
}
}
CreateSocket
makes an asynchronous call to AcceptSocket
- we don't want to block. Debug.Fail
should be replaced with another option, like writing to the Event Log. But during development, a Debug.Fail
is a nice thing ;-)
private static HashSet<Socket> clients = new HashSet<Socket>();
private static void AcceptSocket(IAsyncResult result)
{
try
{
Socket s = listener.EndAcceptSocket(result);
listener.BeginAcceptSocket(new AsyncCallback(AcceptSocket), null);
clients.Add(s);
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
My AcceptSocket
callback is just adding the result socket to a collection and starts another BeginAcceptSocket
method call.
Back to the Send
method:
public void Send(TraceMessage msg)
{
try
{
CreateSocket();
MemoryStream m = new MemoryStream();
msg.ToStream(m);
byte[] buffer = m.GetBuffer();
List<Socket> socketsToRemove = null;
foreach (Socket s in clients)
{
try
{
s.BeginSend(buffer, 0, (int)m.Length, SocketFlags.None,
new AsyncCallback(SendCallBack), s);
}
catch
{
if (socketsToRemove == null)
{
socketsToRemove = new List<Socket>();
}
socketsToRemove.Add(s);
}
}
if (socketsToRemove != null)
{
socketsToRemove.ForEach(s => { s.Close(); clients.Remove(s); });
}
}
catch(Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
private static void SendCallBack(IAsyncResult result)
{
Socket s = result.AsyncState as Socket;
try
{
s.EndSend(result);
}
catch
{
s.Close();
clients.Remove(s);
}
}
The Send
method serializes the TraceMessage to a MemoryStream
. Then, an asynchronous call to Socket.Send
is made to all clients - asynchronous because we don't want to wait. If there is any exception, then the client socket will be removed from the collection. This is done through a small Lambda expression :-)
If any exception is thrown outside the loop - please do not tTrace that exception! It will end up in a recursion.
SendCallBack
simply calls EndSend
. Exception -> close and remove the socket from the collection.
That's it on the TraceListener side, let's look at the TraceMessage:
TraceMessage
public class TraceMessage
{
public DateTime DateTime { get; set; }
public int Id { get; set; }
public TraceEventType EventType { get; set; }
public string Source { get; set; }
public string Message { get; set; }
public int ProcessId { get; set; }
public string ThreadId { get; set; }
public string CallStack { get; set; }
public string ObjectData { get; set; }
public string MachineName { get; set; }
public string AppDomain { get; set; }
...
}
That is a lot of properties. But there is more: ProcessID
, ProcessName
, MachineName
, and AppDomain
are cached in a static constructor.
private static int _pID;
private static string _pName;
private static string _MachineName;
private static string _AppDomain;
static TraceMessage()
{
_pID = System.Diagnostics.Process.GetCurrentProcess().Id;
_pName = System.Diagnostics.Process.GetCurrentProcess().ProcessName;
_MachineName = System.Environment.MachineName;
_AppDomain = System.AppDomain.CurrentDomain.FriendlyName;
}
We also have a serializer:
private static XmlSerializer xml = new XmlSerializer(typeof(TraceMessage));
public void ToStream(Stream s)
{
xml.Serialize(s, this);
}
public static TraceMessage FromStream(Stream s)
{
return (TraceMessage)xml.Deserialize(s);
}
public static TraceMessage FromStream(TextReader s)
{
return (TraceMessage)xml.Deserialize(s);
}
All other methods are simple constructors which only applies properties.
TraceClient
The TraceClient is a WPF application. We have a TraceClientWindow
(the main window), a TraceDetailWindow
, a TraceMessageCollection
, and a TcpTraceReceiver
. Let's start with the TcpTraceReceiver
.
TcpTraceReceiver
This class simply collects TraceMessages. I'll just explain the OnReceive
method.
private byte[] buffer = new byte[1024];
private TcpClient client = null;
private StringBuilder sb = new StringBuilder();
public delegate void TraceMessageReceivedHandler(IPEndPoint sender,
TraceMessage msg);
public event TraceMessageReceivedHandler OnTraceMessageReceived = null;
private void OnReceive(IAsyncResult result)
{
try
{
int count = client.Client.EndReceive(result);
if (count > 0)
{
for (int i = 0; i < count; i++)
{
sb.Append((char)buffer[i]);
}
if (sb.ToString().Contains("</TraceMessage>"))
{
string xml = sb.ToString();
int lastIdx = xml.IndexOf("</TraceMessage>") +
"</TraceMessage>".Length;
xml = xml.Substring(0, lastIdx);
sb.Remove(0, lastIdx);
if (OnTraceMessageReceived != null)
{
OnTraceMessageReceived(sender,
TraceMessage.FromStream(new StringReader(xml)));
}
}
}
client.Client.BeginReceive(buffer, 0, buffer.Length,
SocketFlags.None, new AsyncCallback(OnReceive), null);
}
catch
{
Connect();
}
}
It's an asynchronous callback from BeginReceive
. First of all, we convert the the byte
array to a StringBuilder
. Next, we look for the end of our XML document (TraceMessages are sent through XML). If we find the end, then we extract the XML string and try to convert it back to a TraceMessage
. Then, we fire our OnTraceMessageReceived
event. At the end, we start listening for more data through BeginReceive
.
Any exception, even an invalid XML document, will close the socket (through Connect
).
TraceMessageCollection
This is an interesting part! The TraceMessageCollection
keeps track of all Trace messages and holds a distinct list of machine names, processes, AppDomains, and thread IDs for filtering. All done through ObservableCollection
. This helps us with binding the WPF-elements.
public class TraceMessageCollection : ObservableCollection<TraceMessage>
{
ObservableCollection<Machine> _machineList = new ObservableCollection<Machine>();
public ObservableCollection<Machine> MachineList
{
get
{
return _machineList;
}
}
protected override void InsertItem(int index, TraceMessage item)
{
Machine m = _machineList.FirstOrDefault(i => i.Name == item.MachineName);
if (m == null)
{
m = new Machine() { Name = item.MachineName };
_machineList.Add(m);
}
m.Add(item);
base.InsertItem(index, item);
}
}
InsertItem
looks for a Machine
entry. If not found, it will create one. Then, it passes the TraceMessage
to the Machine
entry so that the entry can look for Process
es and so on. At last, it simply adds the TraceMessage
to its own collection.
public class Machine
{
public string Name { get; set; }
ObservableCollection<Process> _processList =
new ObservableCollection<Process>();
public ObservableCollection<Process> ProcessList
{
get
{
return _processList;
}
}
public void Add(TraceMessage item)
{
Process p = _processList.FirstOrDefault(i => i.PID == item.ProcessId);
if (p == null)
{
p = new Process() {Machine = this, PID = item.ProcessId,
Name = item.Source };
_processList.Add(p);
}
p.Add(item);
}
}
The Add
looks for a process and so on and so on. I like the new C# 3.0 features :-)
TraceClientWindow
The TraceClientWindow
has a TreeView
to display all the machines, processes etc., and to filter the ListView
. Binding is done through the DataContext
in the OnLoad
event.
<Window x:Class="TraceClient.TraceClientWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Trace Client" Height="500" Width="664"
Loaded="Window_Loaded" Closing="Window_Closing">
<DockPanel>
<ToolBar DockPanel.Dock="Top">
...
</ToolBar>
<StatusBar DockPanel.Dock="Bottom" FlowDirection="RightToLeft">
...
</StatusBar>
<TreeView DockPanel.Dock="Left" Width="200"
ItemsSource="{Binding Path=MachineList}"
SelectedItemChanged="TreeView_SelectedItemChanged">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Path=ProcessList}">
<TextBlock Text="{Binding Path=Name}" />
<HierarchicalDataTemplate.ItemTemplate>
<HierarchicalDataTemplate
ItemsSource="{Binding Path=AppDomainList}">
<TextBlock Text="{Binding}"/>
<HierarchicalDataTemplate.ItemTemplate>
<HierarchicalDataTemplate
ItemsSource="{Binding Path=ThreadList}">
<TextBlock Text="{Binding Path=Name}"/>
<HierarchicalDataTemplate.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=TID}" />
</DataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
</HierarchicalDataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
</HierarchicalDataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<ListView Name="lstTrace" ItemsSource="{Binding}"
SelectionMode="Single" MouseDoubleClick="lstTrace_MouseDoubleClick">
<ListView.View>
<GridView>
<GridViewColumn Header="DateTime"
DisplayMemberBinding="{Binding Path=DateTime}" Width="150" />
...
</GridView>
</ListView.View>
</ListView>
</DockPanel>
</Window>
The TreeView
is interesting: it contains a HierarchicalDataTemplate
bound to each parent node. So, we can declare in XAML how the TreeView
should look like. As all collections are ObservableCollection
, we don't have to care about updates :-)
A word about the OnTraceMessageReceived
event which was bound in the Window_Load
event:
private void r_OnTraceMessageReceived(IPEndPoint sender, TraceMessage msg)
{
try
{
this.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal,
(ThreadStart)delegate {
messages.Add(msg);
if(AutoScroll) lstTrace.ScrollIntoView(msg);
});
}
catch
{
}
}
As in good old WinForms, we must not make cross thread calls! So, we add the new TraceMessage
through the Dispatcher
because after the item is added, all controls will be rebound through WPF. We don't have to care about updates.
Autoscrolling is done through the ScrollIntoView
method.
The last point: Filtering TraceMessages
. I've implemented the TreeView_SelectedItemChanged
event:
private void TreeView_SelectedItemChanged(object sender,
RoutedPropertyChangedEventArgs<object> e)
{
try
{
if (e.NewValue is Machine)
{
Machine m = e.NewValue as Machine;
lstTrace.Items.Filter =
(i => (i as TraceMessage).MachineName == m.Name);
}
else if (e.NewValue is Process)
{
Process p = e.NewValue as Process;
lstTrace.Items.Filter =
(i => (i as TraceMessage).MachineName == p.Machine.Name &&
(i as TraceMessage).ProcessId == p.PID);
}
else if (e.NewValue is ApplicationDomain)
{
ApplicationDomain a = e.NewValue as ApplicationDomain;
lstTrace.Items.Filter =
(i => (i as TraceMessage).MachineName == a.Process.Machine.Name &&
(i as TraceMessage).ProcessId == a.Process.PID &&
(i as TraceMessage).AppDomain == a.Name);
}
else if (e.NewValue is Thread)
{
Thread t = e.NewValue as Thread;
lstTrace.Items.Filter =
(i => (i as TraceMessage).MachineName == t.AppDomain.Process.Machine.Name &&
(i as TraceMessage).ProcessId == t.AppDomain.Process.PID &&
(i as TraceMessage).AppDomain == t.AppDomain.Name &&
(i as TraceMessage).ThreadId == t.TID);
}
else
{
lstTrace.Items.Filter = (i => true);
}
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}
}
The ListView
is very nice. It implements a filter through a Lambda expression. Check which tree node is selected and pass the appropriate Lambda expression.
The TraceDetailWindow
simply shows the TraceMessage
in detail. That's all for now!
ToDo's
TraceClient
: Add a configuration dialog.TraceClient
: More filters.TraceClient
: Add Colours to the listview.TraceMessage
: User-defined message types (e.g., method call).
History
Stay tuned for updates at my blog, or of course here!
- 02.01.2008 - Initial version.
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.