Share via


Creating a custom event sink

patterns & practices Developer Center

The Semantic Logging Application Block includes a number of event sinks such as the Rolling Flat File sink, SQL Database sink, and Azure Table Storage sink. These sinks implement the IObserver<EventEntry> interface and provide an implementation of the OnNext method that specifies how to handle each event message.

You can use a custom event sink in both the in-process and out-of-process scenarios. However, if you want to support IntelliSense for a custom sink in the configuration file for the Out-of-Process Host application you must do additional work to add this capability.

This topic describes:

  • Creating a custom event sink
  • Using a custom event sink in-process
  • Using a custom event sink out-of-process
  • Defining a configuration element for a custom event sink
  • Adding IntelliSense support for a custom event sink

Creating a custom event sink

To develop a custom event sink, you create a class that implements the IObserver<EventEntry> interface. This interface contains three methods: OnCompleted, OnError, and OnNext.

public interface IObserver<in T>
{
  void OnCompleted();
  void OnError(Exception error);
  void OnNext(T value);
}

The following example code shows how to create a custom sink named EmailSink that first writes the event to any configured stores using a configured event formatter, and then sends details of the event to specified recipients as an email message.

public sealed class EmailSink : IObserver<EventEntry>
{
  private const string DefaultSubject = "Email Sink Extension";
  private IEventTextFormatter formatter;
  private MailAddress sender;
  private MailAddressCollection recipients = new MailAddressCollection();
  private string subject;
  private string host;
  private int port;
  private NetworkCredential credentials;

  public EmailSink(string host, int port,
    string recipients, string subject, string credentials, 
    IEventTextFormatter formatter)
  {
    this.formatter = formatter ?? new EventTextFormatter();
    this.host = host;
    this.port = GuardPort(port);
    this.credentials = CredentialManager.GetCredentials(credentials);
    this.sender = new MailAddress(this.credentials.UserName);
    this.recipients.Add(GuardRecipients(recipients));
    this.subject = subject ?? DefaultSubject;
  }

  public void OnNext(EventEntry entry)
  {
    if (entry != null)
    {
      using (var writer = new StringWriter())
      {
        this.formatter.WriteEvent(entry, writer);
        Post(writer.ToString());
      }
    }
  }

  public void OnCompleted()  {
  }

  public void OnError(Exception error)  {
  }

  private void Post(string body)
  {
    using (var client = new SmtpClient(this.host, this.port) 
      { Credentials = this.credentials, EnableSsl = true })
    using (var message = new MailMessage(this.sender, this.recipients[0]) 
      { Body = body, Subject = this.subject })
    {
      for (int i = 1; i < this.recipients.Count; i++)
        message.CC.Add(this.recipients[i]);

      try
      {
        client.Send(message);
      }
      catch (SmtpException e)
      {
        SemanticLoggingEventSource.Log.CustomSinkUnhandledFault(
          "SMTP error sending email: " + e.Message);
      }
      catch (InvalidOperationException e)
      {
        SemanticLoggingEventSource.Log.CustomSinkUnhandledFault(
          "Configuration error sending email: " + e.Message);
      }
      catch (...)
      {
        // additional exception handling code here.
      }

    }
  }

  private static int GuardPort(int port)
  {
    if (port < 0)
      throw new ArgumentOutOfRangeException("port");

    return port;
  }

  private static string GuardRecipients(string recipients)
  {
    if (recipients == null)
      throw new ArgumentNullException("recipients");
    if (string.IsNullOrWhiteSpace(recipients))
      throw new ArgumentException(
        "The recipients cannot be empty", "recipients");

    return recipients;
  }
}

Note

For clarity, this simple example uses synchronous code to log the event to an email message. However, synchronous I/O is generally not good practice. You can examine the implementations of the event sinks provided for the application block by downloading the source code from CodePlex to see production-ready examples.

You must also create an extension method that returns a new instance of the listener, and an extension method that returns a SinkSubscription. The following code shows the extension methods for the example email sink.

