Logging with Ninject Interception and Serilog - Part 1

The Problem

I have a project with a lot of places where I want to “cross-cut” the concern of logging into. This is also called “AOP” - Aspect Oriented Programming. AOP claims to “increase modularity by allowing the separation of cross-cutting concerns… by adding additional behavior to existing code (an advice) without modifying the code itself”. By managing dependencies differently, I’ve found myself moving into the anti-pattern of constructor over-injection. Adding an ILogger interface as a new dependency for multiple classes is going to be a burden. This experiment is to see if I can avoid making my constructors too heavy, while broadly covering my code with basic logging.

I plan to use Ninject for this. I’d like to achieve broad logging of events and errors AROUND methods of my choosing. I will also end up needing more specific, behavior-driven logging where I can see what is happening throughout a vertical slice of my application, but this will probably require adding an ILogger dependency later.

Enter Ninject Interception

For my logging that isn’t specific to the behavior of the app, which includes “Method: x executed with x parameters and returned with x values” and “An error was thrown while processing x Entity” - I’m going to use Ninject.Extensions.Interception. A good introduction is here. And some good coverage of the topic in the book Mastering Ninject for Dependency Injection: .

I decided to use Serilog because of its ability to “destructure” objects, which is simply .ToJson() and inserting the result into the formatted string output. Serilog also leaves the doors open to whatever “sink” I’d like to send logs to in the future. I chose Seq as the sink for the greenfield applicaiton that I’m working on.

The way Ninject.Extensions.Interception works is that it provides a wrapper “proxy” around any virtual method that is owned by a dependency that you are injecting. So:

namespace Project {

	// We are intercepting a method in this class
	public class InterceptedTestClass : IInterceptedTestClass {

		public virtual void InterceptMe() {

		};
	}

	// We are going to intercept this method
    public interface IInterceptedTestClass {

        void InterceptMe();
    }

    // Unit test project's Ninject Service Module for configuring bindings
    public class ServiceModuleTest : NinjectModule {

    public override void Load() {

    		Bind<IInterceptedTestClass>().To<InterceptedTestClass>();
        }
    }
}

// The test to prove it
[Fact]
public void Bind_test_class() {

    using (var kernel = new StandardKernel(new ServiceModuleTest())) {

        var testClass = kernel.Get<IInterceptedTestClass>();

        Assert.IsType<InterceptedTestClass>(testClass)
    }
}

In order to get the project we run Install-Package Ninject.Extensions.Interception.DynamicProxy

This will install the infrastructure needed and the actual Ninject.Extensions.Interception project.

To instal the Serilog and the SerilogMetrics libraries - Install-Package Serilog and Install-Package SerilogMetrics

First thing I need to do is create some interceptors. These are the classes you need to inherit in order to use the interception library. I chose to use a mix of both the SimpleInterceptor and the IInterceptor to build my own interceptor classes:

#region Standard logging interceptor

public class LogInterceptor : SimpleInterceptor {

    private readonly ILogger _logger;

    public LogInterceptor(ILogger logger) {

        _logger = logger;
    }

    protected override void BeforeInvoke(IInvocation invocation) {

    }

    protected override void AfterInvoke(IInvocation invocation) {

        _logger.ForContext(invocation.Request.Target.GetType())
            .Information("Method: {Name} called with arguments {@Arguments} returned {@ReturnValue}",
                invocation.Request.Method.Name,
                invocation.Request.Arguments,
				invocation.ReturnValue);
    }
}

#endregion

#region Error logging interceptor

public class LogErrorInterceptor : IInterceptor {

    private readonly ILogger _logger;

    public LogErrorInterceptor(ILogger logger) {

        _logger = logger;
    }

    public void Intercept(IInvocation invocation) {

        try {

            invocation.Proceed();
        }
        catch (Exception exception) {

            _logger.ForContext(invocation.Request.Target.GetType())
                .Error(exception, "Error at Method: {@Name} Arguments: {@Arguments}",
                    invocation.Request.Method.Name,
                    invocation.Request.Arguments);

            throw;
        }
    }
}

