../_images/sitetitle1.png

Custom Targets

We believe that the most common customization of NuLog that will be sought after, will be custom targets. Here’s what it takes.

Where to Start?

Aiming to be as flexible as possible, there are many different layers of abstraction to our targets. This allows you to create targets with just the amount of functionality you desire.

As an example, let’s take a look at the inheritance tree of our ConsoleTarget:

ConsoleTarget > LayoutTargetBase > TargetBase, ILayoutTarget > ITarget

  • ConsoleTarget - The console target implementation.
  • LayoutTargetBase - Provides standard layout parsing and instantiation functionality, for leverage by implementing targets in their Write methods.
  • ILayoutTarget - Defines the base expected behavior of a target with a layout.
  • TargetBase - Provides an empty implementation of the IDisposable pattern, and a couple helper methods for configuration.
  • ITarget - Defines the expected behavior of a target.

Bare Minimum: Implement the ITarget Interface

The ITarget Interface

A target must implement the ITarget interface. This states that a target must:

  • Be disposable (IDisposable interface)
  • Have a Name
  • Must be able to recieve configuration.
  • Must be able to recieve a log event for writing.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public interface ITarget : IDisposable
{
    /// <summary>
    /// The name of this target, which is used to identify this target in the various rules.
    /// </summary>
    string Name { get; set; }

    /// <summary>
    /// Tells the target to configure itself with the given config.
    /// </summary>
    void Configure(TargetConfig config);

    /// <summary>
    /// Write the given log event.
    /// </summary>
    void Write(LogEvent logEvent);
}

The lowest level implementation of a target may look something like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class HelloWorldTarget : ITarget
{
    public string Name { get; set; }

    public void Configure(TargetConfig config)
    {
        // Nothing to do
    }

    public void Write(LogEvent logEvent)
    {
        Debug.WriteLine(logEvent.Message);
    }

    #region IDisposable Support

    protected virtual void Dispose(bool disposing)
    {
        // Nothing to do
    }

    // This code added to correctly implement the disposable pattern.
    public void Dispose()
    {
        Dispose(true);
    }

    #endregion IDisposable Support
}

Using Your Custom Target

Once you’ve created your custom target, you need to reference it in your config:

1
2
3
4
5
6
7
8
<nulog>
  <!-- ... -->
  <targets>
    <target name="myCustomTarget"
             type="NuLogSnippets:NuLogSnippets.Docs.CustomTargets.HelloWorldTarget" />
  </targets>
  <!-- ... -->
</nulog>

Less is More: Extend the TargetBase

You can actually implement your custom target in fewer lines of code by leverage the given TargetBase abstract class:

1
2
3
4
5
6
7
public class HelloWorldShortTarget : TargetBase
{
    public override void Write(LogEvent logEvent)
    {
        Debug.WriteLine(logEvent.Message);
    }
}

Most Useful: Extend the LayoutTargetBase

Since it is text data that we’re generally logging, the abstract class that provides the best starting point for most custom targets, will be the LayoutTargetBase. With the LayoutTargetBase, you get:

  • Everything you get with the `TargetBase`, plus:
  • A protected ILayout instance that is automatically configured using the layout attribute from the config.

The StandardFactory specifically recognizes the ILayoutTarget interface, which has a special Configure method which recieves an instance of ILayoutFactory. This inverts the dependency on ILayoutFactory, allowing it to be injected into the target from above, in compliance of the Dependency Inversion Principle.

When extending the LayoutTargetBase, you get access to the automatically configured layout member:

1
2
3
4
5
6
7
8
public class HelloLayoutTarget : LayoutTargetBase
{
    public override void Write(LogEvent logEvent)
    {
        var formattedText = this.Layout.Format(logEvent);
        Debug.Write(formattedText);
    }
}

Now, when referencing your custom target in your config, you can set the layout:

1
2
3
<target name="myCustomTarget"
           type="NuLogSnippets:NuLogSnippets.Docs.CustomTargets.HelloLayoutTarget"
           layout="${DateTime:'{0:MM/dd/yyyy}'} | ${Message}\r\n" />

Target configuration

If you’re building a custom target, chances are, you’ll need some kind of custom configuration for your target.

Configuration Interface

The ITarget interface defines a method for receiving configuration from the factory:

void Configure(TargetConfig config);

This method recieves a TargetConfig object, which contains the Name of the target, the Type name of the object, and a Dictionary of Properties.

The properties are read from the XML configuration, and are the attributes of the XML target element from the config. Consider the following example:

1
2
3
<target name="mytarget"
           type="NuLogSnippets:NuLogSnippets.Docs.CustomTargets.CustomConfigTarget"
           myCustomProperty="Yellow, World!"/>

The myCustomProperty property would be included in the TargetConfig object’s Properties Dictionary with a key of “myCustomProperty” and a value of “Yellow, World!”.

myCustomProperty could then be retrieved in our configure method, like so:

1
2
3
4
public void Configure(TargetConfig config)
{
    this.MyCustomProperty = Convert.ToString(config.Properties["myCustomProperty"]);
}

Configuration Helpers in TargetBase

The TargetBase abstract class provides a couple helpers to make accessing properties a bit easier:

1
2
3
4
5
6
7
8
9
public override void Configure(TargetConfig config)
{
    base.Configure(config);

    this.MyStringProperty = GetProperty<string>(config, "oneFish");

    bool isTwoFish;
    this.MyBoolProperty = TryGetProperty<bool>(config, "twoFish", out isTwoFish) && isTwoFish;
}