Remotely logging Indigo messages using Indigo

I had a couple of hours available while travelling by train to London the other day, so I decided to play with Indigo's message logging facility. Indigo does a great job of logging in/out SOAP messages to/from services. Check the TracingAndLogging sample that comes with the WinFX SDK March CTP on how to enable message logging.

The IndigoMessageWriterTraceListener saves each message to a given directory as an XML file. We have to go and open each file separately in order to read the message. I thought that a GUI-based tool for inspecting the messages would be nice. The obvious thing to do, of course, is to implement a tool that looks for new files in the specified directory and loads them. But of course, no Indigo-specific code is required in this case, so no fun. Also, we need direct access to the directory. Instead, I decided to enable a remote logging facility built using Indigo itself.

The goal

We want to build the necessary infrastructure which will allow us to send to a logging service those messages logged by Indigo. Easy, right?

The approach

I haven't been able to find a way to directly plug my code into Indigo's logging functionality. Instead, I had to write my own Trace listener. Indigo sends each logged message as a string which is the XML representation of the message wrapped by a MessageLogTraceRecord element with time and source information ('in' or 'out' message). So, it should have been very simple. Our Trace listener receives this string and sends it to the logging service. Easy, eh? All we have to do is register our listener...

<source name="IndigoMessageLogTraceSource" switchValue="Verbose">
  <listeners>
    <add name="nose"
         endpoint="http://localhost:8081/log"
         type="Savas.Nose.MessageListener, Savas.Nose" />
  </listeners>
</source>

The above requires the MessageListener class to understand the @endpoint attribute (the code is bellow).

Of course, if it was that simple, I wouldn't have written this entry. So, where's the catch?

The problem

Well, if we are sending logged messages using Indigo, then those messages get logged as well >:-| Doh! Do you see the problem? After the first service-specific message is sent or received, things start to go wrong. Indigo logs the message, which causes a log message to be sent, which causes a log message of the log message to be sent, which causes a log message of the log message... etc. etc. etc. This continues until the maximum number of messages to be logged is reached.

I tried to find whether logging could be deactivated via code. If we could disable it using code, then we could wrap the process of sending the log message inside a DisableLogging()/EnableLogging() pair. Since I couldn't find such a solution, I looked elsewhere. (A way may still exist but I just haven't found it. I hope the Indigo folks will point it out if there is one.)

The solution

While playing with IntelliSense in VS.NET inside an Indigo configuration file I saw an interesting attribute for the <messageLogging> element: @filterTransportMessages. A ha! Could I use this to filter out my logging-related messages? Of course the answer is 'yes'.

All I have to do is give a specific value for the wsa:Action message for all my log messages. These messages don't need to get logged. We get this behaviour by configuring Indigo message logging like this:

<messageLogging logEntireMessage="true"
                maxMessagesToLog="30"
                logMessagesAtServiceLevel="false"
                logMalformedMessages="true"
                logMessagesAtTransportLevel="true"
                filterTransportMessages="true">
  <transportFilters>
    <add xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
         xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing">
      /soap:Envelope/soap:Header[wsa:Action!='urn:savas:nose:msg']
    </add>
  </sportFilters>
</messageLogging>

Cool, eh?

The code

So, here's the code of the MessageListener class:

namespace Savas.Nose
{
  public class MessageListener : TraceListener
  {
    public MessageListener() {}

    protected override string[] GetSupportedAttributes()
    {
      return new string[] { "endpoint" };
    }

    public override void Write(string message)
    {
      this.sendMsg(message);
    }

    public override void WriteLine(string message)
    {
      this.sendMsg(message);
    }

    private void sendMsg(string message)
    {
      try
      {
        XmlDocument doc = new XmlDocument();

        doc.LoadXml(message);
        BasicProfileBinding binding = new BasicProfileBinding();

        binding.SecurityMode = BasicProfileSecurityMode.None;

        INose logger = ChannelFactory.CreateChannel<INose>(
          new Uri(base.Attributes["endpoint"]),
          binding);

        Message msg = Message.CreateMessage(
          MessageVersion.Soap11Addressing1,
          "urn:savas:nose:msg",
          new XmlNodeReader(doc.DocumentElement));

        logger.Send(msg);
      }
      catch (XmlException)
      {}
    }

    [ServiceContract(Namespace = "urn:savas:nose")]
    interface INose
    {
      [OperationContract(IsOneWay = true, Style = ServiceOperationStyle.DocumentBare)]
      void Send(Message msg);
    }
  }
}

I convert the logged string into an XML document first in order to make sure that only XML gets logged. I should probably also check whether the XML document is a SOAP message but that's not important for the purposes of this discussion.

The logging service

I've hosted the logging service (just an operation receiving a generic Message) inside a Windows Form application. Here's a screenshot:

The code for the GUI is trivial and not worth reporting here.

Conclusion

Indigo provides an excellent message-logging facility by saving messages sent/received as files. In this post we saw how this facility could be leveraged in order to log messages at a service. We needed to take advantage of filters in order to avoid the problem of recursive logging of messages.