public static class EmailSinkExtensions
{
  public static EventListener CreateListener(string host, int port,
         string recipients, string subject, string credentials,
         IEventTextFormatter formatter = null)
  {
    var listener = new ObservableEventListener();
    listener.LogToEmail(host, port, recipients, subject, credentials, formatter);
    return listener;
  }

  public static SinkSubscription<EmailSink> LogToEmail(
         this IObservable<EventEntry> eventStream, string host, int port,
         string recipients, string subject, string credentials,
         IEventTextFormatter formatter = null)
  {
    var sink = new EmailSink(host, port, recipients, subject, credentials, formatter);
    var subscription = eventStream.Subscribe(sink);
    return new SinkSubscription<EmailSink>(subscription, sink);
  }
}

You must compile the sink classes into a DLL and reference it in the in-process scenario, or copy it into the same folder as the configuration file in the out-of-process scenario.

You can use the CustomSinkUnhandledFault method in the SemanticLoggingEventSource class to log any unhandled exceptions in your custom sink class, but you should always avoid throwing exceptions from the methods in your custom sink classes.

Note

You will also find it useful to explore the source code of some of the built-in event sinks such as the ConsoleSink or the SqlDatabaseSink. The SqlDatabaseSink illustrates an approach to buffering events in the sink.

Using a custom event sink in-process

Using event sinks when you are using the Semantic Logging Application Block in-process is a simple case of instantiating and configuring in code the sink you want to use, and then subscribing the sink to the observable event listener. You do exactly the same with your custom event sinks.

The following example code shows how to create and configure an instance of the EmailSink event sink class and call the LogToConsole extension method to subscribe the sink to the observable event listener.

ObservableEventListener listener = new ObservableEventListener();

listener.LogToEmail("smtp.live.com", 587, "bill@adatum.com", "In Proc Sample", "etw");

listener.EnableEvents(MyCompanyEventSource.Log,EventLevel.LogAlways, Keywords.All);

You can also pass a formatter to the LogToEmail method if you want to customize the format of the email message.

Using a custom event sink out-of-process

When you use a custom sink in the out-of-process scenario, you must compile it into a DLL and then instantiate it by adding entries to the configuration file of the Out-of-Process host application. The compiled DLL that contains your custom sink implementation must be copied to the folder from where you will run the Out-of-Process Host application. The application scans this folder for extension classes when it starts.

To add the custom sink to the configuration file, you use the customSink element that is part of the standard configuration schema for the Out-of-Process host. The parameters to pass to the formatter are specified using nested parameter elements. You must ensure that the order and type of the parameter elements in the configuration file match the order and type of the constructor parameters in the custom sink class.

The following excerpt from an XML configuration file shows how to use a custom sink class named EmailSink.

<customSink name="SimpleCustomEmailSink"
            type ="CustomSinkExtension.EmailSink, CustomSinkExtension">
  <sources>
    <eventSource name="MyCompany" level="Critical" />
  </sources>
  <parameters>
    <parameter name="host" type="System.String" value="smtp.live.com" />
    <parameter name="port" type="System.Int32" value="587" />
    <parameter name="recipients" type="System.String" value="bill@adatum.com" />
    <parameter name="subject" type="System.String" value="Simple Custom Email Sink" />
    <parameter name="credentials" type="System.String" value="etw" />
  </parameters>
</customSink>

Defining a configuration element for a custom event sink

You have seen how you can use a custom event sink out-of-process by adding the customSink element to the configuration file of the Out-of-Process host application. However, you can improve the administrative experience for adding a custom sink by creating a specific element for it that can be used in the same way as the elements for the event sinks provided with the Semantic Logging Application Block.

For example, the following excerpt from a configuration file shows how you could use a custom element named emailSink with the Out-of-Process Host application.

<emailSink xmlns="urn:sample.etw.emailsink"
           credentials="etw" host="smtp.live.com" name="ExtensionEmailSink"
           port="587" recipients="bill@adatum.com" 
           subject="Message from Email sink">
  <etw:sources>
    <etw:eventSource name="MyCompany" level="Critical" />
  </etw:sources>
