Logging in the ASP.NET Core framework is implemented as an extensible set of providers that allows you to easily plug in new providers without having to change your logging code itself. The docs give a great summary of how to use the ILogger and ILoggerFactory in your application and how to pipe the output to the console, to Serilog, to Azure etc. However, the ILogger isn't the only logging possibility in ASP.NET Core.

In this post, I'll show how to use the DiagnosticSource logging system in your ASP.NET Core application.

ASP.NET Core logging systems

There are actually three logging system in ASP.NET Core:

  1. EventSource - Fast and strongly typed. Designed to interface with OS logging systems.
  2. ILogger - An extensible logging system designed to allow you to plug in additional consumers of logging events.
  3. DiagnosticSource - Similar in design to EventSource, but does not require the logged data be serialisable.

EventSource has been available since the .NET Framework 4.5 and is used extensively by the framework to instrument itself. The data that gets logged is strongly typed, but must be serialisable as the data is sent out of the process to be logged. Ultimately, EventSource is designed to interface with the underlying operating system's logging infrastructure, e.g. Event Tracing for Windows (ETW) or LTTng on Linux.

The ILogger infrastructure is the most commonly used logging ASP.NET Core infrastructure. You can log to the infrastructure by injecting an instance of ILogger into your classes, and calling, for example, ILogger.LogInformation(). The infrastructure is designed for logging strings only, but does allow you to pass objects as additional parameters which can be used for structured logging (such as that provided by SeriLog). Generally speaking, the ILogger implementation will be the infrastructure you want to use in your applications, so check out the documentation if you are not familiar with it.

The DiagnosticSource infrastructure is very similar to the EventSource infrastructure, but the data being logged does not leave the process, so it does not need to be serialisable. There is also an adapter to allow converting DiagnosticSource events to ETW events which can be useful in some cases. It is worth reading the users guide for DiagnosticSource on GitHub if you wish to use it in your code.

When to use DiagnosticSource vs ILogger?

The ASP.NET Core internals use both the ILogger and the DiagnosticSource infrastructure to instrument itself. Generally speaking, and unsurprisingly, DiagnosticSource is used strictly for diagnostics. It records events such as "Microsoft.AspNetCore.Mvc.BeforeViewComponent" and "Microsoft.AspNetCore.Mvc.ViewNotFound".

In contrast, the ILogger is used to log more specific information such as "Executing JsonResult, writing value {Value}." or when an error occurs such as ""JSON input formatter threw an exception.".

So in essence, you should only use DiagnosticSource for infrastructure related events, for tracing the flow of your application process. Generally, ILogger will be the appropriate interface in almost all cases.

An example project using DiagnosticSource

For the rest of this post I'll show an example of how to log events to DiagnosticSource, and how to write a listener to consume them. This example will simply log to the DiagnosticSource when some custom middleware executes, and the listener will write details about the current request to the console. You can find the example project here.

Adding the necessary dependencies.

We'll start by adding the NuGet packages we're going to need for our DiagnosticSource to our project.json (I haven't moved to csproj based projects yet):

{
  dependencies: {
    ...
    "Microsoft.Extensions.DiagnosticAdapter": "1.1.0",
    "System.Diagnostics.DiagnosticSource": "4.3.0"
  }
}

Strictly speaking, the System.Diagnostics.DiagnosticSource package is the only one required, but we will add the adapter to give us an easier way to write a listener later.

Logging to the DiagnosticSource from middleware

Next, we'll create the custom middleware. This middleware doesn't do anything other than log to the diagnostic source:

public class DemoMiddleware  
{
    private readonly RequestDelegate _next;
    private readonly DiagnosticSource _diagnostics;

    public DemoMiddleware(RequestDelegate next, DiagnosticSource diagnosticSource)
    {
        _next = next;
        _diagnostics = diagnosticSource;
    }

    public async Task Invoke(HttpContext context)
    {
        if (_diagnostics.IsEnabled("DiagnosticListenerExample.MiddlewareStarting"))
        {
            _diagnostics.Write("DiagnosticListenerExample.MiddlewareStarting",
                new
                {
                    httpContext = context
                });
        }

        await _next.Invoke(context);
    }
}

This shows the standard way to log using a DiagnosticSource. You inject the DiagnosticSource into the constructor of the middleware for use when the middleware executes.

When you intend to log an event, you first check that there is a listener for the specific event. This approach keeps the logger lightweight, as the code contained within the body of the if statement is only executed if a listener is attached.

In order to create the log, you use the Write method, providing the event name and the data that should be logged. The data to be logged is generally passed as an anonymous object. In this case, the HttpContext is passed to the attached listeners, which they can use to log the data in any ways they sees fit.

Creating a diagnostic listener

There are a number of ways to create a listener that consumes DiagnosticSource events, but one of the easiest approaches is to use the functionality provided by the Microsoft.Extensions.DiagnosticAdapter package.

To create a listener, you can create a POCO class that contains a method designed to accept parameters of the appropriate type. You then decorate the method with a [DiagnosticName] attribute, providing the event name to listen for:

public class DemoDiagnosticListener  
{
    [DiagnosticName("DiagnosticListenerExample.MiddlewareStarting")]
    public virtual void OnMiddlewareStarting(HttpContext httpContext)
    {
        Console.WriteLine($"Demo Middleware Starting, path: {httpContext.Request.Path}");
    }
}

In this example, the OnMiddlewareStarting() method is configured to handle the "DiagnosticListenerExample.MiddlewareStarting" diagnostic event. The HttpContext, that is provided when the event is logged is passed to the method as it has the same name, httpContext that was provided when the event was logged.

Hopefully one of the advantages of the DiagnosticSource infrastructure is apparent in that you can log anything provided as data. We have access to the full HttpContext object that was passed, so we can choose to log anything it contains (just the request path in this case).

Wiring up the DiagnosticListener

All that remains is to hook up our listener and middleware pipeline in our Startup.Configure method:

public class Startup  
{
    public void Configure(IApplicationBuilder app, DiagnosticListener diagnosticListener)
    {
        // Listen for middleware events and log them to the console.
        var listener = new DemoDiagnosticListener();
        diagnosticListener.SubscribeWithAdapter(listener);

        app.UseMiddleware<DemoMiddleware>();
        app.Run(async (context) =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    }
}

A DiagnosticListener is injected into the Configure method from the DI container. This is the actual class that is used to subscribe to diagnostic events. We use the SubscribeWithAdapter extension method from the Microsoft.Extensions.DiagnosticAdapter package to register our DemoDiagnosticListener. This hooks into the [DiagnosticName] attribute to register our events, so that the listener is invoked when the event is written.

Finally, we configure the middleware pipeline with out demo middleware, and a simple 'Hello world' endpoint to the pipeline.

Running the example

At this point we're all set to run the example. If we hit any page, we just get the 'Hello world' output, no matter the path.

The endpoint

However, if we check the console, we can see the DemoMiddleware has been raising diagnostic events. These have been captured by the DemoDiagnosticListener which logs the path to the console:

Now listening on: http://localhost:5000  
Application started. Press Ctrl+C to shut down.  
Demo Middleware Starting, path: /  
Demo Middleware Starting, path: /a/path  
Demo Middleware Starting, path: /another/path  
Demo Middleware Starting, path: /one/more  

Summary

And that's it, we have successfully written and consumed a DiagnosticSource. As I stated earlier, you are more likely to use the ILogger in your applications than DiagnosticSource, but hopefully now you will able to use it should you need to. Do let me know in the comments if there's anything I've missed or got wrong!