Logging Levels and Categories in .NET Core
One thing that I never implemented was granular control of logging specific levels by category. It turns out that it’s pretty easy to do, though.
Category Funtamentals | Tags: Logging
Published: 15 January 2021
Logging in .NET Core passes LogLevel and Category whenever something is to be logged. Category corresponds, afaik, to namespaces. You can see this in the default Console based logger configuration provided by the standard Visual Studio .NET Core templates:
Also, as far as I know, these settings refer to “minimum” log levels. Whenever log messages are passed to a logger, a filter is checked to determine if the message should be logged. Τhe “true” which means everything is logged. Later, I implemented the concept of a single minimum level, but this isn’t overly useful since .NET Core logs a ton of messages that, really, are framework diagnostics from the Microsoft/System namespaces.
The mimimum level I implemented that I had implemented in the Logger looks like this:
Where the filter is a simple Func<string, LogLevel, bool>
return logLevel != LogLevel.None && logLevel >= matchedCategory.MinLevel;
Notice how I don’t inspect Category. There is no built-in nice feature for granular logging as the Console logger. But, it is pretty easy to implement.
Following the pattern that the Console-based logger uses, I create this section in my appsettings.json:
Remember that I mentioned that the Namespace is synonymous with Category? That’s why you’ll see the key to the array of objects is Namespace. The class that I will map these settings to is, then, pretty simple. A List<AppLogLevel> is populated along with the connections string name.
In the ConfigureServices section of my Startup.cs, I populate the settings, instantiate the settings object, and pass the connection string name into the extension method that creates the AppLoggerProvider:
In the Configure method of Startup.cs, the UseLogger method is called passing the Console configuration and the ApplicationName:
Since the extension methods for AddLogger/UseLogger have changed significantly, the Extension methods are below. You can see that the AddLogger method is adding the DbContext and the IRepository. The key change here is that the “AddProvider” method, which instantiates the AppLoggerProvider and attaches it to the ServiceProvider, has the AppLoggingSettings passed into it.
Now that the boiler plate setup is out of the way, we can focus on create the filter. We’ll use some simple LINQ expresses to match our settings against category and LogLevel.
The AppLoggerProvider will store references to our filter, appName, and the repository factory. It will also have (3) constructors. One will take a predefined filter, one will take a minimum LogLevel, and one will take the AppLoggingSettings.
The first constructor is self-explanatory. We’re only setting our private members.
The next constructor is a bit more interesting since we define our filter. Note that it takes a “minimum” LogLevel and creates a Func<string, LogLevel, bool> from the LogLevel. This does not take into account Category, so it has limited usefulness.
The last, and most versatile constructor takes the AppLoggingSettings object as an input. Based on this object, a robut filter is created. The category will be compared against the Namespace of each AppLogLevel to find a match. If no match is found, then we look for specifically named entries of “Default” or “Application.” I chose these as the “catch all” defaults. If there are LogLevels in the List<LogLevel> of a matched AppLogLevel, then we see if the passed in LogLevel is in the list and log the message if it is. If there are no LogLevels in the List<LogLevel> then we defer to the similar “minimum” LogLevel comparison. And, if we find no matches at all, then the message will not be logged.
The only other change needed was to change the “IsEnabled” method in the AppLogger to execute the filter:
With these bits of code and configuration options in place, I have fine, granular control of what gets logged. Previously, my logging tables will getting slammed with all of the “System” and “Microsoft” messages. Now, I can turn any specific category (Namespace) completely off if I choose to.
As an aside, Microsoft’s LogLevel enum is defined as below:
ASP.NET Core defines the following log levels, ordered here from least to highest severity.
Trace = 0
For information that is valuable only to a developer debugging an issue. These messages may contain sensitive application data and so should not be enabled in a production environment. Disabled by default. Example:
Credentials: {"User":"someuser", "Password":"P@ssword"}
Debug = 1
For information that has short-term usefulness during development and debugging. Example:
Entering method Configure with flag set to true.
Information = 2
For tracking the general flow of the application. These logs typically have some long-term value. Example:
Request received for path /api/todo
Warning = 3
For abnormal or unexpected events in the application flow. These may include errors or other conditions that do not cause the application to stop, but which may need to be investigated. Handled exceptions are a common place to use the
Warning
log level. Example:FileNotFoundException for file quotes.txt.
Error = 4
For errors and exceptions that cannot be handled. These messages indicate a failure in the current activity or operation (such as the current HTTP request), not an application-wide failure. Example log message:
Cannot insert record due to duplicate key violation.
Critical = 5
For failures that require immediate attention. Examples: data loss scenarios, out of disk space.