</emailSink>

To use a custom XML element in this way you must create a class that implements the ISinkElement interface, and that supports reading your custom configuration data from the XML file. The following code shows an example.

public class EmailSinkElement : ISinkElement
{
  private readonly XName sinkName = XName.Get("emailSink", "urn:sample.etw.emailsink");

  public bool CanCreateSink(XElement element)
  {
    return element.Name == this.sinkName;
  }

  public IObserver<EventEntry> CreateSink(XElement element)
  {
    var host = (string)element.Attribute("host");
    var port = (int)element.Attribute("port");
    var recipients = (string)element.Attribute("recipients");
    var subject = (string)element.Attribute("subject");
    var credentials = (string)element.Attribute("credentials");

    var formatter = FormatterElementFactory.Get(element);
    return new EmailSink(host, port, recipients, subject, credentials, formatter);
  }
}

The class must then be compiled into a DLL and placed in the same folder as the XML configuration file for the Out-of-Process Host application.

Adding IntelliSense support for a custom event sink

The event sinks provided with the Semantic Logging Application Block have built-in IntelliSense support for configuring the Out-of-Process host. However, your custom sinks do not support this by default.

If you have defined a custom XML element and attributes for use when configuring a custom sink, you can create a schema that enables IntelliSense support in the Visual Studio XML editor. When you do this, the XML configuration file will look like the following example.

<?xml version="1.0" encoding="utf-8" ?>
<configuration
 xmlns="https://schemas.microsoft.com/practices/2013/entlib/semanticlogging/etw"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns:etw="https://schemas.microsoft.com/practices/2013/entlib/semanticlogging/etw"
 xsi:schemaLocation="urn:sample.etw.emailsink.\EmailSinkElement.xsd
 https://schemas.microsoft.com/practices/2013/entlib/semanticlogging/etw
 SemanticLogging-svc.xsd">
...
<traceEventService />

<sinks>
  <emailSink xmlns="urn:sample.etw.emailsink" 
             credentials="etw" host="smtp.live.com" name="ExtensionEmailSink"
             port="587" recipients="bill@adatum.com"
             subject="Message from Email sink">
    <etw:sources>
      <etw:eventSource name="MyCompany" level="Critical" />
    </etw:sources>
  </emailSink>
  ...

Note

Notice how the emailSink element has a custom XML namespace and the configuration file uses a schemaLocation attribute to specify the location of the schema file.

The schema you create must specify the XML structure for the custom sink element, and apply any restrictions or rules for the values of the attributes and any child elements you use. In this example, the namespace urn:demo.etw.emailsink is used to define the emailSink element and its content.

<?xml version="1.0" encoding="utf-8"?>
<xs:schema id="EmailSinkElement"
    targetNamespace="urn:sample.etw.emailsink"
    xmlns="urn:sample.etw.emailsink"
    xmlns:etw="https://schemas.microsoft.com/practices/2013/entlib/semanticlogging/etw"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"           
    elementFormDefault="qualified"
    attributeFormDefault="unqualified">

  <xs:element name="emailSink">
    <xs:complexType>
      <xs:sequence>
        <xs:any minOccurs="0" maxOccurs="unbounded" processContents="skip"/>
      </xs:sequence>
      <xs:attribute name="name" type="xs:string" use="required" />
      <xs:attribute name="host" type="xs:string" use="required" />
      <xs:attribute name="port" type="xs:int" use="required" />
      <xs:attribute name="credentials" type="xs:string" use="required" />
      <xs:attribute name="recipients" type="xs:string" use="required" />
      <xs:attribute name="subject" type="xs:string" use="optional" />
    </xs:complexType>
  </xs:element>
</xs:schema>

You must copy the custom XML schema file to the same folder where you store the XML configuration file for the Out-of-Process Host application. This folder should already contain the DLL that implements your custom sink and your custom EmailSinkElement type.

Next Topic | Previous Topic | Home | Community