#endregion

#region Timer interceptor

public class TimerInterceptor : IInterceptor {

    private readonly string _timerName;
    private readonly ILogger _logger;

    public TimerInterceptor(string timerName, ILogger logger) {

        _timerName = timerName;
        _logger = logger;
    }

    public void Intercept(IInvocation invocation) {

        using (_logger.ForContext(invocation.Request.Target.GetType())
            .BeginTimedOperation(_timerName, invocation.Request.Method.Name)) {
            invocation.Proceed();
        }
    }
}

#endregion

Here, I have three utility interceptor classes. They are going to take care of my AOP-style logging. I have a simple LogInterceptor that is just going to write which method and with which parameters is being called. This uses the SimpleInterceptor provided by the extension. The SimpleInterceptor allows you to call BeforeInvoke and AfterInvoke surrounding the actual method call. I can also use some properties of the actual invocation using the IInvocation interface that is passed into the interceptor’s methods e.g. invocation.Request.Method.Name and invocation.Request.Arguments.

So to use this SimpleInterceptor class, I define the interception within our Ninject binding definition:

public class ServiceModuleTest : NinjectModule {

public override void Load() {

		Bind<IInterceptedTestClass>()
			.To<InterceptedTestClass>()
			.Intercept().With<LogInterceptor>();
    }
}

And in my Serilog sink (in my case it’s Seq), I get this output on the call of a method. This is the output of logging a method called “CreatePart”.

Here’s the BeforeInvoke interceptor log output:

{
  "Timestamp": "2015-07-01T11:50:39.9740367-04:00",
  "Properties": [
    {
      "Name": "Name",
      "Value": "CreatePart"
    },
    {
      "Name": "Arguments",
      "Value": [
        "FF56D337-A113-E511-B04D-005056BC69BC",
        "D990B9D2-2355-E211-8AB5-001EC9E3D8F3"
      ]
    }
  ],
  "MessageTemplateTokens": [
    {
      "Text": "Method: "
    },
    {
      "PropertyName": "Name"
    },
    {
      "Text": " called with arguments "
    },
    {
      "PropertyName": "Arguments"
    }
  ],
  "EventType": "$0573D34D",
  "Level": "Information",
  "Id": "event-d06e91cf822c08d20500000000000000",
  "Links": {
    "Self": "api/events/event-d06e91cf822c08d20500000000000000{?download,render}",
    "Group": "api/events/resources"
  }
}

And here’s the AfterInvoke interceptor log output:

{
  "Timestamp": "2015-07-01T11:50:48.2028595-04:00",
  "Properties": [
    {
      "Name": "Name",
      "Value": "CreatePart"
    },
    {
      "Name": "ReturnValue",
      "Value": "GA1008-36-07G|04"
    }
  ],
  "MessageTemplateTokens": [
    {
      "Text": "Method: "
    },
    {
      "PropertyName": "Name"
    },
    {
      "Text": " returned "
    },
    {
      "PropertyName": "ReturnValue"
    }
  ],
  "EventType": "$2BEAAA84",
  "Level": "Information",
  "Id": "event-d5563033822c08d20800000000000000",
  "Links": {
    "Self": "api/events/event-d5563033822c08d20800000000000000{?download,render}",
    "Group": "api/events/resources"
  }
}

So that’s the first bit of my basic implementation of Ninject.Extensions.Interception for AOP-style logging. I’ll follow up with more details about the LogErrorInterceptor and the TimerInterceptor as well as the awesome InterceptAttribute.

If you like this post and want to see more come right to your inbox - be sure to sign up for my newsletter. I always try to cover interesting and helpful topics for .NET developers like you!


Related Posts:

Tweet
comments powered by Disqus