Logs are a crucial component of contemporary applications and require special consideration. This is especially true when it comes to web development with ASP.NET Core, where there are practically endless integration possibilities between microservices and APIs, and where keeping track of these connections can be very difficult.
When I was looking at some code, I noticed that the logs were always there, but when I looked closer, I realized that they were frequently irrelevant because they contained little to no significant information. Particularly when there is a problem in the production environment, we often let details that can make a big difference escape due to lack of time, distraction, or ignorance.
In this article, we’ll go over some crucial issues relating to logs and how to use them efficiently while adhering to best practices and making sure they’re pertinent for upcoming data analysis.
1. Log Levels and .NET Logging
The logging severity levels are defined by the C# Enum named LogLevel in the.NET context. The Assembly contains extension methods that provide log level indication. Microsoft.Extensions.Logging.Abstractions.
The table of log levels in.NET Core is shown below.
Log Level | Severity | Extension Method | Description |
---|---|---|---|
Trace | 0 | LogTrace() | 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. |
Debug | 1 | LogDebug() | 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 | 2 | LogInformation() | Logs that track the general flow of the application. These logs should have long-term value. |
Warning | 3 | LogWarning() | Logs that highlight an abnormal or unexpected event in the application flow, but do not otherwise cause the application execution to stop. |
Error | 4 | LogError() | 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 | 5 | LogCritical() | Logs that describe an unrecoverable application or system crash, or a catastrophic failure that requires immediate attention. |
None | 6 | None | Not used for writing log messages. Specifies that a logging category should not write any messages. |
These are the log levels, and you should use them in the appropriate situations. For each level, there are some real-world examples below.
Usage Examples for Each Level
public class LogService { private readonly ILogger<LogService> _logger; public LogService(ILogger<LogService> logger) { _logger = logger; } public void ProccessLog() { var user = new User("John Smith", "smith@mail.com", "Kulas Light", null, null); //Trace _logger.LogTrace("Processing request from the user: {Name} - {ProccessLog}", user.Name, nameof(ProccessLog)); //Debug var zipcodeDefault = "92998-3874"; if (user.Zipcode == zipcodeDefault) _logger.LogDebug("The zip code is default for the user: {Name} - {ProccessLog}", user.Name, nameof(ProccessLog)); //Information _logger.LogInformation("Starting execution... - {ProccessLog}", nameof(ProccessLog)); //Warning if (string.IsNullOrEmpty(user.Zipcode)) _logger.LogWarning("The zip code is null or empty for the user: {Name} - {ProccessLog}", user.Name, nameof(ProccessLog)); //Error try { var zipcodeBase = "92998-3874"; var result = false; if (user.Zipcode == zipcodeBase) result = true; } catch (Exception ex) { _logger.LogError(ex.Message, "Error while processing request from the user: {Name} the zipcode is null or empty. - {ProccessLog}", user.Name, nameof(ProccessLog)); } //Critical try { var userPhone = user.Phone; } catch (Exception ex) { _logger.LogCritical(ex.Message, "The phone number is null or empty for the user: {Name}. Please contact immediately the support team! - {ProccessLog}", user.Name, nameof(ProccessLog)); throw; } //None //Not used for writing log messages } }
2. Logging Frameworks or Libraries
The functions in Microsoft Logging Assembly can be used on any system, no matter how big or small, and they satisfy the fundamental requirements for logging. However, some situations call for more customizable logs and detailed logs. The development community is aware of and accepts libraries as being helpful in these situations.
Here are some usage examples for Serilog, one of the more well-known libraries.
Serilog
The log library that has been downloaded most frequently from the NuGet website is Serilog. Serilog NuGet is where you can find it.
Serilog offers diagnostic logging for files, consoles, and a variety of other locations, just like other libraries. It is simple to configure and has a wide range of features for contemporary applications.
Below, a real-world application of its use will be shown along with some of Serilog’s numerous customization options.
Practical Example
1. Create a new console app with .NET 6.
You can do this through Visual Studio 2022 or via the console with the following command:
dotnet new console --framework net6.0
2. Install the following libraries in the latest stable version:
- Serilog
- Serilog.Expressions
- Serilog.Formatting.Compact
- Serilog.Sinks.Console
- Serilog.Sinks.File
3. Create a class called Offer and paste this code in it:
using Serilog; using Serilog.Templates; ExecuteLogs(); void ExecuteLogs() { LogToConsole(); LogToFile(); } void LogToConsole() { var offer = FillOffer(); Log.Logger = new LoggerConfiguration() .Enrich.WithProperty("offerId", offer.Id) .Enrich.WithProperty("productId", offer.ProductId) .Enrich.WithProperty("quantity", offer.Quantity) .WriteTo.Console(new ExpressionTemplate("{ {@t, @mt, @l: if @l = 'Information' then undefined() else @l, @x, ..@p} }\n")) .CreateLogger(); Log.Information("Information about the Offer"); } void LogToFile() { var offer = FillOffer(); Log.Logger = new LoggerConfiguration() .Enrich.WithProperty("offerId", offer.Id) .Enrich.WithProperty("productId", offer.ProductId) .Enrich.WithProperty("quantity", offer.Quantity) .WriteTo.File(new ExpressionTemplate( "{ {@t, @mt, @l: if @l = 'Information' then undefined() else @l, @x, ..@p} }\n"), "Logs\\log.txt", rollingInterval: RollingInterval.Day) .CreateLogger(); Log.Information("Information about the Offer"); } Offer FillOffer() => new Offer(5488, 100808, "Book", 109, 3);
{"@t":"2021-11-19T18:53:57.9627579-03:00","@mt":"Information about the Offer","offerId":5488,"productId":100808,"quantity": 3}
And in the folder: “\bin\Debug\net6.0\Logs” is the created file. Inside it will be the same data that was displayed in the console.
In this example we created two methods:
- “LogToConsole()” – It creates an object called “Offer,” then uses a new instance of “LoggerConfiguration” and adds the properties values to logging with the method “Enrich.WithProperty.” Then the method “WriteTo.Console” displays the data logging in the console, and makes the configuration of the template through the class “ExpressionTemplate.”
- “LogToFile()” – It does the same as the previous method, but uses the “WriteTo.File” method to create a “Logs” folder if it doesn’t exist, and inside it a text file that will store the log data. The “rollingInterval” rule determines the interval in which a new file will be created—in this example, one day—that is, the logs will be written to the same file until the day ends, and then a new file will be created.
Serilog was only briefly used in this demonstration, but it has a lot of useful features. Explore them at your leisure.
3. Best Practices and Recommendations
Structured Logs
When an application uses the Microsoft Logging Assembly or more sophisticated filtering to look for logs, the creation of structured logs is advised. The log recording mechanism needs to receive the string containing the placeholders and their values separately, so it is necessary.
Here’s an illustration of a structured log:
_logger.LogWarning( "The zip code is null or empty for the user: {Name}", user.Name;
String interpolation is a viable option, but you should make sure the registration service is set up to still have access to the message template and property values after the replacement.
An illustration of a structured log using string interpolation can be found below:
_logger.LogWarning( $"The zip code is null or empty for the user: {user.Name}";
Enable Only Appropriate Logs in Production
You should take into account the actual requirements for using each of the log levels before publishing something in the production environment. For instance, excessively detailed logs may cause the server to become overloaded or the system to operate slowly.
So, before publishing anything, it’s a good idea to review all the logs and keep only the ones that are pertinent and won’t cause any sort of overhead. In a production setting, trace and debug logs must be disabled.
Using a Third-Party Logging Library
Always ask the client you’re working with if they use a third-party library and what it is before beginning a new project because you don’t want to waste your time on something that won’t be put into production because it uses private property.
Logging Sensitive Information
Never include private or delicate data in production logs, such as user-related information like passwords or credit card numbers, or any data pertaining to something that cannot be made public. Additionally to being accessible to anyone with access to logged data, this information typically lacks any encryption, making it vulnerable to disclosure in the event that a hacker attack is made against the system.
Writing Relevant Log Messages
The presence of a log message at a crucial point in the code does not guarantee that the system is ready for a thorough analysis, as its use would be unnecessary if the message didn’t make much sense in that situation.
Therefore, when writing log messages, consider what would be the most crucial data for a later analysis of the execution. For instance, always include the message produced by the “Catch” method in messages inside exception blocks.
Utilizing the name of the method used when writing the log is another suggestion that can be seen in the example below. In the example below, we are using the command “nameof(ProccessLog)” to record the name of the method that is responsible for execution.
public void ProccessLog() { try { //execution... } catch (Exception ex) { //The exception message "ex.Message" is being used in the log _logger.LogError(ex.Message, "Error while processing request from the user: {Name} the zipcode is null or empty. - {ProccessLog}", user.Name, nameof(ProccessLog)); } }
Conclusion
We discussed some helpful writing techniques for effective logs in C# and.NET in this article. There are no hard-and-fast guidelines for writing logs; it all depends on the environment in which you are developing. Nevertheless, if you follow these suggestions, your code will undoubtedly get much better.