Logging is a crucial part of any production-grade .NET application. Each log message gets associated to a level based on the details being written. Those levels can then be used to filter the results down to what’s relevant for you, but only if you’re consistent with the log levels throughout the codebase.
I’ve listed below the rules of thumb I rely on to decide how to classify a log message. They aren’t perfect by any means, but they help me reason about making the right choice.
The LogLevel Enum
The LogLevel enum defines six log levels, expressed as follows:
public enum LogLevel
{
Trace = 0,
Debug = 1,
Information = 2,
Warning = 3,
Error = 4,
Critical = 5,
None = 6
}
The numerical values of the enum make it easy to filter out logs below the minimum level that you specify at runtime. For example, you could set the production environment to only log Information level messages and above, or bump up it up to Debug in the development environment when trying to solve a pesky, non-reproducible bug.
Let’s start the tour of the log levels from the highest level (Critical) to the lowest level (Trace) since it’s a more natural order to think about log classification. I won’t discuss LogLevel.None
since it’s rarely used in practice.
Critical
Anything that causes the application to stop or that is impossible to recover from should be logged as Critical. These errors are frequently unanticipated, and therefore not caught by any exception handling that you may have written. In practice, I don’t use the Critical log that much since very few conditions should cause an application to completely shut down.
Error
As the name implies, any action that causes an invalid operation, incorrect state, a bug or undesired behaviour should be classified as an error. The main difference with critical errors is that the application continues to run after an error occurs. An error usually needs a corrective action to be taken, be it by the user, the application, or the development team. Retrying the action that caused the error, given the same state, should cause the problem to happen again.
Warning
A warning could lead to an undesired behaviour down the road, but it doesn’t require you to stop executing the code’s happy path just yet. For example, an API call fails, but a default response can be used instead of failing the request. In this case, the warning log message indicates that the code used a default value:
_logger.LogWarning("Request to get current inventory failed. Using default value of {defaultQuantity}", defaultQuantity);
Similarly, an abnormal condition that can be worked around or doesn’t have a direct impact on the main processing should also be logged as a warning:
_logger.LogWarning("Found multiple products matching id {productId}. Using first returned result.", productId);
Information
I’d bet that 80% of all log messages are Information level logs. I use them to keep track of significant events that occur in the application, for example, a user click that performs multiple actions, a batch of data was successfully processed, and so on.
Don’t use the Information level too liberally. It’s the default log level in .NET Core, so it can easily overrun your logging with information of little value if used to excess.
Debug
Debug logs dig in deeper to the events and actions that happen inside the application. I’ll use the debug level to track the performance of certain events, such as query execution time, the number of objects affected by an update, or timing issues. They contain data that comes in handy when trying to fix a bug that can’t be easily reproduced on your local machine.
Debug logs are rarely used on an environment other than development or staging since they can cause performance issues if used to excess. For that reason, it’s best to only add debug logs when and where you find yourself needing them.
Trace
The most detailed level of all, I use Trace to expose the inner workings of the application, such as following the flow of the application and even further detailed statistics about its execution. Just like with Debug, I don’t add Trace level messages right from the start but only as I need them. I’ve never seen a huge difference between Trace and Debug, since neither should be turned on in production.
Be Consistent
I’ve briefly shown the logic I use to decide what gets logged to each log level. But each application is different and has its specific use cases that need to be considered.
Well-structured logs make it easy to understand at a glance what’s happening in an application. It doesn’t really matter what you put in each log level, so long as you’re consistent with it throughout.