In my experience building web services I have found that having rich logging to be one of the most important tools for tracking down issues. Logging allows us to record interesting events and capture some state of the system and share it with operational staff, administrators, and engineers in order to evaluate the health of an application and diagnose and debug issues after they occurred.
The Challenge
In previous versions of ASP.NET we were mostly on our own when it came to logging. Typically we would pull in a third party library like log4net or NLog and use that throughout our application. This worked pretty well, however it also meant that we had likely coupled our code to a particular logging framework and replacing or adding to that framework was quite difficult.
Perhaps some of us thought ahead and wrote our own abstraction layer as to separate the interface from implementation. The problem with that is we now got into the business of writing logging frameworks instead of focusing on the core business logic we ought to focus on.
The Solution
ASP.NET 5 attempts to solve this problem by introducing an entirely new mechanism for logging in our applications that integrates nicely with the dependency injection system. The main idea is this: rather than explicitly creating a logger instance that is tied to some particular framework like log4net, we declare in our constructor that we need an
ILogger(https://github.com/aspnet/Logging/blob/dev/src/Microsoft.Extensions.Logging.Abstractions/ILogger.cs), and the ASP.NET 5 framework figures out how to create it and handles forwarding log entries to various logging providers that are configured ahead of time.
As you can see already, this is not complicated stuff. Instead of using concrete third party implementations of logger instances throughout our application, we just use the Microsoft provided facade. This allows us to decouple our applications from individual logging frameworks with the added benefit that we don’t have to write the abstraction layer ourselves.
Third Party Providers
Currently, two third party providers are supported: Serilog and Elmah.io. It seems that since we’re in the early stages of development, not all third parties have completed their integration just yet. I even noticed that certain “sinks” were not available in Serilog yet when targetting the CoreCLR – like writing to a file (We could target just the full .NET framework though which does have support for file sinks). Elmah.io is a cloud logging service, meaning our logs get written to some service in the cloud and we access them there, not on the local filesystem.
In this post we’ll focus on logging using the default configuration provided by the ASP.NET 5 web application template. Currently that means we will log out to the Debug output window as well as the Console (if it is available).
How to Log
Okay – so how do we use this new logging framework? Lets say we want to log something from our
HomeController. The easiest way of doing that is to add a constructor that requires an
ILogger<HomeController>instance. We can then save this instance to a field to be used by our actions later on.
private ILogger<HomeController> _logger; public HomeController(ILogger<HomeController> logger) { _logger = logger; }
Next we can add a log statement in our
Indexaction:
public IActionResult Index() { _logger.LogInformation("Index action requested at {requestTime}", DateTime.Now); return View(); }
Take note of the
{requestTime}placeholder we are using. Don’t confuse this with C# 6.0 string interpolation, this is more akin to
String.Formatexcept the placeholders are named. This will allow certain logging providers to be able to search and filter based on placeholders, which as you can image can be quite powerful.
Log Levels
You may have noticed we used the
LogInformationmethod above. There are actually a handful of these extension methods (
LogDebug,
LogInformation,
LogWarning, etc) that allow us to associate a
LogLevelwith a given log statement. We can then adjust the framework logging level so that we only log the level of information we are concerned about. For example: we may want to enable the most detailed level of logging in our development environment, but only show potential issues (warnings) and errors in production.
Lets take a look at the documentation for each of the available log levels from the enum definition on GitHub:
- Debug (1): Logs that contain the most detailed messages. These messages may contain sensitive application data. These messages are disabled by default and should never be enabled in a production environment.
- Verbose (2): Logs that are used for interactive investigation during development. These logs should primarily contain information useful for debugging and have no long-term value.
- Information (3): Logs that track the general flow of the application. These logs should have long-term value.
- Warning (4): Logs that highlight an abnormal or unexpected event in the application flow, but do not otherwise cause the application execution to stop.
- Error (5): Logs that highlight when the current flow of execution is stopped due to a failure. These should indicate a failure in the current activity, not an application-wide failure.
- Critical (6): Logs that describe an unrecoverable application or system crash, or a catastrophic failure that requires immediate attention.
- None (int.MaxValue): Not used for writing log messages. Specifies that a logging category should not write any messages.
One interesting thing to note here is that other logging frameworks may use
Debugas the second-most granular log level, with something like
Verboseor
Traceas the most detailed. With the Microsoft logging framework
Debugis the most detailed. I think this makes sense actually – when I’m debugging an issue I want to typically see absolutely everything that can help me diagnose what is going on.
Logging Configuration
When we create a new ASP.NET 5 project, the
Startupclass will contain a few lines of code to configure the logging framework. Let’s take a look at the first few lines of the
Configuremethod:
loggerFactory.MinimumLevel = LogLevel.Information; loggerFactory.AddConsole(); loggerFactory.AddDebug();
The first line is setting the framework logging level. As mentioned above, each time we write to our log, we associate a log level with that entry by calling the appropriate extension method like
LogInformationor
LogDebug. When the Microsoft logging framework receives that entry, it will discard it if it is below the framework minimum level. This is really nice since our application doesn’t have to check all over the place just how detailed the logs should be. We just log everything at various log levels and the framework will know whether to use or discard it.
The next two lines are extension methods that register logging “providers” with the logging framework. We can add as many logging providers with the framework as we like – each time we write a log entry, it will be forwarded to each logging provider. In this case, one provider will write to the Console window (if available), and the other will write to the Debug output window.
Provider Logging Level
In addition to having a framework minimum logging level, each individual provider can configure their own logging levels. For example: we may want to log only warnings or higher to our Console. We can do this by using an overload on the
AddConsoleextension method:
loggerFactory.MinimumLevel = LogLevel.Information; loggerFactory.AddConsole(LogLevel.Warning); loggerFactory.AddDebug();
Now our Debug window will show all messages at
Informationlevel or higher while the Console will only be used for
Warningsor higher.
An Unfortunate Name
Let’s take a look at the following example:
loggerFactory.MinimumLevel = LogLevel.Debug; loggerFactory.AddDebug(); var logger = loggerFactory.CreateLogger("Test"); logger.LogDebug("This is a debug statement.");
You might imagine that if we look at our Debug window, we should see
"This is a debug statement". Well, this is not quite the case. When we call
AddDebugwithout specifying the provider log level, it defaults to… LogLevel.Information.
That means even though we set our framework log level to Debug, we are using the Debug log provider, and using the
LogDebugmethod on our
ILogger, the entry will get dropped because the Debug provider by default only wants
LogLevel.Informationor higher. If we want to log
Debuglevel messages to the Debug window, we have to specify that using an overload on
AddDebug:
loggerFactory.MinimumLevel = LogLevel.Debug; loggerFactory.AddDebug(LogLevel.Debug); var logger = loggerFactory.CreateLogger("Test"); logger.LogDebug("This is a debug statement.");
Searching for Answers
This seemed pretty counter intuitive to me. I actually filed an issue with the Logging team on GitHub to figure out what’s happening here, specifically why
LogLevel.Informationwas chosen as the default log level for the Debug provider. Eilon Lipton (GitHub: Eilon) actually gave a pretty reasonable answer:
The reason for Information being the default is that as a whole we decided that the frameworks (ASP.NET + EF) should emit about ~10 log messages at the Information level or higher on a “typical” request (started, some DB queries, routing, MVC, view rendering, etc.).
If the default was Debug or Verbose then a typical request would have hundreds of log messages flowing through, and that would probably make the log output unusable in most cases.
Okay, fair enough. The reasoning is sensible but I still think it is unfortunate because it is not intuitive to people new to this new logging framework. I had no idea at first that we could specify the provider log level in addition to the root log level and I only figured out what was going on after digging into the source on GitHub.
One promising avenue is there’s a bunch of activity on the Logging repository on GitHub right now and it looks like they’re working on a new way of configuring loggers through JSON (much in the same way that EntityFramework 7 stores its connection string in
appsettings.json). I’m looking forward to seeing what they come up with.
Nonetheless I think what we have today is a really good starting point for making use of the new logging framework and keeping our application separated from our logger implementations so that we can easily add and modify logging providers in the future without having to make sweeping changes across our application. If you’re using the new logging framework in ASP.NET 5 today, let us know your experiences with it below.