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:
EventSource
- Fast and strongly typed. Designed to interface with OS logging systems.ILogger
- An extensible logging system designed to allow you to plug in additional consumers of logging events.DiagnosticSource
- Similar in design toEventSource
, 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.
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!